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

namespace GerbilManagerWebAPI.Endpoints
{
    /// <summary>
    /// WEB-0: CMS for the public website. GET /api/site-snapshot is the publish-time export
    /// (with Abgabetiere resolved from live ForSale animals); the /api/pages + /api/blocks
    /// CRUD edits content (hand-edit admin UI in WEB-0b, AI tools later). LAN-only like the
    /// rest of the API.
    /// </summary>
    public static class CmsEndpoints
    {
        public static IEndpointRouteBuilder MapCmsEndpoints(this IEndpointRouteBuilder app)
        {
            var api = app.MapGroup("/api").WithTags("CMS");

            // ---- snapshot (consumed by the static renderer) ----
            api.MapGet("/site-snapshot", async (ApplicationContext db) =>
                TypedResults.Ok(await new SiteSnapshotService(db).BuildAsync()));

            // ---- static render dry-run (WEB-1: returns rendered file map as JSON) ----
            api.MapGet("/render-site", async (ApplicationContext db) =>
            {
                var snapshot = await new SiteSnapshotService(db).BuildAsync();
                var files = SiteRenderer.Render(snapshot);
                return TypedResults.Ok(files.Select(kv => new { path = kv.Key, size = kv.Value.Length }).ToList());
            });

            // ---- WEB-3: lokale Vorschau — rendert live (nur veröffentlichte Seiten)
            //      und liefert die Datei mit passendem Content-Type aus. Relative
            //      Links/CSS der gerenderten Seite funktionieren dadurch im
            //      Vorschau-iframe genauso wie später auf der echten Webseite. ----
            api.MapGet("/preview/{**path}", async (string? path, ApplicationContext db) =>
            {
                var snapshot = await new SiteSnapshotService(db).BuildAsync();
                var files = SiteRenderer.Render(snapshot);

                var key = string.IsNullOrWhiteSpace(path) ? "index.html" : path.TrimEnd('/');
                if (!files.TryGetValue(key, out var content) &&
                    !files.TryGetValue($"{key}/index.html", out content))
                {
                    return Results.NotFound();
                }
                return Results.Content(content, PreviewContentType(key));
            });

            // ---- pages ----
            api.MapGet("/pages", async (ApplicationContext db) =>
                TypedResults.Ok(await db.Pages.AsNoTracking().OrderBy(p => p.Slug)
                    .Select(p => new PageSummaryDto(p.Id, p.Slug, p.Title, p.SeoDescription, p.Status))
                    .ToListAsync()));

            api.MapGet("/pages/{slug}", async Task<Results<Ok<PageDto>, NotFound>> (string slug, ApplicationContext db) =>
            {
                var p = await db.Pages.AsNoTracking().Include(x => x.Blocks)
                    .FirstOrDefaultAsync(x => x.Slug == slug);
                return p is null ? TypedResults.NotFound() : TypedResults.Ok(ToPageDto(p));
            });

            api.MapPost("/pages", async Task<Results<Created<PageDto>, ValidationProblem, Conflict<string>>> (PageInput input, ApplicationContext db) =>
            {
                if (string.IsNullOrWhiteSpace(input.Slug) || string.IsNullOrWhiteSpace(input.Title))
                    return TypedResults.ValidationProblem(new Dictionary<string, string[]> { ["slug"] = ["Slug and title are required."] });
                if (await db.Pages.AnyAsync(p => p.Slug == input.Slug))
                    return TypedResults.Conflict($"A page with slug '{input.Slug}' already exists.");

                var page = new Page
                {
                    Id = Guid.NewGuid(),
                    Slug = input.Slug,
                    Title = input.Title,
                    SeoDescription = input.SeoDescription,
                    Status = input.Status ?? PageStatus.Draft,
                };
                db.Pages.Add(page);
                await db.SaveChangesAsync();
                return TypedResults.Created($"/api/pages/{page.Slug}", ToPageDto(page));
            });

            api.MapPut("/pages/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, PageInput input, ApplicationContext db) =>
            {
                var page = await db.Pages.FirstOrDefaultAsync(p => p.Id == id);
                if (page is null) return TypedResults.NotFound();
                page.Slug = input.Slug;
                page.Title = input.Title;
                page.SeoDescription = input.SeoDescription;
                if (input.Status is PageStatus s) page.Status = s;
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            api.MapDelete("/pages/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
            {
                var page = await db.Pages.FirstOrDefaultAsync(p => p.Id == id);
                if (page is null) return TypedResults.NotFound();
                db.Pages.Remove(page);   // cascades blocks
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            // ---- blocks ----
            api.MapPost("/pages/{pageId:guid}/blocks", async Task<Results<Created<BlockDto>, NotFound, BadRequest<string>>> (
                Guid pageId, BlockInput input, ApplicationContext db) =>
            {
                if (!await db.Pages.AnyAsync(p => p.Id == pageId)) return TypedResults.NotFound();
                if (input.Data.ValueKind is not (JsonValueKind.Object or JsonValueKind.Undefined))
                    return TypedResults.BadRequest("Block data must be a JSON object.");

                int order = input.Order ?? ((await db.Blocks.Where(b => b.PageId == pageId)
                    .Select(b => (int?)b.Order).MaxAsync() ?? -1) + 1);
                var block = new Block
                {
                    Id = Guid.NewGuid(),
                    PageId = pageId,
                    Order = order,
                    Type = input.Type,
                    Data = input.Data.ValueKind == JsonValueKind.Object ? input.Data.GetRawText() : "{}",
                };
                db.Blocks.Add(block);
                await db.SaveChangesAsync();
                return TypedResults.Created($"/api/blocks/{block.Id}", ToBlockDto(block));
            });

            api.MapPut("/blocks/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<string>>> (
                Guid id, BlockInput input, ApplicationContext db) =>
            {
                var block = await db.Blocks.FirstOrDefaultAsync(b => b.Id == id);
                if (block is null) return TypedResults.NotFound();
                if (input.Data.ValueKind is not (JsonValueKind.Object or JsonValueKind.Undefined))
                    return TypedResults.BadRequest("Block data must be a JSON object.");
                block.Type = input.Type;
                if (input.Data.ValueKind == JsonValueKind.Object) block.Data = input.Data.GetRawText();
                if (input.Order is int o) block.Order = o;
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            api.MapDelete("/blocks/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
            {
                var block = await db.Blocks.FirstOrDefaultAsync(b => b.Id == id);
                if (block is null) return TypedResults.NotFound();
                db.Blocks.Remove(block);
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            // reorder: body = ordered block ids; Order set to the list index
            api.MapPut("/pages/{pageId:guid}/blocks/order", async Task<Results<NoContent, NotFound, BadRequest<string>>> (
                Guid pageId, BlockOrderInput input, ApplicationContext db) =>
            {
                var blocks = await db.Blocks.Where(b => b.PageId == pageId).ToListAsync();
                if (blocks.Count == 0 && !await db.Pages.AnyAsync(p => p.Id == pageId))
                    return TypedResults.NotFound();
                var byId = blocks.ToDictionary(b => b.Id);
                if (input.BlockIds.Count != blocks.Count || input.BlockIds.Any(bid => !byId.ContainsKey(bid)))
                    return TypedResults.BadRequest("blockIds must list exactly the page's block ids.");
                for (int i = 0; i < input.BlockIds.Count; i++)
                    byId[input.BlockIds[i]].Order = i;
                await db.SaveChangesAsync();
                return TypedResults.NoContent();
            });

            return app;
        }

        /// <summary>WEB-3: Content-Type der Vorschau-Dateien (Renderer erzeugt HTML + CSS).</summary>
        private static string PreviewContentType(string path) =>
            path.EndsWith(".css", StringComparison.OrdinalIgnoreCase) ? "text/css; charset=utf-8"
            : path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) ? "application/xml; charset=utf-8"
            : path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) ? "text/plain; charset=utf-8"
            : "text/html; charset=utf-8";

        private static BlockDto ToBlockDto(Block b) =>
            new(b.Id, b.Order, b.Type, JsonNode.Parse(string.IsNullOrWhiteSpace(b.Data) ? "{}" : b.Data));

        private static PageDto ToPageDto(Page p) => new(
            p.Id, p.Slug, p.Title, p.SeoDescription, p.Status,
            p.Blocks.OrderBy(b => b.Order).Select(ToBlockDto).ToList());
    }
}
