using System.Globalization;
using System.Text;
using MaBibli.Shared.Dtos;

namespace MaBibli.Api.Services.Souhaits;

/// <summary>
/// Sérialisation de la liste d'envies en fichier emportable.
/// </summary>
/// <remarks>
/// <b>Fonctions pures</b> : elles prennent une liste et rendent une chaîne, sans base ni HTTP.
/// C'est ce qui permet de vérifier le contenu exact du fichier en test plutôt que de se fier à
/// une inspection visuelle.
/// <para>
/// <b>Aucune dépendance, ni côté production ni côté lecture.</b> IDEES.md demandait un format
/// « lisible partout » : ni PDF, ni tableur binaire, ni JSON. Les deux sorties sont du texte.
/// </para>
/// </remarks>
public static class ExportSouhaits
{
    /// <summary>
    /// Séparateur du CSV : le <b>point-virgule</b>, pas la virgule.
    /// </summary>
    /// <remarks>
    /// Le CSV n'a qu'un seul travail — s'ouvrir proprement dans un tableur. Or Excel en locale
    /// française attend le point-virgule comme séparateur de liste : un fichier à virgules y
    /// atterrit intégralement dans la colonne A, ce qui rend l'export inutilisable pour l'usage
    /// visé (se répartir des achats avant un anniversaire). La collection étant francophone,
    /// c'est ce cas-là qu'il faut servir. Les titres contenant un point-virgule restent corrects :
    /// ils sont entre guillemets, conformément à RFC 4180.
    /// </remarks>
    public const char SeparateurCsv = ';';

    /// <summary>
    /// Marque d'ordre des octets, <b>indispensable</b> en tête du CSV.
    /// </summary>
    /// <remarks>
    /// Sans elle, Excel lit un CSV en codage hérité et affiche « Ã‰mile Zola ». Sur une liste de
    /// livres français, à peu près chaque ligne serait touchée. Le BOM ne gêne aucun autre
    /// lecteur (LibreOffice, tableurs en ligne, éditeurs de texte).
    /// </remarks>
    public static readonly byte[] Bom = Encoding.UTF8.GetPreamble();

    public static string NomFichier(FormatExportSouhaits format, DateTime maintenant) =>
        $"liste-envies-{maintenant:yyyy-MM-dd}.{(format == FormatExportSouhaits.Csv ? "csv" : "txt")}";

    public static string TypeMime(FormatExportSouhaits format) =>
        format == FormatExportSouhaits.Csv ? "text/csv; charset=utf-8" : "text/plain; charset=utf-8";

    public static string Produire(
        FormatExportSouhaits format, IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant) =>
        format switch
        {
            FormatExportSouhaits.Csv => Csv(souhaits),
            _ => Texte(souhaits, proprietaire, maintenant),
        };

    /// <summary>
    /// Liste lisible telle quelle, groupée par auteur — la version qu'on emporte en librairie.
    /// </summary>
    /// <remarks>
    /// Le groupement par auteur n'est pas cosmétique : c'est ainsi qu'une librairie est rangée.
    /// Une liste à plat obligerait à parcourir tout le fichier pour chaque rayon.
    /// </remarks>
    public static string Texte(IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant)
    {
        var sortie = new StringBuilder();

        sortie.Append("Liste d'envies");
        if (!string.IsNullOrWhiteSpace(proprietaire))
        {
            sortie.Append(" de ").Append(proprietaire);
        }

        sortie.Append(" — ").Append(maintenant.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)).Append('\n');
        sortie.Append(new string('=', 40)).Append('\n');

        if (souhaits.Count == 0)
        {
            sortie.Append("\n(aucun livre dans la liste)\n");
            return sortie.ToString();
        }

        sortie.Append('\n').Append(souhaits.Count).Append(souhaits.Count > 1 ? " livres" : " livre").Append('\n');

        // « Auteur inconnu » est rejeté en fin de liste : ce sont les entrées les moins
        // exploitables en rayon, elles ne doivent pas ouvrir le fichier.
        var groupes = souhaits
            .GroupBy(s => string.IsNullOrWhiteSpace(s.Auteur) ? null : s.Auteur!.Trim())
            .OrderBy(g => g.Key is null)
            .ThenBy(g => g.Key, StringComparer.CurrentCultureIgnoreCase);

        foreach (var groupe in groupes)
        {
            sortie.Append('\n').Append(groupe.Key ?? "Auteur non précisé").Append('\n');

            foreach (var souhait in groupe.OrderBy(s => s.Titre, StringComparer.CurrentCultureIgnoreCase))
            {
                sortie.Append("  - ").Append(souhait.Titre);

                // Éditeur, année et ISBN sur la même ligne, entre parenthèses : c'est ce qu'on
                // épelle à un libraire, et ça reste lisible sur un écran de téléphone.
                var precisions = new[] { souhait.Editeur, souhait.Annee, Isbn(souhait.Isbn) }
                    .Where(p => !string.IsNullOrWhiteSpace(p))
                    .ToList();

                if (precisions.Count > 0)
                {
                    sortie.Append(" (").Append(string.Join(", ", precisions)).Append(')');
                }

                sortie.Append('\n');

                if (!string.IsNullOrWhiteSpace(souhait.Note))
                {
                    sortie.Append("    ").Append(souhait.Note.Trim()).Append('\n');
                }
            }
        }

        return sortie.ToString();
    }

    /// <summary>Tableau ouvrable dans un tableur, une envie par ligne.</summary>
    public static string Csv(IReadOnlyList<SouhaitDto> souhaits)
    {
        var sortie = new StringBuilder();

        Ligne(sortie, "Titre", "Auteur", "Éditeur", "Année", "ISBN", "Note", "Ajouté le");

        foreach (var souhait in souhaits)
        {
            Ligne(
                sortie,
                souhait.Titre,
                souhait.Auteur,
                souhait.Editeur,
                souhait.Annee,
                souhait.Isbn,
                souhait.Note,
                souhait.DateAjout.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
        }

        return sortie.ToString();
    }

    private static string? Isbn(string? isbn) =>
        string.IsNullOrWhiteSpace(isbn) ? null : $"ISBN {isbn}";

    private static void Ligne(StringBuilder sortie, params string?[] champs)
    {
        sortie.AppendJoin(SeparateurCsv, champs.Select(Echapper));

        // CRLF : RFC 4180 le prescrit, et c'est ce qu'attendent les tableurs sous Windows.
        sortie.Append("\r\n");
    }

    /// <summary>
    /// Échappement RFC 4180 : guillemets doublés, champ encadré dès qu'il contient un
    /// séparateur, un guillemet ou un saut de ligne.
    /// </summary>
    /// <remarks>
    /// Une note libre peut contenir n'importe quoi : sans échappement, un simple « offert ; à
    /// rendre » décalerait toutes les colonnes suivantes.
    /// </remarks>
    private static string Echapper(string? champ)
    {
        var valeur = champ ?? string.Empty;

        var doitEncadrer = valeur.Contains(SeparateurCsv)
                           || valeur.Contains('"')
                           || valeur.Contains('\n')
                           || valeur.Contains('\r');

        return doitEncadrer ? $"\"{valeur.Replace("\"", "\"\"")}\"" : valeur;
    }
}
