import type { ResultatDepot } from './types';

/**
 * L'ENVOI EN LOT, ligne après ligne — logique pure, sans React ni réseau.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * POURQUOI LE NAVIGATEUR ENCHAÎNE, ET POURQUOI JAMAIS EN PARALLÈLE
 * ═════════════════════════════════════════════════════════════════════════════
 * Chaque dépôt est UNE déclaration réelle à la DGI, et le portail en accepte
 * une par appel. Le serveur expose donc un point d'entrée par ligne
 * (`lots/{lot}/lignes/{ligne}/deposer`, limité à 30/min) et c'est l'écran qui
 * enchaîne. Deux appels en vol en même temps, c'est deux dépôts dont on ne
 * saurait plus dire lequel a échoué — et un second qui part alors que le
 * premier vient d'être refusé pour une raison qui vaut pour tous.
 *
 * D'où les règles, tenues ici et testées dans `lot-runner.test.ts` :
 *   - l'ordre est celui de la file, une ligne à la fois (`await` chacune) ;
 *   - la PREMIÈRE erreur arrête tout — on la montre, on ne continue pas ;
 *   - « Reprendre » repart de la ligne en échec (elle est retentée d'abord),
 *     puis continue avec celles qui restent ; une ligne envoyée ne repart jamais.
 *
 * ═════════════════════════════════════════════════════════════════════════════
 * LA LIGNE ÉCARTÉE : CE QUI NE PEUT PLUS PARTIR PAR CE LOT
 * ═════════════════════════════════════════════════════════════════════════════
 * Une ligne que le serveur dit `non_deposable` ou `hors_lot` ne partira JAMAIS
 * par ce lot. Le cas typique : un appel précédent a expiré côté réseau alors que
 * le fichier est bien arrivé à la DGI, et la ligne est désormais « en attente
 * DGI ». La retenter d'abord à chaque « Reprendre » bloquerait le lot pour
 * toujours. Elle devient donc `ecartee` : l'envoi S'ARRÊTE quand même dessus
 * (la première erreur arrête tout, et elle doit se voir), mais « Reprendre »
 * passe à la suivante. « Passer cette ligne » écarte de même, à la main, une
 * ligne en erreur ordinaire. Une ligne écartée ne repart jamais d'ici.
 *
 * `deposer` est injecté : l'écran y branche l'appel réseau, les tests une
 * fonction qui répond ce qu'on veut, quand on veut.
 */

export type EtatLigneLot = 'en_attente' | 'en_cours' | 'envoyee' | 'erreur' | 'ecartee';

export interface ResultatLigneLot {
    etat: EtatLigneLot;
    /** Message du serveur — succès comme erreur. */
    message: string | null;
    /** Référence du dépôt, quand la DGI en rend une. */
    reference: string | null;
    detail: string | null;
    /** Code d'erreur du serveur (`lot_expire`, `non_deposable`…) — `null` si aucun. */
    code: string | null;
}

export type StatutLot = 'pret' | 'en_cours' | 'arrete' | 'termine';

export interface EtatLot {
    /** La file, dans l'ordre d'envoi. */
    ordre: number[];
    lignes: Record<number, ResultatLigneLot>;
    /** La ligne en vol — `null` hors envoi. */
    courante: number | null;
    statut: StatutLot;
    /** La première erreur, qui a arrêté l'envoi — `null` sinon. */
    erreur: { ligneId: number; message: string; code: string | null } | null;
}

export type DeposerLigne = (ligneId: number) => Promise<ResultatDepot>;

