<?php

namespace App\Services\Catalog;

use App\Services\Keywords\ProductKeywordParser;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;

class CatalogProductImportValidator
{
    public const MIN_PRODUCTS = 2;

    public function __construct(
        private CatalogProductJsonFormatter $formatter = new CatalogProductJsonFormatter,
        private ProductKeywordParser $keywordParser = new ProductKeywordParser,
    ) {}

    /**
     * @return array{products: list<array<string, mixed>>, error: ?string}
     */
    public function parse(string $json): array
    {
        $json = trim($json);

        if ($json === '') {
            return [
                'products' => [],
                'error' => 'Paste JSON or upload a file with at least '.self::MIN_PRODUCTS.' products.',
            ];
        }

        try {
            $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            return [
                'products' => [],
                'error' => 'Invalid JSON: '.$exception->getMessage(),
            ];
        }

        if (! is_array($decoded)) {
            return [
                'products' => [],
                'error' => 'JSON must be an object with a products array, or a bare products array.',
            ];
        }

        if (array_is_list($decoded)) {
            return [
                'products' => $this->listRows($decoded),
                'error' => null,
            ];
        }

        if (! isset($decoded['products']) || ! is_array($decoded['products'])) {
            return [
                'products' => [],
                'error' => 'JSON must include a products array with at least '.self::MIN_PRODUCTS.' items.',
            ];
        }

        return [
            'products' => $this->listRows($decoded['products']),
            'error' => null,
        ];
    }

    /**
     * @return array{valid: bool, errors: list<string>, products: list<array<string, mixed>>}
     */
    public function validate(string $json): array
    {
        $parsed = $this->parse($json);

        if ($parsed['error'] !== null) {
            return [
                'valid' => false,
                'errors' => [$parsed['error']],
                'products' => [],
            ];
        }

        $errors = [];
        $normalized = [];

        foreach ($parsed['products'] as $index => $row) {
            if (! is_array($row)) {
                $errors[] = 'Product #'.($index + 1).' must be an object.';

                continue;
            }

            $row = $this->formatter->flattenRow($row);

            $validator = Validator::make($row, [
                'name' => ['required', 'string', 'min:2', 'max:500'],
                'url' => ['required', 'url', 'max:2000'],
                'price' => ['required', 'string', 'max:50'],
                'image_url' => ['required', 'url', 'max:2000'],
                'list_price' => ['nullable', 'string', 'max:50'],
                'rating' => ['nullable', 'string', 'max:20'],
                'review_count' => ['nullable', 'string', 'max:20'],
                'keyword_primary' => ['required', 'string', 'min:2', 'max:80'],
                'keyword_related' => ['nullable', 'array', 'max:5'],
                'keyword_related.*' => ['string', 'min:2', 'max:80'],
                'popularity_note' => ['nullable', 'string', 'max:500'],
            ]);

            if ($validator->fails()) {
                foreach ($validator->errors()->all() as $message) {
                    $errors[] = 'Product #'.($index + 1).': '.$message;
                }

                continue;
            }

            $validated = $validator->validated();
            $productKeywords = $this->keywordParser->parse(
                null,
                $validated['keyword_primary'],
                $validated['keyword_related'] ?? [],
            );

            $normalized[] = [
                'name' => trim((string) $validated['name']),
                'url' => trim((string) $validated['url']),
                'price' => trim((string) $validated['price']),
                'list_price' => filled($validated['list_price'] ?? null) ? trim((string) $validated['list_price']) : null,
                'rating' => filled($validated['rating'] ?? null) ? trim((string) $validated['rating']) : null,
                'review_count' => filled($validated['review_count'] ?? null) ? trim((string) $validated['review_count']) : null,
                'image_url' => trim((string) $validated['image_url']),
                'product_keywords' => $productKeywords,
                'keyword_primary' => $productKeywords['primary'],
                'keyword_related' => $productKeywords['related'],
                'popularity_note' => filled($validated['popularity_note'] ?? null)
                    ? trim((string) $validated['popularity_note'])
                    : $this->defaultPopularityNote($validated),
                'discovery_source' => 'json',
            ];
        }

        if (count($normalized) < self::MIN_PRODUCTS) {
            $errors[] = 'Provide at least '.self::MIN_PRODUCTS.' valid products (found '.count($normalized).').';
        }

        return [
            'valid' => $errors === [],
            'errors' => $errors,
            'products' => $normalized,
        ];
    }

    /**
     * @throws InvalidArgumentException
     */
    public function validatedProducts(string $json): array
    {
        $result = $this->validate($json);

        if (! $result['valid']) {
            throw new InvalidArgumentException(implode(' ', $result['errors']));
        }

        return $result['products'];
    }

    public function exampleJson(): string
    {
        return $this->formatter->encode($this->formatter->exampleProducts());
    }

    /**
     * @param  list<array<string, mixed>>  $products
     */
    public function encodeProducts(array $products): string
    {
        return $this->formatter->encode($products, includeExampleMeta: false);
    }

    /**
     * @param  array<string, mixed>  $row
     */
    private function defaultPopularityNote(array $row): string
    {
        $parts = [];

        if (filled($row['rating'] ?? null)) {
            $parts[] = trim((string) $row['rating']).' stars';
        }

        if (filled($row['review_count'] ?? null)) {
            $parts[] = trim((string) $row['review_count']).' reviews';
        }

        return $parts !== [] ? implode(', ', $parts) : 'Imported product';
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function listRows(array $rows): array
    {
        return array_values(array_filter($rows, fn (mixed $row): bool => is_array($row)));
    }
}
