<?php

declare(strict_types=1);

namespace App\Administration\Application\Service\Import;

use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\TeacherColumnMapping;
use App\Administration\Domain\Model\Import\TeacherImportBatch;
use App\Administration\Domain\Model\Import\TeacherImportField;
use App\Administration\Domain\Repository\ClassRepository;
use App\Administration\Domain\Repository\SavedTeacherColumnMappingRepository;
use App\Administration\Domain\Repository\SubjectRepository;
use App\Administration\Domain\Repository\TeacherImportBatchRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;

use function array_map;
use function count;
use function in_array;

use InvalidArgumentException;

use function trim;

/**
 * Orchestre la chaîne d'import d'enseignants : parse → détection → mapping → validation.
 *
 * @see FR77: Import enseignants via CSV
 */
final readonly class TeacherImportOrchestrator
{
    public function __construct(
        private CsvParser $csvParser,
        private XlsxParser $xlsxParser,
        private ImportFormatDetector $formatDetector,
        private TeacherColumnMappingSuggester $mappingSuggester,
        private SubjectRepository $subjectRepository,
        private ClassRepository $classRepository,
        private TeacherImportBatchRepository $teacherImportBatchRepository,
        private SavedTeacherColumnMappingRepository $savedMappingRepository,
        private ExistingTeacherFinder $existingTeacherFinder,
        private TeacherDuplicateDetector $duplicateDetector,
        private Clock $clock,
    ) {
    }

    /**
     * Analyse un fichier uploadé : parse, détecte le format, suggère un mapping,
     * crée le batch et enregistre les lignes mappées.
     *
     * @return array{batch: TeacherImportBatch, suggestedMapping: array<string, TeacherImportField>}
     */
    public function analyzeFile(string $filePath, string $extension, string $originalFilename, TenantId $tenantId): array
    {
        $parseResult = match ($extension) {
            'csv', 'txt' => $this->csvParser->parse($filePath),
            'xlsx', 'xls' => $this->xlsxParser->parse($filePath),
            default => throw new InvalidArgumentException('Format non supporté. Utilisez CSV ou XLSX.'),
        };

        $detectedFormat = $this->formatDetector->detecter($parseResult->columns);
        $suggestedMapping = $this->suggestMapping($parseResult->columns, $detectedFormat, $tenantId);

        $batch = TeacherImportBatch::creer(
            tenantId: $tenantId,
            originalFilename: $originalFilename,
            totalRows: $parseResult->totalRows(),
            detectedColumns: $parseResult->columns,
            detectedFormat: $detectedFormat,
            createdAt: $this->clock->now(),
        );

        $rows = $this->mapRows($parseResult, $suggestedMapping);
        $batch->enregistrerLignes($rows);

        $this->teacherImportBatchRepository->save($batch);

        return ['batch' => $batch, 'suggestedMapping' => $suggestedMapping];
    }

    /**
     * Applique un mapping de colonnes sur un batch existant et re-mappe les lignes.
     */
    public function applyMapping(TeacherImportBatch $batch, TeacherColumnMapping $columnMapping): void
    {
        $batch->appliquerMapping($columnMapping);

        $remapped = [];
        foreach ($batch->lignes() as $row) {
            $mappedData = [];
            foreach ($columnMapping->mapping as $column => $field) {
                $mappedData[$field->value] = $row->rawData[$column] ?? '';
            }
            $remapped[] = new ImportRow($row->lineNumber, $row->rawData, $mappedData);
        }
        $batch->enregistrerLignes($remapped);

        $this->teacherImportBatchRepository->save($batch);
    }

    /**
     * Valide les lignes du batch et retourne les résultats avec les matières et classes inconnues.
     *
     * @return array{validatedRows: list<ImportRow>, report: ImportReport, unknownSubjects: list<string>, unknownClasses: list<string>}
     */
    public function generatePreview(TeacherImportBatch $batch, TenantId $tenantId): array
    {
        $existingSubjects = $this->getExistingSubjectNames($tenantId);
        $existingClasses = $this->getExistingClassNames($tenantId);
        $validator = new TeacherImportRowValidator($existingSubjects, $existingClasses);

        $validatedRows = $validator->validerTout($batch->lignes());

        $existingTeachers = $this->existingTeacherFinder->findAllForTenant($tenantId);
        $validatedRows = $this->duplicateDetector->detecter($validatedRows, $existingTeachers);

        $batch->enregistrerLignes($validatedRows);
        $this->teacherImportBatchRepository->save($batch);

        $report = ImportReport::fromValidatedRows($validatedRows);
        $unknownSubjects = $this->detectUnknownValues($validatedRows, TeacherImportField::SUBJECTS, $existingSubjects);
        $unknownClasses = $this->detectUnknownValues($validatedRows, TeacherImportField::CLASSES, $existingClasses);

        return [
            'validatedRows' => $validatedRows,
            'report' => $report,
            'unknownSubjects' => $unknownSubjects,
            'unknownClasses' => $unknownClasses,
        ];
    }

    /**
     * Prépare le batch pour la confirmation : re-valide si nécessaire
     * et filtre les lignes selon les options choisies par l'utilisateur.
     *
     * La détection de doublons est ré-appliquée après re-validation
     * pour ne pas perdre les erreurs _duplicate.
     */
    public function prepareForConfirmation(
        TeacherImportBatch $batch,
        bool $createMissingSubjects,
        bool $importValidOnly,
        TenantId $tenantId,
    ): void {
        if ($createMissingSubjects) {
            $validator = new TeacherImportRowValidator();
            // Strip old errors before re-validating — the previous validation
            // may have added subject/class errors that we no longer want.
            $cleanRows = array_map(
                static fn (ImportRow $row) => new ImportRow($row->lineNumber, $row->rawData, $row->mappedData),
                $batch->lignes(),
            );
            $revalidated = $validator->validerTout($cleanRows);

            $existingTeachers = $this->existingTeacherFinder->findAllForTenant($tenantId);
            $revalidated = $this->duplicateDetector->detecter($revalidated, $existingTeachers);

            $batch->enregistrerLignes($revalidated);
        }

        if ($importValidOnly) {
            $batch->enregistrerLignes($batch->lignesValides());
        }

        if ($batch->mapping !== null) {
            $this->savedMappingRepository->save(
                $batch->tenantId,
                $batch->mapping->format,
                $batch->mapping->mapping,
            );
        }

        $this->teacherImportBatchRepository->save($batch);
    }

    /**
     * @param list<string> $columns
     *
     * @return array<string, TeacherImportField>
     */
    private function suggestMapping(array $columns, KnownImportFormat $format, TenantId $tenantId): array
    {
        $saved = $this->savedMappingRepository->findByTenantAndFormat($tenantId, $format);

        if ($saved !== null && $this->savedMappingMatchesColumns($saved, $columns)) {
            return $saved;
        }

        return $this->mappingSuggester->suggerer($columns, $format);
    }

    /**
     * Vérifie que le mapping sauvegardé correspond exactement aux colonnes du fichier.
     *
     * Retourne false si le fichier contient des colonnes qui pourraient être mappées
     * mais ne le sont pas par le mapping sauvegardé (ex: colonne « Matières » absente
     * d'un mapping sauvegardé à 3 colonnes).
     *
     * @param array<string, TeacherImportField> $mapping
     * @param list<string>                      $columns
     */
    private function savedMappingMatchesColumns(array $mapping, array $columns): bool
    {
        foreach (array_keys($mapping) as $column) {
            if (!in_array($column, $columns, true)) {
                return false;
            }
        }

        // Reject saved mapping if file has more columns than the mapping covers:
        // the auto-detection might map them and the user expects to see them.
        $autoMapping = $this->mappingSuggester->suggerer($columns, KnownImportFormat::CUSTOM);
        if (count($autoMapping) > count($mapping)) {
            return false;
        }

        return true;
    }

    /**
     * @param array<string, TeacherImportField> $mapping
     *
     * @return list<ImportRow>
     */
    private function mapRows(FileParseResult $parseResult, array $mapping): array
    {
        $rows = [];
        $lineNumber = 1;

        foreach ($parseResult->rows as $rawData) {
            $mappedData = [];
            foreach ($mapping as $column => $field) {
                $mappedData[$field->value] = $rawData[$column] ?? '';
            }

            $rows[] = new ImportRow($lineNumber, $rawData, $mappedData);
            ++$lineNumber;
        }

        return $rows;
    }

    /**
     * @return list<string>
     */
    private function getExistingSubjectNames(TenantId $tenantId): array
    {
        $subjects = $this->subjectRepository->findAllActiveByTenant($tenantId);

        return array_values(array_map(
            static fn ($subject) => (string) $subject->name,
            $subjects,
        ));
    }

    /**
     * @return list<string>
     */
    private function getExistingClassNames(TenantId $tenantId): array
    {
        $classes = $this->classRepository->findAllActiveByTenant($tenantId);

        return array_values(array_map(
            static fn ($class) => (string) $class->name,
            $classes,
        ));
    }

    /**
     * @param list<ImportRow> $rows
     * @param list<string>    $existingValues
     *
     * @return list<string>
     */
    private function detectUnknownValues(array $rows, TeacherImportField $field, array $existingValues): array
    {
        $parser = new MultiValueParser();
        $unknown = [];

        foreach ($rows as $row) {
            $raw = $row->mappedData[$field->value] ?? null;
            if ($raw === null || trim($raw) === '') {
                continue;
            }

            foreach ($parser->parse($raw) as $value) {
                if (!in_array($value, $existingValues, true)
                    && !in_array($value, $unknown, true)
                ) {
                    $unknown[] = $value;
                }
            }
        }

        return $unknown;
    }
}
