using MaBibli.Api.Data;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using MaBibli.Shared.Textes;
using Microsoft.EntityFrameworkCore;

namespace MaBibli.Api.Services.Revues;

/// <summary>Issue d'une écriture sur une revue : la revue, ou un message pour l'utilisateur.</summary>
public readonly record struct ResultatRevue(RevueDto? Revue, string? Erreur)
{
    public static ResultatRevue Ok(RevueDto revue) => new(revue, null);

    public static ResultatRevue Invalide(string message) => new(null, message);

    public static readonly ResultatRevue Introuvable = new(null, null);

    public bool EstIntrouvable => Revue is null && Erreur is null;
}

public interface IServiceRevues
{
    Task<IReadOnlyList<RevueDto>> ListerAsync(CancellationToken ct = default);

    /// <summary>
    /// Crée la revue, ou <b>rend celle qui existe déjà</b> quand l'ISSN ou le titre la désigne.
    /// </summary>
    Task<ResultatRevue> CreerOuRetrouverAsync(
        EnregistrementRevue saisie, string? utilisateur, CancellationToken ct = default);

    Task<ResultatRevue> ModifierAsync(
        int id, EnregistrementRevue saisie, CancellationToken ct = default);

    Task<bool> SupprimerAsync(int id, CancellationToken ct = default);

    Task<ResultatRevue> AjouterNumeroAsync(
        int revueId, AjoutNumeroRevue saisie, CancellationToken ct = default);

    Task<bool> RetirerNumeroAsync(int numeroId, CancellationToken ct = default);
}

/// <summary>
/// Revues et magazines : une fiche par revue, les numéros à l'intérieur.
/// </summary>
/// <remarks>
/// ⚠️ <b>Communes au foyer</b>, comme le catalogue : aucune méthode ne filtre sur un
/// utilisateur, <see cref="Revue.AjoutePar"/> n'étant qu'une trace.
/// <para>
/// Un numéro est <b>recensé, rien de plus</b> — ni prêt, ni statut de lecture (décidé le
/// 2026-08-19). C'est ce qui garde cette table à l'écart de deux mécanismes attachés à
/// <c>Livre</c> par clé étrangère.
/// </para>
/// </remarks>
public sealed class ServiceRevues(MaBibliDbContext db) : IServiceRevues
{
    public async Task<IReadOnlyList<RevueDto>> ListerAsync(CancellationToken ct = default)
    {
        var revues = await db.Revues
            .AsNoTracking()
            .Include(r => r.Numeros)
            .OrderBy(r => r.TitreNormalise)
            .ToListAsync(ct);

        return revues.Select(Projeter).ToList();
    }

    public async Task<ResultatRevue> CreerOuRetrouverAsync(
        EnregistrementRevue saisie, string? utilisateur, CancellationToken ct = default)
    {
        var titre = saisie.Titre?.Trim();
        if (string.IsNullOrWhiteSpace(titre))
        {
            return ResultatRevue.Invalide("Le titre de la revue est obligatoire.");
        }

        var issn = Vide(saisie.Issn);
        var normalise = NormalisationTexte.Normaliser(titre);

        // ⚠️ « Créer OU retrouver » et non « créer » : le flux du code-barres 977 rescanne
        // forcément la même revue à chaque numéro. Refuser le doublon obligerait l'utilisateur
        // à comprendre qu'il doit aller chercher la fiche lui-même — alors que le geste est
        // toujours le même : « voici un numéro de plus ».
        var existante = await db.Revues
            .Include(r => r.Numeros)
            .FirstOrDefaultAsync(
                r => (issn != null && r.Issn == issn) || r.TitreNormalise == normalise, ct);

        if (existante is not null)
        {
            // Un ISSN appris après coup complète la fiche créée à la main : c'est ce qui
            // permettra de la retrouver au prochain scan.
            if (existante.Issn is null && issn is not null)
            {
                existante.Issn = issn;
                await db.SaveChangesAsync(ct);
            }

            return ResultatRevue.Ok(Projeter(existante));
        }

        var revue = new Revue
        {
            Titre = titre,
            Issn = issn,
            Editeur = Vide(saisie.Editeur),
            DateAjout = DateTime.UtcNow,
            AjoutePar = utilisateur,
        };

        revue.RecalculerFormes();
        db.Revues.Add(revue);
        await db.SaveChangesAsync(ct);

        return ResultatRevue.Ok(Projeter(revue));
    }

