using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Identite;
using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;

namespace MaBibli.Api.Endpoints;

public static class LivresEndpoints
{
    /// <summary>
    /// CRUD du catalogue.
    /// </summary>
    /// <remarks>
    /// <b>Aucune lecture ne filtre sur <c>AjoutePar</c></b> : la bibliothèque est commune.
    /// Ce champ n'est même pas acceptable en paramètre de requête, précisément pour qu'on ne
    /// puisse pas s'en servir comme d'un cloisonnement par inadvertance.
    /// <para>
    /// Le statut de lecture, lui, est <b>personnel</b> : il est toujours celui de l'appelant,
    /// déduit des en-têtes SSOwat. Aucun point d'entrée n'accepte un nom d'utilisateur — on ne
    /// consulte ni ne modifie la lecture de quelqu'un d'autre.
    /// </para>
    /// </remarks>
    public static IEndpointRouteBuilder MapLivresEndpoints(this IEndpointRouteBuilder routes)
    {
        var groupe = routes.MapGroup("/api/livres").WithTags("Livres");

        groupe.MapGet("/", async (
                string? recherche,
                Format? format,
                Statut? statut,
                int? auteurId,
                IServiceCatalogue service,
                IFournisseurUtilisateur utilisateurs,
                CancellationToken ct) =>
            {
                var criteres = new CritereLivres
                {
                    Recherche = recherche,
                    Format = format,
                    Statut = statut,
                    AuteurId = auteurId,
                };

                var utilisateur = utilisateurs.Obtenir().Identifiant;
                return Results.Ok(await service.ListerAsync(criteres, utilisateur, ct));
            })
            .WithName("ListerLivres")
            .WithSummary("Liste le catalogue, filtrable par format, statut et auteur ; cherchable sur titre et auteurs.")
            .WithDescription(
                "Catalogue unique : physiques et numériques cohabitent, distingués par leur format. "
                + "La bibliothèque est commune — la liste n'est jamais restreinte à l'utilisateur courant. "
                + "Le filtre « statut » porte en revanche sur le statut de lecture de l'appelant, "
                + "qui lui est propre. La recherche ignore la casse et les accents.")
            .Produces<IReadOnlyList<LivreDto>>();

        groupe.MapGet("/{id:int}", async (
                int id,
                IServiceCatalogue service,
                IFournisseurUtilisateur utilisateurs,
                CancellationToken ct) =>
            {
                var livre = await service.ObtenirAsync(id, utilisateurs.Obtenir().Identifiant, ct);
                return livre is null ? Results.NotFound() : Results.Ok(livre);
            })
            .WithName("ObtenirLivre")
            .Produces<LivreDto>()
            .Produces(StatusCodes.Status404NotFound);

        groupe.MapPost("/", async (
                EnregistrementLivre saisie,
                IServiceCatalogue service,
                IFournisseurUtilisateur utilisateurs,
                CancellationToken ct) =>
            {
                // AjoutePar vient du serveur, pas de la charge utile.
                var utilisateur = utilisateurs.Obtenir();
                var resultat = await service.CreerAsync(saisie, utilisateur.Identifiant, ct);

                return resultat.Erreur is not null
                    ? Results.BadRequest(new { erreur = resultat.Erreur })
                    : Results.Created($"/api/livres/{resultat.Livre!.Id}", resultat.Livre);
            })
            .WithName("CreerLivre")
            .WithSummary("Ajoute un livre. « AjoutePar » est déterminé par le serveur.")
            .Produces<LivreDto>(StatusCodes.Status201Created)
            .Produces(StatusCodes.Status400BadRequest);

        groupe.MapPut("/{id:int}", async (
                int id,
                EnregistrementLivre saisie,
                IServiceCatalogue service,
                IFournisseurUtilisateur utilisateurs,
                CancellationToken ct) =>
            {
                var resultat = await service.ModifierAsync(
                    id, saisie, utilisateurs.Obtenir().Identifiant, ct);

                if (resultat.EstIntrouvable)
                {
                    return Results.NotFound();
                }

                return resultat.Erreur is not null
                    ? Results.BadRequest(new { erreur = resultat.Erreur })
                    : Results.Ok(resultat.Livre);
            })
            .WithName("ModifierLivre")
            .WithSummary("Modifie un livre. « AjoutePar » et « DateAjout » restent inchangés ; le statut posé est celui de l'appelant.")
            .Produces<LivreDto>()
            .Produces(StatusCodes.Status400BadRequest)
            .Produces(StatusCodes.Status404NotFound);

        groupe.MapPatch("/{id:int}/statut", async (
                int id,
                ChangementStatut changement,
                IServiceCatalogue service,
                IFournisseurUtilisateur utilisateurs,
                CancellationToken ct) =>
            {
                var livre = await service.ChangerStatutAsync(
                    id, changement.Statut, utilisateurs.Obtenir().Identifiant, ct);

                return livre is null ? Results.NotFound() : Results.Ok(livre);
            })
            .WithName("ChangerStatutLivre")
            .WithSummary("Change le seul statut de lecture de l'appelant — l'action la plus fréquente.")
            .WithDescription("Un statut nul remet le livre à « non commencé » : la ligne est supprimée.")
            .Produces<LivreDto>()
            .Produces(StatusCodes.Status404NotFound);

        groupe.MapDelete("/{id:int}", async (int id, IServiceCatalogue service, CancellationToken ct) =>
            {
                var supprime = await service.SupprimerAsync(id, ct);
                return supprime ? Results.NoContent() : Results.NotFound();
            })
            .WithName("SupprimerLivre")
            .Produces(StatusCodes.Status204NoContent)
            .Produces(StatusCodes.Status404NotFound);

        return routes;
    }

