using GerbilManagerWebAPI.Common;
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Import;
using GerbilManagerWebAPI.Models;
using GerbilManagerWebAPI.Services;
using Gridify;
using Gridify.EntityFramework;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace GerbilManagerWebAPI.Endpoints
{
    public static class GerbilEndpoints
    {
        public static IEndpointRouteBuilder MapGerbilEndpoints(this IEndpointRouteBuilder app)
        {
            var group = app.MapGroup("/gerbils").WithTags("Gerbils");

            // GET /gerbils  (Gridify: filter/order/page; e.g. status==Active, litterId==…, orderBy=name)
            group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) =>
            {
                var q = query.ToQuery();
                var paging = await db.Gerbils.AsNoTracking().GridifyAsync(q);
                var ids = paging.Data.Select(g => g.Id).ToList();
                var photoMap = ids.Count == 0 ? new Dictionary<Guid, string>() :
                    await db.GerbilPhotos.AsNoTracking()
                        .Where(p => ids.Contains(p.GerbilId))
                        .GroupBy(p => p.GerbilId)
                        .Select(g => new { GerbilId = g.Key, FileName = g.OrderBy(p => p.SortOrder).First().FileName })
                        .ToDictionaryAsync(x => x.GerbilId, x => x.FileName);
                var items = paging.Data
                    .Select(g => ToDto(g, photoMap.TryGetValue(g.Id, out var fn) ? $"/photos/files/{fn}" : null))
                    .ToList();
                return TypedResults.Ok(new PagedResult<GerbilDto>(items, paging.Count, q.Page, q.PageSize));
            });

            // GET /gerbils/breeders — distinct non-empty Herkunft values for the Tiere filter dropdown
            group.MapGet("/breeders", async (ApplicationContext db) =>
                TypedResults.Ok(await db.Gerbils.AsNoTracking()
                    .Where(g => g.OriginBreeder != null && g.OriginBreeder != "")
                    .Select(g => g.OriginBreeder!)
                    .Distinct().OrderBy(b => b).ToListAsync()));

            // GET /gerbils/{id}
            group.MapGet("/{id:guid}", async Task<Results<Ok<GerbilDto>, NotFound>> (Guid id, ApplicationContext db) =>
            {
                var g = await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
                if (g is null) return TypedResults.NotFound();
                var photoFileName = await db.GerbilPhotos.AsNoTracking()
                    .Where(p => p.GerbilId == id)
                    .OrderBy(p => p.SortOrder)
                    .Select(p => p.FileName)
                    .FirstOrDefaultAsync();
                var profilePhotoUrl = photoFileName != null ? $"/photos/files/{photoFileName}" : null;
                return TypedResults.Ok(ToDto(g, profilePhotoUrl));
            });

            // POST /gerbils
            group.MapPost("/", async Task<Results<Created<GerbilDto>, ValidationProblem>> (GerbilInput input, ApplicationContext db) =>
            {
                if (string.IsNullOrWhiteSpace(input.Name))
                    return TypedResults.ValidationProblem(new Dictionary<string, string[]> { ["name"] = ["Name is required."] });

                // Manually created in the UI → IsManual=true, so the import re-ingest never wipes or
                // overwrites this animal (and its sub-records).
                var g = new Gerbil { Id = Guid.NewGuid(), Name = input.Name!, IsManual = true };
                Apply(g, input, isCreate: true);
                db.Gerbils.Add(g);
                await db.SaveChangesAsync();
                return TypedResults.Created($"/gerbils/{g.Id}", ToDto(g));
            });

            // PUT /gerbils/{id}  — PATCH semantics: omitted/null fields keep the stored value.
            group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, GerbilInput input, ApplicationContext db) =>
            {
                var g = await db.Gerbils.FirstOrDefaultAsync(x => x.Id == id);
                if (g is null) return TypedResults.NotFound();

                // Capture the own-field values BEFORE the edit so manual changes to an IMPORTED animal
                // can be pinned per-field (survive the next ingest). Manual animals need no override —
                // they survive the ingest wholesale.
                var before = GerbilSnapshotService.BuildFreezeObject(g);
                Apply(g, input, isCreate: false);
                await db.SaveChangesAsync();

                if (!g.IsManual)
                    await RecordEditOverrideAsync(db, g, before);

                return TypedResults.NoContent();
            });

            // DELETE /gerbils/{id}  (409 if referenced as a litter parent)
            group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound, Conflict<string>>> (Guid id, ApplicationContext db) =>
            {
                var g = await db.Gerbils.FirstOrDefaultAsync(x => x.Id == id);
                if (g is null) return TypedResults.NotFound();
                db.Gerbils.Remove(g);
                try
                {
                    await db.SaveChangesAsync();
                    return TypedResults.NoContent();
                }
                catch (DbUpdateException)
                {
                    return TypedResults.Conflict("Gerbil is referenced as a litter parent and cannot be deleted.");
                }
            });

            return app;
        }

        // CR-2 FIX: PATCH semantics — every omitted/null field keeps the stored value.
        // Prevents silent data loss when the frontend sends partial bodies (ForSale toggle,
        // Charakterbogen save, any partial updateGerbil call). On create, supply safe defaults
        // for fields the frontend omits. A non-null input value always wins (including explicit
        // nulls — callers that want to clear a nullable field must send a full object; a
        // dedicated PATCH endpoint can be added later if point-clear is needed).
        private static void Apply(Gerbil g, GerbilInput i, bool isCreate)
        {
            if (!string.IsNullOrWhiteSpace(i.Name)) g.Name = i.Name!;
            g.Gender = i.Gender ?? (isCreate ? Gender.unknown : g.Gender);
            // Status is applied as a user preference and then overridden by GerbilStatusService.
            g.Status = i.Status ?? (isCreate ? GerbilStatus.Breeding : g.Status);
            g.LitterId = i.LitterId ?? g.LitterId;
            g.OriginContactId = i.OriginContactId ?? g.OriginContactId;
            g.ReceiverContactId = i.ReceiverContactId ?? g.ReceiverContactId;
            g.EnclosureId = i.EnclosureId ?? g.EnclosureId;
            g.ColorVarietyId = i.ColorVarietyId ?? g.ColorVarietyId;
            g.DateOfBirth = i.DateOfBirth ?? g.DateOfBirth;
            g.DateOfDeath = i.DateOfDeath ?? g.DateOfDeath;
            g.CauseOfDeath = i.CauseOfDeath ?? g.CauseOfDeath;
            g.GoHomeDate = i.GoHomeDate ?? g.GoHomeDate;
            g.Genotype = i.Genotype ?? g.Genotype;
            g.SpottingType = i.SpottingType ?? g.SpottingType;
            g.Notes = i.Notes ?? g.Notes;
            g.ImportSource = i.ImportSource ?? g.ImportSource;
            g.ExternalRef = i.ExternalRef ?? g.ExternalRef;
            g.OriginBreeder = i.OriginBreeder ?? g.OriginBreeder;
            g.CharacterTraits = i.CharacterTraits ?? g.CharacterTraits;
            g.CharacterNote = i.CharacterNote ?? g.CharacterNote;
            g.IsDeaf = i.IsDeaf ?? g.IsDeaf;
            g.IsResident = i.IsResident ?? (isCreate ? true : g.IsResident);
            g.IsCastrated = i.IsCastrated ?? (isCreate ? false : g.IsCastrated);
            GerbilStatusService.Apply(g, DateOnly.FromDateTime(DateTime.UtcNow));
        }

        // EDIT PROTECTION: after a manual edit to an IMPORTED animal, pin the change so the next
        // ingest can't overwrite it. A VERIFIED animal keeps its certification and its whole golden
        // is refreshed to the edited state (Frage 5). An unverified animal gets/updates a "geschützt"
        // override carrying ONLY the changed fields (per-field) — untouched fields keep receiving
        // import improvements. Called after the edit is already persisted.
        private static async Task RecordEditOverrideAsync(ApplicationContext db, Gerbil g, System.Text.Json.Nodes.JsonObject before)
        {
            var ov = await db.GerbilOverrides.FirstOrDefaultAsync(o => o.GerbilId == g.Id);
            if (ov is { IsVerified: true })
            {
                ov.EntityName = g.Name;
                ov.OverrideJson = GerbilSnapshotService.BuildFreezeJson(g);
                var snap = await GerbilSnapshotService.BuildSnapshotAsync(db, g.Id);
                if (snap is not null) ov.SnapshotJson = GerbilSnapshotService.SerializeSnapshot(snap);
                ov.LastImportSnapshotJson = null;
                ov.LastImportDiffJson = null;
                ov.UpdatedAt = DateTimeOffset.UtcNow;
                await db.SaveChangesAsync();
                return;
            }

            var after = GerbilSnapshotService.BuildFreezeObject(g);
            var changed = GerbilSnapshotService.ChangedFields(before, after);
            if (changed.Count == 0) return;

            if (ov is null)
            {
                ov = new GerbilOverride { Id = Guid.NewGuid(), GerbilId = g.Id, IsVerified = false, OverrideJson = "{}" };
                db.GerbilOverrides.Add(ov);
            }
            ov.EntityName = g.Name;
            ov.OverrideJson = GerbilSnapshotService.MergeOverrideJson(ov.OverrideJson, changed);
            ov.UpdatedAt = DateTimeOffset.UtcNow;
            await db.SaveChangesAsync();
        }

        internal static GerbilDto ToDto(Gerbil g, string? profilePhotoUrl = null) => new(
            g.Id, g.Name, g.Gender, g.Status, g.LitterId, g.OriginContactId, g.ReceiverContactId,
            g.EnclosureId, g.ColorVarietyId, g.DateOfBirth, g.DateOfDeath, g.CauseOfDeath,
            g.GoHomeDate, g.Genotype, g.SpottingType, g.Notes, g.ImportSource, g.ExternalRef, g.OriginBreeder,
            g.CharacterTraits, g.CharacterNote, g.IsDeaf, g.IsResident, profilePhotoUrl, g.IsCastrated,
            g.Provenance, g.BirthOrder);
    }
}
