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

namespace MaBibli.Api.Services.Souhaits;

/// <summary>Issue d'un ajout d'envie : soit la ligne créée, soit un message pour l'utilisateur.</summary>
public readonly record struct ResultatSouhait(SouhaitDto? Souhait, string? Erreur)
{
    public static ResultatSouhait Ok(SouhaitDto souhait) => new(souhait, null);

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

public interface IServiceSouhaits
{
    Task<IReadOnlyList<SouhaitDto>> ListerAsync(string? utilisateur, CancellationToken ct = default);

    Task<ResultatSouhait> AjouterAsync(
        EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default);

    Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default);
}

/// <summary>
/// La liste d'envies, <b>personnelle</b>.
/// </summary>
/// <remarks>
/// ⚠️ <b>Toute méthode filtre sur <paramref name="utilisateur"/>, sans exception.</b> C'est la
/// différence de fond avec <c>ServiceCatalogue</c>, dont les lectures ne filtrent jamais sur
/// <c>AjoutePar</c> : le catalogue est commun, la liste d'envies ne l'est pas. Une envie qui
/// fuiterait vers un autre membre du foyer gâcherait exactement ce que la fonctionnalité sert
/// à préparer.
/// <para>
/// Sans identité (ni en-tête SSOwat ni utilisateur simulé), il n'y a pas de liste : on ne rend
/// rien et on n'écrit rien, plutôt que de rattacher des envies à un propriétaire inventé.
/// </para>
/// </remarks>
public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
{
    public async Task<IReadOnlyList<SouhaitDto>> ListerAsync(
        string? utilisateur, CancellationToken ct = default)
    {
        if (utilisateur is null)
        {
            return [];
        }

        var souhaits = await db.LivresSouhaites
            .AsNoTracking()
            .Where(s => s.Utilisateur == utilisateur)
            .OrderBy(s => s.AuteurNormalise == string.Empty)
            .ThenBy(s => s.AuteurNormalise)
            .ThenBy(s => s.TitreNormalise)
            .ToListAsync(ct);

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

    public async Task<ResultatSouhait> AjouterAsync(
        EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default)
    {
        if (utilisateur is null)
        {
            return ResultatSouhait.Invalide(
                "Impossible d'ajouter une envie sans savoir à qui elle appartient.");
        }

        if (string.IsNullOrWhiteSpace(saisie.Titre))
        {
            return ResultatSouhait.Invalide("Le titre est obligatoire.");
        }

        // L'ISBN reste facultatif — on souhaite souvent une œuvre sans avoir choisi son édition —
        // mais s'il est saisi il doit tenir debout, comme pour un livre du catalogue.
        string? isbn = null;
        if (!string.IsNullOrWhiteSpace(saisie.Isbn))
        {
            var normalise = IsbnUtils.Normaliser(saisie.Isbn);
            if (!IsbnUtils.EstValide(normalise))
            {
                return ResultatSouhait.Invalide(
                    $"« {saisie.Isbn} » n'est pas un ISBN valide. Laissez le champ vide si vous ne l'avez pas.");
            }

            isbn = normalise;
        }

        var souhait = new LivreSouhaite
        {
            Utilisateur = utilisateur,
            Titre = saisie.Titre,
            Auteur = saisie.Auteur,
            Editeur = Vide(saisie.Editeur),
            Annee = Vide(saisie.Annee),
            Isbn = isbn,
            CoverUrl = Vide(saisie.CoverUrl),
            Note = Vide(saisie.Note),
            DateAjout = DateTime.UtcNow,
        };

        souhait.RecalculerFormes();

        // Le doublon est refusé avec un message plutôt que laissé à l'index unique : l'écran de
        // bibliographie rend le double clic facile, et « UNIQUE constraint failed » ne veut rien
        // dire pour l'utilisateur.
        var deja = await db.LivresSouhaites.AnyAsync(
            s => s.Utilisateur == utilisateur
                 && s.TitreNormalise == souhait.TitreNormalise
                 && s.AuteurNormalise == souhait.AuteurNormalise,
            ct);

        if (deja)
        {
            return ResultatSouhait.Invalide($"« {souhait.Titre} » est déjà dans votre liste d'envies.");
        }

        db.LivresSouhaites.Add(souhait);
        await db.SaveChangesAsync(ct);

        return ResultatSouhait.Ok(Projeter(souhait));
    }

    public async Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default)
    {
        if (utilisateur is null)
        {
            return false;
        }

        // Le filtre sur l'utilisateur fait partie de la CLÉ de recherche, pas d'une vérification
        // ultérieure : ainsi personne ne peut supprimer l'envie d'un autre en devinant son
        // identifiant. Une envie inexistante et une envie appartenant à un autre sont
        // indiscernables de l'extérieur, ce qui est bien le comportement voulu.
        var souhait = await db.LivresSouhaites
            .FirstOrDefaultAsync(s => s.Id == id && s.Utilisateur == utilisateur, ct);

        if (souhait is null)
        {
            return false;
        }

        db.LivresSouhaites.Remove(souhait);
        await db.SaveChangesAsync(ct);
        return true;
    }

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

    /// <summary>
    /// Projection vers le DTO. <b>Sans le champ <c>Utilisateur</c></b> : il ne sort jamais de
    /// l'API, pour qu'aucun client ne puisse croire qu'il désigne de qui il parle.
    /// </summary>
    internal static SouhaitDto Projeter(LivreSouhaite souhait) => new()
    {
        Id = souhait.Id,
        Titre = souhait.Titre,
        Auteur = souhait.Auteur,
        Editeur = souhait.Editeur,
        Annee = souhait.Annee,
        Isbn = souhait.Isbn,
        CoverUrl = souhait.CoverUrl,
        Note = souhait.Note,
        DateAjout = souhait.DateAjout,
    };
}