    /// <summary>
    /// Consultation des auteurs et arbitrage des rapprochements ambigus.
    /// </summary>
    /// <remarks>
    /// Les variantes sûres sont réunies automatiquement à l'écriture d'un livre et n'atteignent
    /// jamais ces points d'entrée. Ce qui arrive ici est ce que la machine <b>refuse</b> de
    /// trancher : un patronyme seul face à un nom complet, où seul l'utilisateur sait s'il
    /// s'agit d'une même personne.
    /// </remarks>
    public static IEndpointRouteBuilder MapAuteursEndpoints(this IEndpointRouteBuilder routes)
    {
        var groupe = routes.MapGroup("/api/auteurs").WithTags("Auteurs");

        groupe.MapGet("/", async (IServiceAuteurs service, CancellationToken ct) =>
                Results.Ok(await service.ListerAsync(ct)))
            .WithName("ListerAuteurs")
            .WithSummary("Tous les auteurs du catalogue, avec leur nombre de livres.")
            .Produces<IReadOnlyList<AuteurDto>>();

        groupe.MapGet("/{id:int}", async (int id, IServiceAuteurs service, CancellationToken ct) =>
            {
                var auteur = await service.ObtenirAsync(id, ct);
                return auteur is null ? Results.NotFound() : Results.Ok(auteur);
            })
            .WithName("ObtenirAuteur")
            .Produces<AuteurDto>()
            .Produces(StatusCodes.Status404NotFound);

        groupe.MapGet("/rapprochements", async (IServiceAuteurs service, CancellationToken ct) =>
                Results.Ok(await service.SuggestionsAsync(ct)))
            .WithName("ListerRapprochements")
            .WithSummary("Rapprochements ambigus proposés, refus déjà écartés.")
            .Produces<IReadOnlyList<SuggestionRapprochement>>();

        groupe.MapPost("/rapprochements/accepter", async (
                DecisionRapprochement decision, IServiceAuteurs service, CancellationToken ct) =>
            {
                var fusionne = await service.FusionnerAsync(decision.SourceId, decision.CibleId, ct);
                return fusionne ? Results.NoContent() : Results.NotFound();
            })
            .WithName("AccepterRapprochement")
            .WithSummary("Fusionne les deux auteurs : « source » disparaît au profit de « cible ».")
            .Produces(StatusCodes.Status204NoContent)
            .Produces(StatusCodes.Status404NotFound);

        groupe.MapPost("/rapprochements/refuser", async (
                DecisionRapprochement decision, IServiceAuteurs service, CancellationToken ct) =>
            {
                var refuse = await service.RefuserAsync(decision.SourceId, decision.CibleId, ct);
                return refuse ? Results.NoContent() : Results.NotFound();
            })
            .WithName("RefuserRapprochement")
            .WithSummary("Mémorise que ces deux auteurs sont bien deux personnes : la suggestion ne reviendra plus.")
            .Produces(StatusCodes.Status204NoContent)
            .Produces(StatusCodes.Status404NotFound);

        return routes;
    }

    /// <summary>Expose l'identité de la personne connectée, pour affichage côté client.</summary>
    public static IEndpointRouteBuilder MapIdentiteEndpoints(this IEndpointRouteBuilder routes)
    {
        routes.MapGet("/api/moi", (IFournisseurUtilisateur utilisateurs) =>
                Results.Ok(utilisateurs.Obtenir()))
            .WithName("UtilisateurCourant")
            .WithTags("Identité")
            .WithSummary("Identité lue dans les en-têtes SSOwat, ou utilisateur simulé en développement.")
            .Produces<UtilisateurCourant>();

        return routes;
    }
}
