using System.Text.Json;
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

namespace GerbilManagerWebAPI.Endpoints
{
    /// <summary>
    /// FEEDBACK: the "Fehler melden" report sink + ticket management ("Meine Tickets").
    ///   POST   /feedback       -> persist a user bug report (with captured debug context), returns 201.
    ///   GET    /feedback       -> list reports, newest first (for the ticket list).
    ///   PUT    /feedback/{id}  -> edit message, toggle status, attach a clarifying question (Rückfrage),
    ///                             or submit the breeder's answer. Question -> NeedsInfo; Answer -> Answered + AnsweredAt.
    ///   DELETE /feedback/{id}  -> remove a report. 404 on missing id.
    /// Feedback is decoupled from gerbils/litters (loose nullable Guid columns, no FK), so
    /// rows survive the import re-ingest wipe.
    /// </summary>
    public static class FeedbackEndpoints
    {
        /// <summary>Aufbewahrungsfrist im Papierkorb: danach werden Tickets endgültig gelöscht.</summary>
        private const int TrashRetentionDays = 30;

        /// <summary>Maximale Anhang-Größe (10 MB) — Fotos vom Handy passen locker, schützt aber die DB.</summary>
        private const int MaxAttachmentBytes = 10 * 1024 * 1024;

        public static IEndpointRouteBuilder MapFeedbackEndpoints(this IEndpointRouteBuilder app)
        {
            var group = app.MapGroup("/feedback").WithTags("Feedback");

            group.MapPost("/", async Task<Results<Created<FeedbackDto>, BadRequest<string>>> (
                FeedbackInput input, ApplicationContext db, HttpContext http) =>
            {
                if (string.IsNullOrWhiteSpace(input.Message))
                    return TypedResults.BadRequest("Message darf nicht leer sein.");

                var entity = new Feedback
                {
                    Id = Guid.NewGuid(),
                    Message = input.Message.Trim(),
                    Context = string.IsNullOrWhiteSpace(input.Context) ? "unknown" : input.Context.Trim(),
                    GerbilId = input.GerbilId,
                    LitterId = input.LitterId,
                    ContactId = input.ContactId,
                    EntityName = input.EntityName,
                    Url = input.Url,
                    ClientTimestamp = input.ClientTimestamp,
                    UserAgent = http.Request.Headers.UserAgent.ToString() is { Length: > 0 } ua ? ua : null,
                    CreatedAt = DateTimeOffset.UtcNow,
                    Status = "Open",
                    ResolvedAt = null,
                };
                db.Feedback.Add(entity);
                await db.SaveChangesAsync();
                return TypedResults.Created($"/feedback/{entity.Id}", ToDto(entity));
            });

            group.MapGet("/", async (ApplicationContext db) =>
            {
                // In memory verarbeiten: SQLite (Test-Host) kann weder ORDER BY noch WHERE-Vergleiche
                // auf DateTimeOffset-Spalten übersetzen.
                var rows = await db.Feedback.ToListAsync();

                // Aufräumen: soft-gelöschte Tickets werden nach TrashRetentionDays endgültig entfernt
                // (lazy beim Abruf — genügt für Single-User, kein Hintergrunddienst nötig).
                var cutoff = DateTimeOffset.UtcNow.AddDays(-TrashRetentionDays);
                var expired = rows.Where(f => f.DeletedAt is { } d && d < cutoff).ToList();
                if (expired.Count > 0)
                {
                    db.Feedback.RemoveRange(expired);
                    await db.SaveChangesAsync();
                    rows = rows.Except(expired).ToList();
                }

                // Anhang-Metadaten (OHNE Bytes) laden und je Ticket zuordnen.
                var attMeta = await db.FeedbackAttachments
                    .Select(a => new { a.Id, a.FeedbackId, a.FileName, a.ContentType, a.Size })
                    .ToListAsync();
                var byTicket = attMeta
                    .GroupBy(a => a.FeedbackId)
                    .ToDictionary(
                        g => g.Key,
                        g => (IReadOnlyList<FeedbackAttachmentDto>)g
                            .Select(a => new FeedbackAttachmentDto(a.Id, a.FileName, a.ContentType, a.Size))
                            .ToList());

                return TypedResults.Ok(rows
                    .OrderByDescending(f => f.CreatedAt)
                    .Select(f => ToDto(f, byTicket.GetValueOrDefault(f.Id)))
                    .ToList());
            });

            group.MapPut("/{id:guid}", async Task<Results<Ok<FeedbackDto>, NotFound, BadRequest<string>>> (
                Guid id, FeedbackUpdate input, ApplicationContext db, Push.PushNotifier push) =>
            {
                var entity = await db.Feedback.FirstOrDefaultAsync(f => f.Id == id);
                if (entity is null)
                    return TypedResults.NotFound();

                // Zustand VOR den Mutationen merken — für die Wiederöffnen-Erkennung weiter unten.
                var wasResolved = entity.Status.Equals("Resolved", StringComparison.OrdinalIgnoreCase);
                var hadOpenRueckfrage = !string.IsNullOrWhiteSpace(entity.Question);

                if (input.Message is not null)
                {
                    if (string.IsNullOrWhiteSpace(input.Message))
                        return TypedResults.BadRequest("Message darf nicht leer sein.");
                    entity.Message = input.Message.Trim();
                }

                // Attach a clarifying question (Rückfrage). A non-empty question moves the ticket to
                // NeedsInfo (waiting on the breeder) unless it is already Resolved. A blank/whitespace
                // question clears it. If a PREVIOUS Q&A exchange already exists, archive it into the
                // thread (oldest first) before overwriting — so earlier rounds aren't lost — and clear
                // the current Answer so the new question starts a fresh open exchange.
                if (input.Question is not null)
                {
                    var q = input.Question.Trim();
                    if (q.Length > 0)
                    {
                        ArchiveCurrentExchange(entity);
                        entity.Question = q;
                        if (!entity.Status.Equals("Resolved", StringComparison.OrdinalIgnoreCase))
                        {
                            entity.Status = "NeedsInfo";
                            entity.ResolvedAt = null;
                        }
                    }
                    else
                    {
                        entity.Question = null;
                    }
                }

                // The breeder's answer. A non-empty answer stamps AnsweredAt and moves to Answered.
                if (input.Answer is not null)
                {
                    var a = input.Answer.Trim();
                    if (a.Length == 0)
                    {
                        entity.Answer = null;
                        entity.AnsweredAt = null;
                    }
                    else
                    {
                        entity.Answer = a;
                        entity.AnsweredAt = DateTimeOffset.UtcNow;
                        entity.Status = "Answered";
                        entity.ResolvedAt = null;
                    }
                }

                if (input.Status is not null)
                {
                    // Normalize to a known lifecycle state. Resolving stamps ResolvedAt; any other
                    // state clears it. Open/NeedsInfo/Answered/Resolved are accepted (case-insensitive),
                    // anything else falls back to Open.
                    var status = input.Status.Trim();
                    var resolved = status.Equals("Resolved", StringComparison.OrdinalIgnoreCase);
                    var normalized = status switch
                    {
                        _ when status.Equals("Resolved", StringComparison.OrdinalIgnoreCase) => "Resolved",
                        _ when status.Equals("NeedsInfo", StringComparison.OrdinalIgnoreCase) => "NeedsInfo",
                        _ when status.Equals("Answered", StringComparison.OrdinalIgnoreCase) => "Answered",
                        _ => "Open",
                    };
                    entity.Status = normalized;
                    entity.ResolvedAt = resolved
                        ? (entity.ResolvedAt ?? DateTimeOffset.UtcNow)
                        : null;

                    // Wiederöffnen-Zeitstempel: nur, wenn ein ECHT gelöstes Ticket (ohne offene
                    // Rückfrage) wieder geöffnet wird. Tickets, die nur eine offene Rückfrage hatten
                    // und manuell auf gelöst gesetzt wurden, kehren beim Wiederöffnen einfach in
                    // ihre Rückfrage zurück und bekommen KEINEN Zeitstempel.
                    if (!resolved && wasResolved && !hadOpenRueckfrage)
                        entity.ReopenedAt = DateTimeOffset.UtcNow;
                }

                // FIX-NOTE: breeder-friendly changelog (typically set together with status=Resolved).
                // A blank/whitespace value clears it.
                if (input.FixNote is not null)
                {
                    var note = input.FixNote.Trim();
                    entity.FixNote = note.Length == 0 ? null : note;
                }

                // AGENT-CONTEXT (internal working memory): updated without changing the status.
                // A blank/whitespace value clears it.
                if (input.AgentContext is not null)
                {
                    var c = input.AgentContext.Trim();
                    entity.AgentContext = c.Length == 0 ? null : c;
                }

                // KATEGORIE: frei wählbares Thema für Filter/Übersicht; leer = löschen.
                if (input.Category is not null)
                {
                    var cat = input.Category.Trim();
                    entity.Category = cat.Length == 0 ? null : cat;
                }

                // HILFREICH (👍/👎): nur setzen, wenn übermittelt (null = unverändert).
                if (input.Helpful is not null)
                    entity.Helpful = input.Helpful;

                await db.SaveChangesAsync();

                // Push an die Züchterin, wenn die KI das anfordert (notify=true). Nachricht aus dem
                // resultierenden Status ableiten. Fehler dürfen die Antwort nicht stören.
                if (input.Notify == true && push.Enabled)
                {
                    var name = string.IsNullOrWhiteSpace(entity.EntityName) ? "" : $" ({entity.EntityName})";
                    var (title, body) = entity.Status switch
                    {
                        "NeedsInfo" => ("Neue Rückfrage" + name, Trim(entity.Question) ?? "Bitte schau in deine Tickets."),
                        "Resolved" => ("Ticket gelöst" + name, Trim(entity.FixNote) ?? Trim(entity.Message) ?? "Erledigt."),
                        _ => ("Neues zu deinem Ticket" + name, Trim(entity.Message) ?? ""),
                    };
                    try
                    {
                        await push.NotifyAllAsync(title, body, $"/hilfe/tickets?focus={entity.Id}");
                    }
                    catch (Exception)
                    {
                        // Push ist best-effort — niemals die API-Antwort daran scheitern lassen.
                    }
                }

                return TypedResults.Ok(ToDto(entity));
            });

            // SOFT-DELETE: das Ticket wird NICHT entfernt, sondern als gelöscht markiert
            // (DeletedAt = jetzt) und wandert in die „Gelöscht"-Kategorie. Wiederherstellbar
            // über POST /feedback/{id}/restore.
            group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (
                Guid id, ApplicationContext db) =>
            {
                var entity = await db.Feedback.FirstOrDefaultAsync(f => f.Id == id);
                if (entity is null)
                    return TypedResults.NotFound();

                entity.DeletedAt ??= DateTimeOffset.UtcNow;
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            // WIEDERHERSTELLEN: hebt das Soft-Delete auf (DeletedAt → null); das Ticket kehrt in
            // seinen vorherigen Status (Offen/Rückfrage/…) zurück.
            group.MapPost("/{id:guid}/restore", async Task<Results<Ok<FeedbackDto>, NotFound>> (
                Guid id, ApplicationContext db) =>
            {
                var entity = await db.Feedback.FirstOrDefaultAsync(f => f.Id == id);
                if (entity is null)
                    return TypedResults.NotFound();

                entity.DeletedAt = null;
                await db.SaveChangesAsync();
                return TypedResults.Ok(ToDto(entity));
            });

            // ANHANG hochladen (base64). Bild/Datei zu einem Ticket. Größenlimit MaxAttachmentBytes.
            group.MapPost("/{id:guid}/attachments", async Task<Results<Created<FeedbackAttachmentDto>, NotFound, BadRequest<string>>> (
                Guid id, FeedbackAttachmentInput input, ApplicationContext db) =>
            {
                var ticket = await db.Feedback.FirstOrDefaultAsync(f => f.Id == id);
                if (ticket is null)
                    return TypedResults.NotFound();
                if (string.IsNullOrWhiteSpace(input.DataBase64) || string.IsNullOrWhiteSpace(input.FileName))
                    return TypedResults.BadRequest("FileName und Daten sind erforderlich.");

                byte[] bytes;
                try
                {
                    // erlaubt sowohl reines base64 als auch eine data:-URL
                    var raw = input.DataBase64;
                    var comma = raw.IndexOf(',');
                    if (raw.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
                        raw = raw[(comma + 1)..];
                    bytes = Convert.FromBase64String(raw);
                }
                catch (FormatException)
                {
                    return TypedResults.BadRequest("Daten sind kein gültiges base64.");
                }
                if (bytes.Length == 0)
                    return TypedResults.BadRequest("Datei ist leer.");
                if (bytes.Length > MaxAttachmentBytes)
                    return TypedResults.BadRequest($"Datei zu groß (max. {MaxAttachmentBytes / (1024 * 1024)} MB).");

                var att = new FeedbackAttachment
                {
                    Id = Guid.NewGuid(),
                    FeedbackId = id,
                    FileName = input.FileName.Trim(),
                    ContentType = string.IsNullOrWhiteSpace(input.ContentType) ? "application/octet-stream" : input.ContentType.Trim(),
                    Size = bytes.Length,
                    Data = bytes,
                    CreatedAt = DateTimeOffset.UtcNow,
                };
                db.FeedbackAttachments.Add(att);
                await db.SaveChangesAsync();
                return TypedResults.Created($"/feedback/attachments/{att.Id}",
                    new FeedbackAttachmentDto(att.Id, att.FileName, att.ContentType, att.Size));
            });

            // ANHANG-Bytes ausliefern (für <img>/Download).
            group.MapGet("/attachments/{attId:guid}", async Task<Results<FileContentHttpResult, NotFound>> (
                Guid attId, ApplicationContext db) =>
            {
                var att = await db.FeedbackAttachments.AsNoTracking().FirstOrDefaultAsync(a => a.Id == attId);
                if (att is null)
                    return TypedResults.NotFound();
                return TypedResults.File(att.Data, att.ContentType, att.FileName);
            });

            // ANHANG löschen.
            group.MapDelete("/attachments/{attId:guid}", async Task<Results<NoContent, NotFound>> (
                Guid attId, ApplicationContext db) =>
            {
                var att = await db.FeedbackAttachments.FirstOrDefaultAsync(a => a.Id == attId);
                if (att is null)
                    return TypedResults.NotFound();
                db.FeedbackAttachments.Remove(att);
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            return app;
        }

        private static readonly JsonSerializerOptions ThreadJson = new(JsonSerializerDefaults.Web);

        /// <summary>Für Push-Texte: leeren Wert zu null, sonst auf ~140 Zeichen kürzen.</summary>
        private static string? Trim(string? s)
        {
            if (string.IsNullOrWhiteSpace(s)) return null;
            var t = s.Trim();
            return t.Length > 140 ? t[..139] + "…" : t;
        }

        /// <summary>
        /// Append the ticket's current (Question, Answer) exchange to the thread/history JSON
        /// before it gets overwritten by a new round, then clear the current Answer. Only the
        /// non-empty parts are archived (a question with no answer yet is still preserved).
        /// </summary>
        private static void ArchiveCurrentExchange(Feedback entity)
        {
            var history = DeserializeThread(entity.Thread).ToList();
            var archivedAny = false;

            if (!string.IsNullOrWhiteSpace(entity.Question))
            {
                history.Add(new FeedbackThreadEntry("maintainer", entity.Question!, entity.CreatedAt));
                archivedAny = true;
            }
            if (!string.IsNullOrWhiteSpace(entity.Answer))
            {
                history.Add(new FeedbackThreadEntry("breeder", entity.Answer!, entity.AnsweredAt));
                archivedAny = true;
            }

            if (archivedAny)
                entity.Thread = JsonSerializer.Serialize(history, ThreadJson);

            // The new question starts a fresh open exchange — clear the prior answer.
            entity.Answer = null;
            entity.AnsweredAt = null;
        }

        private static IReadOnlyList<FeedbackThreadEntry> DeserializeThread(string? json)
        {
            if (string.IsNullOrWhiteSpace(json))
                return Array.Empty<FeedbackThreadEntry>();
            try
            {
                return JsonSerializer.Deserialize<List<FeedbackThreadEntry>>(json, ThreadJson)
                       ?? new List<FeedbackThreadEntry>();
            }
            catch (JsonException)
            {
                return Array.Empty<FeedbackThreadEntry>();
            }
        }

        private static FeedbackDto ToDto(Feedback f, IReadOnlyList<FeedbackAttachmentDto>? attachments = null) =>
            new(f.Id, f.Message, f.Context, f.GerbilId, f.LitterId, f.ContactId, f.EntityName, f.Url,
                f.ClientTimestamp, f.UserAgent, f.CreatedAt, f.Status, f.ResolvedAt,
                f.Question, f.Answer, f.AnsweredAt, f.FixNote, f.AgentContext,
                DeserializeThread(f.Thread), f.ReopenedAt, f.DeletedAt, f.Category, f.Helpful,
                attachments ?? Array.Empty<FeedbackAttachmentDto>());
    }
}