    public async Task<ResultatRevue> ModifierAsync(
        int id, EnregistrementRevue saisie, CancellationToken ct = default)
    {
        var revue = await db.Revues.Include(r => r.Numeros).FirstOrDefaultAsync(r => r.Id == id, ct);
        if (revue is null)
        {
            return ResultatRevue.Introuvable;
        }

        var titre = saisie.Titre?.Trim();
        if (string.IsNullOrWhiteSpace(titre))
        {
            return ResultatRevue.Invalide("Le titre de la revue est obligatoire.");
        }

        var normalise = NormalisationTexte.Normaliser(titre);
        if (await db.Revues.AnyAsync(r => r.Id != id && r.TitreNormalise == normalise, ct))
        {
            return ResultatRevue.Invalide($"La revue « {titre} » existe déjà.");
        }

        var issn = Vide(saisie.Issn);
        if (issn is not null && await db.Revues.AnyAsync(r => r.Id != id && r.Issn == issn, ct))
        {
            return ResultatRevue.Invalide($"L'ISSN {issn} est déjà celui d'une autre revue.");
        }

        revue.Titre = titre;
        revue.Issn = issn;
        revue.Editeur = Vide(saisie.Editeur);
        revue.RecalculerFormes();

        await db.SaveChangesAsync(ct);
        return ResultatRevue.Ok(Projeter(revue));
    }

    public async Task<bool> SupprimerAsync(int id, CancellationToken ct = default)
    {
        var revue = await db.Revues.FirstOrDefaultAsync(r => r.Id == id, ct);
        if (revue is null)
        {
            return false;
        }

        // Les numéros partent en cascade : ils n'existent que par leur revue, contrairement aux
        // livres d'une série, qui vivent leur vie au catalogue.
        db.Revues.Remove(revue);
        await db.SaveChangesAsync(ct);
        return true;
    }

    public async Task<ResultatRevue> AjouterNumeroAsync(
        int revueId, AjoutNumeroRevue saisie, CancellationToken ct = default)
    {
        var revue = await db.Revues.Include(r => r.Numeros).FirstOrDefaultAsync(r => r.Id == revueId, ct);
        if (revue is null)
        {
            return ResultatRevue.Introuvable;
        }

        var numero = saisie.Numero?.Trim();
        if (string.IsNullOrWhiteSpace(numero))
        {
            return ResultatRevue.Invalide("Le numéro est obligatoire.");
        }

        var normalise = NormalisationTexte.Normaliser(numero);
        if (revue.Numeros.Any(n => n.NumeroNormalise == normalise))
        {
            return ResultatRevue.Invalide($"Le numéro {numero} est déjà dans votre collection.");
        }

        if (saisie.DateParution is { } parution && parution.Date > DateTime.UtcNow.Date.AddDays(1))
        {
            // Comme pour les prêts : une date future est une faute de frappe, pas une intention.
            // Un jour de marge, les revues datées du mois suivant étant courantes en kiosque.
            return ResultatRevue.Invalide("La date de parution ne peut pas être dans le futur.");
        }

        var ligne = new NumeroRevue
        {
            RevueId = revueId,
            Numero = numero,
            DateParution = saisie.DateParution,
            Note = Vide(saisie.Note),
            DateAjout = DateTime.UtcNow,
        };

        ligne.RecalculerFormes();
        db.NumerosRevue.Add(ligne);
        await db.SaveChangesAsync(ct);

        var relue = await db.Revues.AsNoTracking().Include(r => r.Numeros)
            .FirstAsync(r => r.Id == revueId, ct);

        return ResultatRevue.Ok(Projeter(relue));
    }

    public async Task<bool> RetirerNumeroAsync(int numeroId, CancellationToken ct = default)
    {
        var numero = await db.NumerosRevue.FirstOrDefaultAsync(n => n.Id == numeroId, ct);
        if (numero is null)
        {
            return false;
        }

        db.NumerosRevue.Remove(numero);
        await db.SaveChangesAsync(ct);
        return true;
    }

    private static string? Vide(string? valeur) =>
        string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim();

    private static RevueDto Projeter(Revue revue) => new()
    {
        Id = revue.Id,
        Titre = revue.Titre,
        Issn = revue.Issn,
        Editeur = revue.Editeur,
        AjoutePar = revue.AjoutePar,

        // Du plus récent au plus ancien : sur un magazine, c'est le dernier reçu qu'on cherche,
        // à l'inverse d'une série dont on suit l'ordre de lecture. Les numéros sans date de
        // parution ferment la liste plutôt que de passer pour les plus vieux.
        Numeros = revue.Numeros
            .OrderByDescending(n => n.DateParution.HasValue)
            .ThenByDescending(n => n.DateParution)
            .ThenByDescending(n => n.NumeroNormalise, StringComparer.Ordinal)
            .Select(n => new NumeroRevueDto
            {
                Id = n.Id,
                Numero = n.Numero,
                DateParution = n.DateParution,
                Note = n.Note,
            })
            .ToList(),
    };
}
