namespace MaBibli.Shared.Isbn;

/// <summary>
/// Découpage d'un ISBN en tranches séparées par des tirets, <b>pour l'affichage seulement</b>.
/// </summary>
/// <remarks>
/// <c>9782846391009</c> → <c>978-2-84639-100-9</c>. La valeur <b>stockée reste nue</b> : ces
/// tirets ne franchissent jamais la frontière de la base ni celle des requêtes aux sources
/// (la BnF indexe l'ISBN sans tiret). Le seul point d'entrée est <see cref="Afficher"/>.
/// <para>
/// Le découpage dépend du <b>groupe linguistique</b> puis de l'<b>éditeur</b>, et les tranches
/// sont publiées par l'agence internationale — elles ne se devinent pas. Les tables ci-dessous
/// sont extraites du <c>RangeMessage.xml</c> officiel (<c>isbn-international.org</c>), version
/// du 2026-08-18.
/// </para>
/// <para>
/// <b>Principe tenu :</b> on n'affiche <b>jamais</b> une coupure qu'on ne sait pas juste. Le
/// français (<c>978-2</c>) est découpé en entier, jusqu'à l'éditeur. Ailleurs, on s'arrête au
/// groupe et à la clé — deux frontières toujours exactes — et l'on ne devine pas la limite
/// éditeur/publication. Une tranche en moins se lit encore ; une tranche fausse trompe.
/// </para>
/// </remarks>
public static class FormatageIsbn
{
    /// <summary>Longueur du groupe linguistique, d'après les 7 chiffres qui suivent <c>978</c>.</summary>
    private static readonly (int Debut, int Fin, int Longueur)[] GroupesSous978 =
    [
        (0000000, 5999999, 1),
        (6000000, 6499999, 3),
        (6500000, 6599999, 2),
        (6600000, 6699999, 2),
        (6999000, 6999999, 5),
        (7000000, 7999999, 1),
        (8000000, 9499999, 2),
        (9500000, 9899999, 3),
        (9900000, 9989999, 4),
        (9990000, 9999999, 5),
    ];

    /// <summary>Idem sous <c>979</c> : seuls deux groupes existent à ce jour.</summary>
    private static readonly (int Debut, int Fin, int Longueur)[] GroupesSous979 =
    [
        (1000000, 1599999, 2),
        (8000000, 8999999, 1),
    ];

    /// <summary>
    /// Longueur du numéro d'éditeur dans le groupe <b>francophone</b>, d'après les 7 chiffres
    /// qui suivent <c>978-2</c>.
    /// </summary>
    /// <remarks>
    /// C'est la seule table d'éditeurs embarquée, et c'est un choix : la collection est
    /// majoritairement francophone, et les 19 tranches ci-dessous couvrent donc l'essentiel du
    /// fonds. Embarquer les ~220 groupes du monde entier alourdirait le WASM téléchargé par le
    /// navigateur pour un gain marginal.
    /// </remarks>
    private static readonly (int Debut, int Fin, int Longueur)[] EditeursFrancophones =
    [
        (0000000, 1999999, 2),
        (2000000, 3499999, 3),
        (3500000, 3999999, 5),
        (4000000, 4869999, 3),
        (4870000, 4949999, 6),
        (4950000, 4959999, 3),
        (4960000, 4966999, 4),
        (4967000, 4969999, 5),
        (4970000, 5279999, 3),
        (5280000, 5299999, 4),
        (5300000, 6999999, 3),
        (7000000, 8399999, 4),
        (8400000, 8999999, 5),
        (9000000, 9197999, 6),
        (9198000, 9198099, 5),
        (9198100, 9199429, 6),
        (9199430, 9199689, 7),
        (9199690, 9499999, 6),
        (9500000, 9999999, 7),
    ];

    private const string GroupeFrancophone = "2";

