<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\AuditLog;
use App\Entity\Composant;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\Piece;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
use App\Repository\PieceRepository;
use Doctrine\Common\Collections\Collection;
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:check-missing-custom-field-values',
    description: 'List missing or empty custom field values for pieces and composants',
)]
final class CheckMissingCustomFieldValuesCommand extends Command
{
    public function __construct(
        private readonly PieceRepository $pieces,
        private readonly ComposantRepository $composants,
        private readonly AuditLogRepository $auditLogs,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('entity', null, InputOption::VALUE_REQUIRED, 'piece, composant or all', 'all')
            ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Audit entries inspected per entity', '200')
            ->addOption('max-rows', null, InputOption::VALUE_REQUIRED, 'Maximum rows displayed in the final table', '300')
            ->addOption('recoverable-only', null, InputOption::VALUE_NONE, 'Show only rows recoverable from audit')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io              = new SymfonyStyle($input, $output);
        $entityScope     = (string) $input->getOption('entity');
        $limit           = max(1, (int) $input->getOption('limit'));
        $maxRows         = max(1, (int) $input->getOption('max-rows'));
        $recoverableOnly = (bool) $input->getOption('recoverable-only');

        if (!in_array($entityScope, ['all', 'piece', 'composant'], true)) {
            $io->error('Invalid --entity value. Use: all, piece, composant');

            return Command::FAILURE;
        }

        $rows   = [];
        $counts = [
            'piece'     => 0,
            'composant' => 0,
        ];

        if ('all' === $entityScope || 'piece' === $entityScope) {
            foreach ($this->pieces->findAll() as $piece) {
                if (!$piece instanceof Piece) {
                    continue;
                }

                $pieceRows = $this->inspectPiece($piece, $limit, $recoverableOnly);
                $counts['piece'] += count($pieceRows);
                $rows = [...$rows, ...$pieceRows];
            }
        }

        if ('all' === $entityScope || 'composant' === $entityScope) {
            foreach ($this->composants->findAll() as $composant) {
                if (!$composant instanceof Composant) {
                    continue;
                }

                $composantRows = $this->inspectComposant($composant, $limit, $recoverableOnly);
                $counts['composant'] += count($composantRows);
                $rows = [...$rows, ...$composantRows];
            }
        }

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

            return Command::SUCCESS;
        }

        $displayRows = array_slice($rows, 0, $maxRows);
        $io->table(
            ['Entity', 'ID', 'Name', 'Reference', 'Category', 'Field', 'Issue', 'Recoverable', 'Audit value'],
            $displayRows,
        );

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

        $io->note(sprintf(
            'Missing/empty values found: pieces=%d, composants=%d, total=%d.',
            $counts['piece'],
            $counts['composant'],
            count($rows),
        ));

        return Command::SUCCESS;
    }

    /**
     * @return list<array<int, string>>
     */
    private function inspectPiece(Piece $piece, int $limit, bool $recoverableOnly): array
    {
        $type = $piece->getTypePiece();
        if (null === $type) {
            return [];
        }

        return $this->inspectEntity(
            entityType: 'piece',
            entityId: (string) $piece->getId(),
            entityName: $piece->getName(),
            entityReference: $piece->getReference() ?? '',
            typeName: $type->getName(),
            definitions: $type->getPieceCustomFields(),
            currentValues: $piece->getCustomFieldValues(),
            limit: $limit,
            recoverableOnly: $recoverableOnly,
        );
    }

    /**
     * @return list<array<int, string>>
     */
    private function inspectComposant(Composant $composant, int $limit, bool $recoverableOnly): array
    {
        $type = $composant->getTypeComposant();
        if (null === $type) {
            return [];
        }

        return $this->inspectEntity(
            entityType: 'composant',
            entityId: (string) $composant->getId(),
            entityName: $composant->getName(),
            entityReference: $composant->getReference() ?? '',
            typeName: $type->getName(),
            definitions: $type->getComponentCustomFields(),
            currentValues: $composant->getCustomFieldValues(),
            limit: $limit,
            recoverableOnly: $recoverableOnly,
        );
    }

    /**
     * @return list<array<int, string>>
     */
    private function inspectEntity(
        string $entityType,
        string $entityId,
        string $entityName,
        string $entityReference,
        string $typeName,
        Collection $definitions,
        Collection $currentValues,
        int $limit,
        bool $recoverableOnly,
    ): array {
        if (0 === $definitions->count()) {
            return [];
        }

        $currentValuesByFieldId = $this->indexCurrentValues($currentValues);
        $history                = $this->auditLogs->findEntityHistory($entityType, $entityId, $limit);
        $historicalValues       = $this->extractHistoricalValues($history);
        $rows                   = [];

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

            $currentValue = $currentValuesByFieldId[$definition->getId()] ?? null;
            $issue        = null;

            if (!$currentValue instanceof CustomFieldValue) {
                $issue = 'missing';
            } elseif ('' === trim($currentValue->getValue())) {
                $issue = 'empty';
            }

            if (null === $issue) {
                continue;
            }

            $auditCandidate = $historicalValues[$this->normalizeFieldName($definition->getName())] ?? null;
            if ($recoverableOnly && null === $auditCandidate) {
                continue;
            }
            $rows[] = [
                $entityType,
                $entityId,
                $entityName,
                $entityReference,
                $typeName,
                $definition->getName(),
                $issue,
                $auditCandidate ? 'yes' : 'no',
                $auditCandidate['value'] ?? '',
            ];
        }

        return $rows;
    }

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

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