<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\AuditLog;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
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\InputArgument;
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 is_array;
use function is_string;
use function ksort;
use function preg_replace;
use function sprintf;
use function str_starts_with;
use function strlen;
use function strtolower;
use function trim;

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

    protected function configure(): void
    {
        $this
            ->addArgument('pieceId', InputArgument::REQUIRED, 'Piece ID to restore')
            ->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', '500')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io      = new SymfonyStyle($input, $output);
        $pieceId = (string) $input->getArgument('pieceId');
        $apply   = (bool) $input->getOption('apply');
        $limit   = max(1, (int) $input->getOption('limit'));

        $piece = $this->pieces->find($pieceId);
        if (!$piece instanceof Piece) {
            $io->error(sprintf('Piece not found: %s', $pieceId));

            return Command::FAILURE;
        }

        $type = $piece->getTypePiece();
        if (null === $type) {
            $io->error('This piece has no category (typePiece).');

            return Command::FAILURE;
        }

        $definitions = $type->getPieceCustomFields();
        if (0 === $definitions->count()) {
            $io->warning('This piece category has no current custom field definitions.');

            return Command::SUCCESS;
        }

        $history = $this->auditLogs->findEntityHistory('piece', $pieceId, $limit);
        if ([] === $history) {
            $io->warning('No audit history found for this piece.');

            return Command::SUCCESS;
        }

        $historicalValues = $this->extractHistoricalValues($history);
        if ([] === $historicalValues) {
            $io->warning('No historical custom field values were found in audit logs.');

            return Command::SUCCESS;
        }

        $currentValuesByFieldId = $this->indexCurrentValues($piece->getCustomFieldValues());
        $plannedRows            = [];
        $changesCount           = 0;

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

            $normalizedName = $this->normalizeFieldName($definition->getName());
            if ('' === $normalizedName || !isset($historicalValues[$normalizedName])) {
                continue;
            }

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

            $candidate     = $historicalValues[$normalizedName];
            $plannedRows[] = [
                $definition->getName(),
                $candidate['value'],
                $candidate['sourceDate'],
                $currentValue ? 'update-empty' : 'create-missing',
            ];
            ++$changesCount;

            if (!$apply) {
                continue;
            }

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

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

        if (0 === $changesCount) {
            $io->success('No missing or empty custom field values needed restoration.');

            return Command::SUCCESS;
        }

        if ($apply) {
            $this->em->flush();
        }

        $io->table(
            ['Field', 'Restored value', 'Audit date', 'Action'],
            $plannedRows,
        );

        if ($apply) {
            $io->success(sprintf('%d custom field value(s) restored.', $changesCount));
        } else {
            $io->note(sprintf(
                'Dry-run only. Re-run with --apply to persist %d restoration(s).',
                $changesCount,
            ));
        }

        return Command::SUCCESS;
    }

    /**
     * @param list<AuditLog> $history
     *
     * @return array<string, array{value: string, sourceDate: string, sourceField: 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;
                }

                $rawName        = trim(substr($field, strlen('customField:')));
                $normalizedName = $this->normalizeFieldName($rawName);
                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'),
                    'sourceField' => $rawName,
                ];
            }
        }

        ksort($values);

        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));
    }
}
