<?php

declare(strict_types=1);

namespace App\Module\Transport\Application\Qualimat;

/**
 * Mapping pur d'un item brut de l'API QUALIMAT vers une ligne normalisee
 * prete a l'upsert dans `qualimat_carrier`. Sans dependance (testable en
 * isolation). Voir ERP-39 § 2 pour les pieges qualite de la source.
 */
final class QualimatRowMapper
{
    /**
     * Mappe un lot d'items. Les items sans SIRET exploitable sont ignores et
     * comptes a part (cf. `rows_skipped` du journal). Les doublons de SIRET
     * (source "sale" : memes chiffres a separateurs pres) sont fusionnes,
     * derniere occurrence gagnante — l'upsert ne verrait qu'une ligne de toute
     * facon, et le compte `rows_upserted` reflete ainsi les transporteurs
     * distincts.
     *
     * @param array<int, array<string, mixed>> $items
     *
     * @return array{rows: list<array<string, mixed>>, skipped: int}
     */
    public static function mapMany(array $items): array
    {
        $bySiret = [];
        $skipped = 0;

        foreach ($items as $item) {
            $row = self::mapOne($item);

            if (null === $row) {
                ++$skipped;

                continue;
            }

            // Cle = SIRET normalise : une occurrence ulterieure ecrase la
            // precedente (derniere gagnante).
            $bySiret[$row['siret']] = $row;
        }

        return ['rows' => array_values($bySiret), 'skipped' => $skipped];
    }

    /**
     * Mappe un item unique. Retourne null si le SIRET est absent ou vide
     * (ligne inexploitable : pas de cle naturelle pour l'upsert).
     *
     * @param array<string, mixed> $item
     *
     * @return null|array<string, mixed>
     */
    public static function mapOne(array $item): ?array
    {
        $siret = self::normalizeSiret(self::str($item['Siret'] ?? null));

        if (null === $siret) {
            return null;
        }

        return [
            'siret' => $siret,
            // Nom et Societe sont identiques a la source : une seule colonne.
            'name'        => self::str($item['Nom'] ?? null) ?? '',
            'address'     => self::str($item['Adresse'] ?? null),
            'postal_code' => self::str($item['CodePostal'] ?? null),
            'city'        => self::str($item['Ville'] ?? null),
            'phone'       => self::str($item['Telephone_1'] ?? null),
            'department'  => self::str($item['Departement'] ?? null),
            // Statut conserve brut (feed externe, valeurs non contraintes).
            'status'        => self::str($item['Statut'] ?? null) ?? '',
            'validity_date' => self::parseDate(self::str($item['Validite'] ?? null)),
        ];
    }

    /**
     * Normalise un SIRET : ne conserve que les chiffres. Null si vide.
     * La source est "sale" (longueurs variables 7 a 14) : aucune contrainte
     * de longueur, on stocke les chiffres tels quels.
     */
    public static function normalizeSiret(?string $raw): ?string
    {
        if (null === $raw) {
            return null;
        }

        $digits = preg_replace('/\D+/', '', $raw) ?? '';

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

    /**
     * Convertit une date "dd/mm/yyyy" en "yyyy-mm-dd". Null si le format ne
     * correspond pas ou si la date n'est pas un jour calendaire valide
     * (garde-fou : evite un INSERT en erreur sur une date impossible).
     */
    public static function parseDate(?string $raw): ?string
    {
        if (null === $raw || !preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $raw, $m)) {
            return null;
        }

        $day   = (int) $m[1];
        $month = (int) $m[2];
        $year  = (int) $m[3];

        if (!checkdate($month, $day, $year)) {
            return null;
        }

        return sprintf('%04d-%02d-%02d', $year, $month, $day);
    }

    /**
     * Trim d'une valeur scalaire ; null si la chaine resultante est vide.
     */
    private static function str(mixed $value): ?string
    {
        if (null === $value) {
            return null;
        }

        $trimmed = trim((string) $value);

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