<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Cart\LegacyOrderImport;

final readonly class LegacyOrderImportMapping
{
    public const string UNRESOLVED = '__A_COMPLETER__';

    /**
     * @param array<string, string|null> $variants
     * @param array<string, string|null> $paymentMethods
     * @param array<string, string|null> $deliveryMethods
     */
    private function __construct(
        public array $variants,
        public array $paymentMethods,
        public array $deliveryMethods,
        public ?int $schemaVersion = null,
        public ?string $sourceId = null,
        public ?string $sourceKey = null,
        public ?string $scope = null,
    ) {
    }

    public static function empty(): self
    {
        return new self([], [], []);
    }

    public static function fromFile(string $path): self
    {
        if (!is_file($path) || !is_readable($path)) {
            throw new \InvalidArgumentException(\sprintf('Le fichier de correspondance "%s" est introuvable ou illisible.', $path));
        }

        $contents = file_get_contents($path);
        if (false === $contents) {
            throw new \InvalidArgumentException(\sprintf('Impossible de lire le fichier de correspondance "%s".', $path));
        }

        try {
            $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            throw new \InvalidArgumentException(\sprintf('Le fichier de correspondance n\'est pas un JSON valide : %s', $exception->getMessage()), previous: $exception);
        }

        if (!\is_array($data)) {
            throw new \InvalidArgumentException('Le fichier de correspondance doit contenir un objet JSON.');
        }

        $schemaVersion = $data['schema_version'] ?? null;
        if (null !== $schemaVersion && 1 !== $schemaVersion) {
            throw new \InvalidArgumentException('La version du fichier de correspondance est inconnue.');
        }

        $source = $data['source'] ?? null;
        if (null !== $source && !\is_array($source)) {
            throw new \InvalidArgumentException('La cle JSON "source" doit contenir un objet.');
        }

        $source ??= [];
        $sourceId = self::normalizeOptionalString($source['source_id'] ?? null, 'source.source_id');
        $sourceKey = self::normalizeOptionalString($source['source_key'] ?? null, 'source.source_key');
        $scope = self::normalizeOptionalString($source['scope'] ?? null, 'source.scope');

        if (1 === $schemaVersion) {
            if (null === $sourceId) {
                throw new \InvalidArgumentException('Un fichier de correspondance version 1 doit conserver source.source_id.');
            }

            if (null === $sourceKey) {
                throw new \InvalidArgumentException('Un fichier de correspondance version 1 doit conserver source.source_key.');
            }

            if (null === $scope) {
                throw new \InvalidArgumentException('Un fichier de correspondance version 1 doit conserver source.scope.');
            }

            if (!\in_array($scope, ['all', 'paid'], true)) {
                throw new \InvalidArgumentException('La cle JSON "source.scope" doit valoir "all" ou "paid".');
            }
        }

        return new self(
            self::normalizeMap($data['variants'] ?? [], 'variants', allowNull: true),
            self::normalizeMap($data['payment_methods'] ?? [], 'payment_methods', allowNull: true),
            self::normalizeMap($data['delivery_methods'] ?? [], 'delivery_methods', allowNull: true),
            $schemaVersion,
            $sourceId,
            $sourceKey,
            $scope,
        );
    }

    public function hasVariant(string $sourceId): bool
    {
        return \array_key_exists(mb_strtolower($sourceId), $this->variants);
    }

    public function variant(string $sourceId): ?string
    {
        return $this->variants[mb_strtolower($sourceId)] ?? null;
    }

    public function hasPaymentMethod(string $sourceMethod): bool
    {
        return \array_key_exists(mb_strtolower($sourceMethod), $this->paymentMethods);
    }

    public function paymentMethod(string $sourceMethod): ?string
    {
        return $this->paymentMethods[mb_strtolower($sourceMethod)] ?? null;
    }

    public function hasDeliveryMethod(string $sourceId): bool
    {
        return \array_key_exists(mb_strtolower($sourceId), $this->deliveryMethods);
    }

    public function deliveryMethod(string $sourceId): ?string
    {
        return $this->deliveryMethods[mb_strtolower($sourceId)] ?? null;
    }

    /**
     * @return list<string>
     */
    public function contextErrors(string $sourceId, string $sourceKey, string $scope): array
    {
        $errors = [];

        if (null !== $this->sourceId && $this->sourceId !== $sourceId) {
            $errors[] = \sprintf('Le fichier de correspondance appartient a la source "%s", pas a "%s".', $this->sourceId, $sourceId);
        }

        if (null !== $this->sourceKey && !hash_equals($this->sourceKey, $sourceKey)) {
            $errors[] = 'L empreinte de source du fichier de correspondance ne correspond pas a ce lancement.';
        }

        if (null !== $this->scope && $this->scope !== $scope) {
            $errors[] = \sprintf('Le fichier de correspondance a ete genere pour le perimetre "%s", pas "%s".', $this->scope, $scope);
        }

        return $errors;
    }

    /**
     * @return array<string, string|null>
     */
    private static function normalizeMap(mixed $value, string $name, bool $allowNull): array
    {
        if (!\is_array($value)) {
            throw new \InvalidArgumentException(\sprintf('La cle JSON "%s" doit contenir un objet.', $name));
        }

        $normalized = [];

        foreach ($value as $source => $target) {
            if (!\is_string($source) || '' === mb_trim($source)) {
                throw new \InvalidArgumentException(\sprintf('Chaque cle de "%s" doit etre une chaine non vide.', $name));
            }

            $normalizedSource = mb_strtolower(mb_trim($source));
            if (\array_key_exists($normalizedSource, $normalized)) {
                throw new \InvalidArgumentException(\sprintf('La cle "%s" de "%s" est definie plusieurs fois apres normalisation.', $source, $name));
            }

            if (null === $target && $allowNull) {
                $normalized[$normalizedSource] = null;

                continue;
            }

            if (!\is_string($target) || '' === mb_trim($target)) {
                throw new \InvalidArgumentException(\sprintf('La correspondance "%s" de "%s" doit etre une chaine non vide ou null.', $source, $name));
            }

            $normalized[$normalizedSource] = mb_trim($target);
        }

        return $normalized;
    }

    private static function normalizeOptionalString(mixed $value, string $name): ?string
    {
        if (null === $value) {
            return null;
        }

        if (!\is_string($value) || '' === mb_trim($value)) {
            throw new \InvalidArgumentException(\sprintf('La cle JSON "%s" doit contenir une chaine non vide.', $name));
        }

        return mb_trim($value);
    }
}