export interface LotRunner {
    /** Lance — ou relance — l'envoi des lignes non encore envoyées ni écartées. Sans effet si un envoi est déjà en cours. */
    lancer: () => Promise<EtatLot>;
    /** Alias de `lancer` : retente la ligne en échec d'abord (sauf écartée), puis continue. */
    reprendre: () => Promise<EtatLot>;
    /** Écarte la ligne en erreur d'un envoi arrêté — sans rien relancer. `false` s'il n'y a rien à écarter. */
    ecarterLigneEnErreur: () => boolean;
    /** « Passer cette ligne » : l'écarte, puis reprend avec les suivantes. */
    passer: () => Promise<EtatLot>;
    /** Demande l'arrêt après la ligne en vol (jamais au milieu d'un appel). */
    arreter: () => void;
    etat: () => EtatLot;
    /** Vrai tant qu'un appel est en vol ou qu'une ligne reste à enchaîner. */
    enCours: () => boolean;
}

/** Le code qui dit que le lot a expiré — à recréer avant de reprendre. */
export const CODE_LOT_EXPIRE = 'lot_expire';

/** Les codes d'une ligne qui ne partira jamais par ce lot. */
export const CODES_ECARTES: readonly string[] = ['non_deposable', 'hors_lot'];

const LIGNE_EN_ATTENTE: ResultatLigneLot = { etat: 'en_attente', message: null, reference: null, detail: null, code: null };

/** La file au départ : toutes les lignes en attente, rien en vol. Les doublons sont retirés. */
export function etatInitialLot(ids: number[]): EtatLot {
    const ordre = [...new Set(ids)];
    const lignes: Record<number, ResultatLigneLot> = {};
    for (const id of ordre) lignes[id] = { ...LIGNE_EN_ATTENTE };

    return { ordre, lignes, courante: null, statut: 'pret', erreur: null };
}

/** Une ligne qui ne repartira plus : envoyée, ou écartée. */
function estClose(resultat: ResultatLigneLot | undefined): boolean {
    return resultat?.etat === 'envoyee' || resultat?.etat === 'ecartee';
}

/** Les lignes qui restent à envoyer, dans l'ordre — la ligne en échec comprise, en tête ; jamais une écartée. */
export function lignesRestantes(etat: EtatLot): number[] {
    return etat.ordre.filter((id) => !estClose(etat.lignes[id]));
}

/** `n / N` — lignes ENVOYÉES sur le total de la file (une écartée ne compte pas comme partie). */
export function progressionLot(etat: EtatLot): { envoyees: number; total: number; pourcentage: number } {
    const total = etat.ordre.length;
    const envoyees = etat.ordre.filter((id) => etat.lignes[id]?.etat === 'envoyee').length;

    return { envoyees, total, pourcentage: total === 0 ? 0 : Math.round((envoyees / total) * 100) };
}

/** Le bilan d'un passage : envoyées, écartées, en erreur, encore en attente. */
export function bilanLot(etat: EtatLot): { envoyees: number; ecartees: number; erreurs: number; enAttente: number } {
    const compter = (e: EtatLigneLot) => etat.ordre.filter((id) => etat.lignes[id]?.etat === e).length;

    return { envoyees: compter('envoyee'), ecartees: compter('ecartee'), erreurs: compter('erreur'), enAttente: compter('en_attente') };
}

/**
 * Le lot a-t-il expiré ? Il faut alors en recréer un avant de reprendre.
 *
 * Le signal premier est le code du serveur, `lot_expire`. Deux replis, pour un
 * serveur qui le dirait autrement :
 *   - un code ou un message qui parle d'expiration ;
 *   - l'HORLOGE, contre `expire_a` rendu à la création du lot. Une date
 *     illisible ne vaut pas expiration : on laisse alors le serveur trancher.
 */
export function lotExpire(expireA: string | null, erreur: { message: string; code: string | null } | null, maintenant: number): boolean {
    if (erreur?.code === CODE_LOT_EXPIRE) return true;
    if (erreur && (/expir/i.test(erreur.code ?? '') || /expir/i.test(erreur.message))) return true;
    if (expireA === null) return false;

    const echeance = Date.parse(expireA);

    return Number.isFinite(echeance) && echeance <= maintenant;
}

