<?php

declare(strict_types=1);

namespace App\Administration\Infrastructure\Persistence\Doctrine;

use App\Administration\Domain\Exception\ImportBatchNotFoundException;
use App\Administration\Domain\Model\Import\ImportBatchId;
use App\Administration\Domain\Model\Import\ImportRow;
use App\Administration\Domain\Model\Import\ImportRowError;
use App\Administration\Domain\Model\Import\ImportStatus;
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\TeacherImportBatchRepository;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;

use function json_decode;
use function json_encode;

use const JSON_THROW_ON_ERROR;

use Override;

final readonly class DoctrineTeacherImportBatchRepository implements TeacherImportBatchRepository
{
    public function __construct(
        private Connection $connection,
    ) {
    }

    #[Override]
    public function save(TeacherImportBatch $batch): void
    {
        $this->connection->executeStatement(
            'INSERT INTO teacher_import_batches
                (id, tenant_id, original_filename, total_rows, detected_columns, detected_format,
                 status, mapping_data, imported_count, error_count, rows_data, created_at, completed_at)
             VALUES
                (:id, :tenant_id, :original_filename, :total_rows, :detected_columns, :detected_format,
                 :status, :mapping_data, :imported_count, :error_count, :rows_data, :created_at, :completed_at)
             ON CONFLICT (id) DO UPDATE SET
                 total_rows = EXCLUDED.total_rows,
                 status = EXCLUDED.status,
                 mapping_data = EXCLUDED.mapping_data,
                 imported_count = EXCLUDED.imported_count,
                 error_count = EXCLUDED.error_count,
                 rows_data = EXCLUDED.rows_data,
                 completed_at = EXCLUDED.completed_at',
            [
                'id' => (string) $batch->id,
                'tenant_id' => (string) $batch->tenantId,
                'original_filename' => $batch->originalFilename,
                'total_rows' => $batch->totalRows,
                'detected_columns' => json_encode($batch->detectedColumns, JSON_THROW_ON_ERROR),
                'detected_format' => $batch->detectedFormat?->value,
                'status' => $batch->status->value,
                'mapping_data' => $batch->mapping !== null
                    ? json_encode($this->serializeMapping($batch->mapping), JSON_THROW_ON_ERROR)
                    : null,
                'imported_count' => $batch->importedCount,
                'error_count' => $batch->errorCount,
                'rows_data' => json_encode($this->serializeRows($batch->lignes()), JSON_THROW_ON_ERROR),
                'created_at' => $batch->createdAt->format(DateTimeImmutable::ATOM),
                'completed_at' => $batch->completedAt?->format(DateTimeImmutable::ATOM),
            ],
        );
    }

    #[Override]
    public function get(ImportBatchId $id): TeacherImportBatch
    {
        return $this->findById($id) ?? throw ImportBatchNotFoundException::withId($id);
    }

    #[Override]
    public function findById(ImportBatchId $id): ?TeacherImportBatch
    {
        $row = $this->connection->fetchAssociative(
            'SELECT * FROM teacher_import_batches WHERE id = :id',
            ['id' => (string) $id],
        );

        if ($row === false) {
            return null;
        }

        return $this->hydrate($row);
    }

    #[Override]
    public function findByTenant(TenantId $tenantId): array
    {
        $rows = $this->connection->fetchAllAssociative(
            'SELECT * FROM teacher_import_batches WHERE tenant_id = :tenant_id ORDER BY created_at DESC',
            ['tenant_id' => (string) $tenantId],
        );

        return array_map(fn ($row) => $this->hydrate($row), $rows);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): TeacherImportBatch
    {
        /** @var string $id */
        $id = $row['id'];
        /** @var string $tenantId */
        $tenantId = $row['tenant_id'];
        /** @var string $originalFilename */
        $originalFilename = $row['original_filename'];
        /** @var string|int $totalRowsRaw */
        $totalRowsRaw = $row['total_rows'];
        $totalRows = (int) $totalRowsRaw;
        /** @var string $detectedColumnsJson */
        $detectedColumnsJson = $row['detected_columns'];
        /** @var string|null $detectedFormat */
        $detectedFormat = $row['detected_format'];
        /** @var string $status */
        $status = $row['status'];
        /** @var string|null $mappingJson */
        $mappingJson = $row['mapping_data'];
        /** @var string|int $importedCountRaw */
        $importedCountRaw = $row['imported_count'];
        $importedCount = (int) $importedCountRaw;
        /** @var string|int $errorCountRaw */
        $errorCountRaw = $row['error_count'];
        $errorCount = (int) $errorCountRaw;
        /** @var string $rowsJson */
        $rowsJson = $row['rows_data'];
        /** @var string $createdAt */
        $createdAt = $row['created_at'];
        /** @var string|null $completedAt */
        $completedAt = $row['completed_at'];

        /** @var list<string> $detectedColumns */
        $detectedColumns = json_decode($detectedColumnsJson, true, 512, JSON_THROW_ON_ERROR);

        $mapping = $mappingJson !== null ? $this->hydrateMapping($mappingJson) : null;

        $batch = TeacherImportBatch::reconstitute(
            id: ImportBatchId::fromString($id),
            tenantId: TenantId::fromString($tenantId),
            originalFilename: $originalFilename,
            totalRows: $totalRows,
            detectedColumns: $detectedColumns,
            detectedFormat: $detectedFormat !== null ? KnownImportFormat::from($detectedFormat) : null,
            status: ImportStatus::from($status),
            mapping: $mapping,
            importedCount: $importedCount,
            errorCount: $errorCount,
            createdAt: new DateTimeImmutable($createdAt),
            completedAt: $completedAt !== null ? new DateTimeImmutable($completedAt) : null,
        );

        $rows = $this->hydrateRows($rowsJson);
        $batch->enregistrerLignes($rows);

        return $batch;
    }

    private function hydrateMapping(string $json): TeacherColumnMapping
    {
        /** @var array{mapping: array<string, string>, format: string} $data */
        $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

        /** @var array<string, TeacherImportField> $mapping */
        $mapping = [];
        foreach ($data['mapping'] as $column => $fieldValue) {
            $mapping[$column] = TeacherImportField::from($fieldValue);
        }

        return TeacherColumnMapping::creer($mapping, KnownImportFormat::from($data['format']));
    }

    /**
     * @return list<ImportRow>
     */
    private function hydrateRows(string $json): array
    {
        /** @var list<array{lineNumber: int, rawData: array<string, string>, mappedData: array<string, string>, errors: list<array{column: string, message: string}>}> $data */
        $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

        return array_map(
            static fn (array $rowData) => new ImportRow(
                lineNumber: $rowData['lineNumber'],
                rawData: $rowData['rawData'],
                mappedData: $rowData['mappedData'],
                errors: array_map(
                    static fn (array $err) => new ImportRowError($err['column'], $err['message']),
                    $rowData['errors'],
                ),
            ),
            $data,
        );
    }

    /**
     * @return array{mapping: array<string, string>, format: string}
     */
    private function serializeMapping(TeacherColumnMapping $mapping): array
    {
        $serialized = [];
        foreach ($mapping->mapping as $column => $field) {
            $serialized[$column] = $field->value;
        }

        return [
            'mapping' => $serialized,
            'format' => $mapping->format->value,
        ];
    }

    /**
     * @param list<ImportRow> $rows
     *
     * @return list<array{lineNumber: int, rawData: array<string, string>, mappedData: array<string, string>, errors: list<array{column: string, message: string}>}>
     */
    private function serializeRows(array $rows): array
    {
        return array_map(
            static fn (ImportRow $row) => [
                'lineNumber' => $row->lineNumber,
                'rawData' => $row->rawData,
                'mappedData' => $row->mappedData,
                'errors' => array_map(
                    static fn (ImportRowError $error) => [
                        'column' => $error->column,
                        'message' => $error->message,
                    ],
                    $row->errors,
                ),
            ],
            $rows,
        );
    }
}
