<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\AuditLog;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Repository\AuditLogRepository;
use App\Repository\PieceRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use function array_key_exists;
use function array_slice;
use function count;
use function iconv;
use function in_array;
use function is_array;
use function is_string;
use function preg_replace;
use function sprintf;
use function str_starts_with;
use function strtolower;
use function trim;

#[AsCommand(
    name: 'app:restore-recoverable-piece-custom-field-values',
    description: 'Restore all recoverable missing or empty custom field values for pieces',
)]
final class RestoreRecoverablePieceCustomFieldValuesCommand extends Command
{
    public function __construct(
        private readonly PieceRepository $pieces,
        private readonly AuditLogRepository $auditLogs,
        private readonly EntityManagerInterface $em,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('apply', null, InputOption::VALUE_NONE, 'Persist restored values instead of dry-run mode')
            ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of audit entries to inspect per piece', '500')
            ->addOption('category', null, InputOption::VALUE_REQUIRED, 'Only process pieces whose ModelType name matches this category')
            ->addOption('piece-id', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Restrict to one or more piece IDs')
            ->addOption('max-rows', null, InputOption::VALUE_REQUIRED, 'Maximum rows displayed in the preview table', '300')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io          = new SymfonyStyle($input, $output);
        $apply       = (bool) $input->getOption('apply');
        $limit       = max(1, (int) $input->getOption('limit'));
        $maxRows     = max(1, (int) $input->getOption('max-rows'));
        $category    = $this->normalizeOptionalString($input->getOption('category'));
        $pieceIdsRaw = $input->getOption('piece-id');
        $pieceIds    = is_array($pieceIdsRaw) ? array_values(array_filter(array_map('strval', $pieceIdsRaw))) : [];

        $rows         = [];
        $changesCount = 0;
        $pieceCount   = 0;

        foreach ($this->pieces->findAll() as $piece) {
            if (!$piece instanceof Piece) {
                continue;
            }

            if ([] !== $pieceIds && !in_array((string) $piece->getId(), $pieceIds, true)) {
                continue;
            }

            $type = $piece->getTypePiece();
            if (!$type instanceof ModelType) {
                continue;
            }

            if (null !== $category && $this->normalizeFieldName($type->getName()) !== $this->normalizeFieldName($category)) {
                continue;
            }

            $pieceRows = $this->restorePiece($piece, $limit, $apply);
            if ([] === $pieceRows) {
                continue;
            }

            ++$pieceCount;
            $changesCount += count($pieceRows);
            $rows = [...$rows, ...$pieceRows];
        }

        if ([] === $rows) {
            $io->success('No recoverable piece custom field values found.');

            return Command::SUCCESS;
        }

        $displayRows = array_slice($rows, 0, $maxRows);
        $io->table(
            ['Piece ID', 'Name', 'Reference', 'Category', 'Field', 'Restored value', 'Audit date', 'Action'],
            $displayRows,
        );

        if (count($rows) > $maxRows) {
            $io->warning(sprintf('Output truncated: showing %d of %d row(s).', $maxRows, count($rows)));
        }

        if ($apply) {
            $this->em->flush();
            $io->success(sprintf('%d value(s) restored across %d piece(s).', $changesCount, $pieceCount));
        } else {
            $io->note(sprintf(
                'Dry-run only. %d value(s) recoverable across %d piece(s). Re-run with --apply to persist.',
                $changesCount,
                $pieceCount,
            ));
        }

        return Command::SUCCESS;
    }

    /**
     * @return list<array<int, string>>
     */
    private function restorePiece(Piece $piece, int $limit, bool $apply): array
    {
        $type = $piece->getTypePiece();
        if (!$type instanceof ModelType) {
            return [];
        }

        $definitions = $type->getPieceCustomFields();
        if (0 === $definitions->count()) {
            return [];
        }

        $history = $this->auditLogs->findEntityHistory('piece', (string) $piece->getId(), $limit);
        if ([] === $history) {
            return [];
        }

        $historicalValues = $this->extractHistoricalValues($history);
        if ([] === $historicalValues) {
            return [];
        }

        $currentValuesByFieldId = $this->indexCurrentValues($piece->getCustomFieldValues());
        $rows                   = [];

        foreach ($definitions as $definition) {
            if (!$definition instanceof CustomField) {
                continue;
            }

            $normalizedName = $this->normalizeFieldName($definition->getName());
            $candidate      = $historicalValues[$normalizedName] ?? null;
            if (null === $candidate) {
                continue;
            }

            $currentValue  = $currentValuesByFieldId[$definition->getId()] ?? null;
            $shouldRestore = null === $currentValue || '' === trim($currentValue->getValue());
            if (!$shouldRestore) {
                continue;
            }

            $action = $currentValue instanceof CustomFieldValue ? 'update-empty' : 'create-missing';
            $rows[] = [
                (string) $piece->getId(),
                $piece->getName(),
                $piece->getReference() ?? '',
                $type->getName(),
                $definition->getName(),
                $candidate['value'],
                $candidate['sourceDate'],
                $action,
            ];

            if (!$apply) {
                continue;
            }

            if (!$currentValue instanceof CustomFieldValue) {
                $currentValue = new CustomFieldValue();
                $currentValue->setPiece($piece);
                $currentValue->setCustomField($definition);
                $this->em->persist($currentValue);
            }

            $currentValue->setValue($candidate['value']);
        }

        return $rows;
    }

    /**
     * @param list<AuditLog> $history
     *
     * @return array<string, array{value: string, sourceDate: string}>
     */
    private function extractHistoricalValues(array $history): array
    {
        $values = [];

        foreach ($history as $log) {
            $diff = $log->getDiff();
            if (!is_array($diff)) {
                continue;
            }

            foreach ($diff as $field => $change) {
                if (!is_string($field) || !str_starts_with($field, 'customField:') || !is_array($change)) {
                    continue;
                }

                $normalizedName = $this->normalizeFieldName(trim(substr($field, 12)));
                if ('' === $normalizedName || array_key_exists($normalizedName, $values)) {
                    continue;
                }

                $candidate = $this->extractCandidateValue($change);
                if (null === $candidate) {
                    continue;
                }

                $values[$normalizedName] = [
                    'value'      => $candidate,
                    'sourceDate' => $log->getCreatedAt()->format('Y-m-d H:i:s'),
                ];
            }
        }

        return $values;
    }

    /**
     * @param array{from?: mixed, to?: mixed} $change
     */
    private function extractCandidateValue(array $change): ?string
    {
        $to = $change['to'] ?? null;
        if (is_string($to) && '' !== trim($to)) {
            return $to;
        }

        $from = $change['from'] ?? null;
        if (is_string($from) && '' !== trim($from)) {
            return $from;
        }

        return null;
    }

    /**
     * @return array<string, CustomFieldValue>
     */
    private function indexCurrentValues(Collection $customFieldValues): array
    {
        $indexed = [];

        foreach ($customFieldValues as $customFieldValue) {
            if (!$customFieldValue instanceof CustomFieldValue) {
                continue;
            }

            $indexed[$customFieldValue->getCustomField()->getId()] = $customFieldValue;
        }

        return $indexed;
    }

    private function normalizeFieldName(string $name): string
    {
        $normalized = trim($name);
        if ('' === $normalized) {
            return '';
        }

        $transliterated = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
        if (false !== $transliterated) {
            $normalized = $transliterated;
        }

        $normalized = strtolower($normalized);
        $normalized = (string) preg_replace('/[^a-z0-9]+/', ' ', $normalized);

        return trim((string) preg_replace('/\s+/', ' ', $normalized));
    }

    private function normalizeOptionalString(mixed $value): ?string
    {
        if (!is_string($value)) {
            return null;
        }

        $trimmed = trim($value);

        return '' === $trimmed ? null : $trimmed;
    }
}