/** Message d'une exception levée par `deposer` — le réseau, pas la DGI. */
function messageException(erreur: unknown): string {
    if (erreur instanceof Error && erreur.message.trim() !== '') return erreur.message;

    return typeof erreur === 'string' && erreur.trim() !== '' ? erreur : 'Le serveur n’a pas répondu.';
}

/**
 * Crée l'orchestrateur d'un lot.
 *
 * `surChangement` reçoit un NOUVEL objet à chaque transition (jamais muté) :
 * l'écran peut le passer tel quel à `setState`.
 */
export function creerLotRunner(
    ids: number[],
    deposer: DeposerLigne,
    surChangement?: (etat: EtatLot) => void,
    codesEcartes: readonly string[] = CODES_ECARTES,
): LotRunner {
    let etat = etatInitialLot(ids);
    /** Verrou : jamais deux boucles, donc jamais deux appels en vol. */
    let enVol = false;
    let arretDemande = false;

    const publier = (suivant: EtatLot) => {
        etat = suivant;
        surChangement?.(etat);
    };

    const avecLigne = (id: number, resultat: Partial<ResultatLigneLot>, champs: Partial<EtatLot> = {}): EtatLot => ({
        ...etat,
        ...champs,
        lignes: { ...etat.lignes, [id]: { ...LIGNE_EN_ATTENTE, ...etat.lignes[id], ...resultat } },
    });

    const lancer = async (): Promise<EtatLot> => {
        if (enVol || etat.statut === 'termine') return etat;

        enVol = true;
        arretDemande = false;

        try {
            // La ligne en erreur redevient « en attente » : elle repart la
            // première. Une ligne ÉCARTÉE, elle, reste écartée.
            const remises: Record<number, ResultatLigneLot> = { ...etat.lignes };
            for (const id of etat.ordre) {
                if (remises[id].etat === 'erreur') remises[id] = { ...LIGNE_EN_ATTENTE };
            }
            publier({ ...etat, lignes: remises, statut: 'en_cours', erreur: null });

            for (const id of etat.ordre) {
                if (estClose(etat.lignes[id])) continue;

                if (arretDemande) {
                    publier({ ...etat, statut: 'arrete', courante: null });

                    return etat;
                }

                publier(avecLigne(id, { etat: 'en_cours' }, { courante: id }));

                let resultat: ResultatDepot;
                try {
                    resultat = await deposer(id);
                } catch (erreur) {
                    resultat = { ok: false, message: messageException(erreur), code: null };
                }

                if (resultat.ok) {
                    publier(
                        avecLigne(
                            id,
                            { etat: 'envoyee', message: resultat.message, reference: resultat.reference, detail: resultat.detail, code: null },
                            { courante: null },
                        ),
                    );
                    continue;
                }

                // La PREMIÈRE erreur arrête tout — écartée ou non.
                const ecartee = resultat.code !== null && codesEcartes.includes(resultat.code);
                publier(
                    avecLigne(
                        id,
                        { etat: ecartee ? 'ecartee' : 'erreur', message: resultat.message, reference: null, detail: null, code: resultat.code },
                        { courante: null, statut: 'arrete', erreur: { ligneId: id, message: resultat.message, code: resultat.code } },
                    ),
                );

                return etat;
            }

            publier({ ...etat, statut: 'termine', courante: null });

            return etat;
        } finally {
            enVol = false;
        }
    };

    const ecarterLigneEnErreur = (): boolean => {
        const erreur = etat.erreur;
        if (enVol || etat.statut !== 'arrete' || erreur === null || etat.lignes[erreur.ligneId]?.etat !== 'erreur') return false;

        publier(avecLigne(erreur.ligneId, { etat: 'ecartee' }));

        return true;
    };

    return {
        lancer,
        reprendre: lancer,
        ecarterLigneEnErreur,
        passer: async () => {
            ecarterLigneEnErreur();

            return lancer();
        },
        arreter: () => {
            if (enVol) arretDemande = true;
        },
        etat: () => etat,
        enCours: () => enVol,
    };
}