    /// <summary>
    /// Rend l'ISBN lisible, tirets compris.
    /// </summary>
    /// <returns>
    /// La forme découpée, ou l'entrée telle quelle si elle n'est pas un ISBN valide — on ne
    /// découpe jamais ce qu'on n'a pas reconnu, un ISBN mal saisi doit se voir tel qu'il est.
    /// </returns>
    public static string Afficher(string? brut)
    {
        if (string.IsNullOrWhiteSpace(brut))
        {
            return string.Empty;
        }

        var isbn = IsbnUtils.Normaliser(brut);
        if (!IsbnUtils.EstValide(isbn))
        {
            return brut.Trim();
        }

        var decoupe = isbn!.Length == 13 ? Decouper13(isbn) : Decouper10(isbn);
        return decoupe ?? isbn;
    }

    private static string? Decouper13(string isbn)
    {
        var prefixe = isbn[..3];
        var table = prefixe switch
        {
            "978" => GroupesSous978,
            "979" => GroupesSous979,

            // 977 (périodique) et tout autre préfixe EAN ne portent pas d'ISBN : rien à découper.
            _ => null,
        };

        if (table is null || !TrouverLongueur(table, isbn.AsSpan(3), out var longueurGroupe))
        {
            return null;
        }

        var groupe = isbn.Substring(3, longueurGroupe);
        var corps = isbn[(3 + longueurGroupe)..];

        return prefixe + "-" + Assembler(groupe, corps, francophone: prefixe == "978" && groupe == GroupeFrancophone);
    }

    private static string? Decouper10(string isbn)
    {
        // Un ISBN-10 est un ISBN-13 en 978 amputé de son préfixe : les tranches de groupe
        // sont donc exactement les mêmes.
        if (!TrouverLongueur(GroupesSous978, isbn.AsSpan(), out var longueurGroupe))
        {
            return null;
        }

        var groupe = isbn[..longueurGroupe];
        var corps = isbn[longueurGroupe..];

        return Assembler(groupe, corps, francophone: groupe == GroupeFrancophone);
    }

    /// <summary>
    /// Recolle <c>groupe - [éditeur - publication] - clé</c> à partir du corps de l'ISBN
    /// (tout ce qui suit le groupe, clé comprise).
    /// </summary>
    private static string Assembler(string groupe, string corps, bool francophone)
    {
        var cle = corps[^1..];
        var milieu = corps[..^1];

        if (francophone
            && TrouverLongueur(EditeursFrancophones, corps.AsSpan(), out var longueurEditeur)
            // Il doit rester au moins un chiffre de publication : sinon la tranche est plus
            // longue que le corps, et on afficherait une coupure absurde plutôt que rien.
            && longueurEditeur < milieu.Length)
        {
            var editeur = milieu[..longueurEditeur];
            var publication = milieu[longueurEditeur..];
            return $"{groupe}-{editeur}-{publication}-{cle}";
        }

        return $"{groupe}-{milieu}-{cle}";
    }

    /// <summary>
    /// Cherche la tranche contenant les 7 premiers chiffres de <paramref name="chiffres"/>.
    /// </summary>
    /// <remarks>
    /// Les tables de l'agence sont toutes indexées sur <b>7 chiffres</b>, complétés par des
    /// zéros quand il en manque — c'est ce que fait le remplissage ci-dessous.
    /// </remarks>
    private static bool TrouverLongueur(
        (int Debut, int Fin, int Longueur)[] table, ReadOnlySpan<char> chiffres, out int longueur)
    {
        longueur = 0;

        var cle = 0;
        for (var i = 0; i < 7; i++)
        {
            // Le X final d'un ISBN-10 n'est jamais dans les 7 premiers chiffres d'un corps
            // exploitable, mais le vérifier coûte moins cher que d'y croire.
            var c = i < chiffres.Length ? chiffres[i] : '0';
            if (!char.IsDigit(c))
            {
                return false;
            }

            cle = (cle * 10) + (c - '0');
        }

        foreach (var (debut, fin, valeur) in table)
        {
            if (cle >= debut && cle <= fin)
            {
                longueur = valeur;
                return true;
            }
        }

        // Tranche non attribuée : on ne devine pas.
        return false;
    }
}
