using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Domain.Enums;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models;
using Microsoft.EntityFrameworkCore;

namespace FlatRender.ContentSvc.Application.Services;

/// <summary>
/// Imports a scanned After Effects project structure (scenes / frl_ frd_ elements /
/// frd_ &amp; frshare colours) into a project. Supports a dry-run <see cref="PreviewAsync"/>
/// that reports the diff without writing, and an <see cref="ApplyAsync"/> that merges:
/// matched items (by key) are refreshed, new items added, manual edits preserved.
/// </summary>
public class AepImportService(ContentDbContext db)
{
    public Task<ImportDiff> PreviewAsync(Guid projectId, ScanResult scan) => RunAsync(projectId, scan, null);

    public Task<ImportDiff> ApplyAsync(Guid projectId, ScanResult scan, ScanApplyOptions options) =>
        RunAsync(projectId, scan, options);

    private async Task<ImportDiff> RunAsync(Guid projectId, ScanResult scan, ScanApplyOptions? apply)
    {
        // Load ALL scenes incl. soft-deleted: the UNIQUE(project_id,key) constraint
        // covers deleted rows too, so a re-scan must REVIVE a soft-deleted match
        // rather than insert a duplicate (which would violate the constraint).
        // IgnoreQueryFilters() is REQUIRED — Scene has a global DeletedAt==null filter
        // that would otherwise hide the soft-deleted rows we need to revive.
        var existing = await db.Scenes
            .IgnoreQueryFilters()
            .Where(s => s.ProjectId == projectId)
            .Include(s => s.ContentElements)
            .Include(s => s.ColorElements)
            .ToListAsync();
        var existingShared = await db.SharedColors.Where(c => c.ProjectId == projectId).ToListAsync();

        var scanScenes = scan.Scenes ?? new List<ScanScene>();
        var byKey = existing.ToDictionary(s => s.Key, StringComparer.Ordinal);
        var scanKeys = new HashSet<string>(scanScenes.Select(s => s.Key), StringComparer.Ordinal);

        int sAdded = 0, sChanged = 0, sUnchanged = 0;
        var diffs = new List<SceneDiff>();
        int sortBase = existing.Count;

        foreach (var ss in scanScenes)
        {
            byKey.TryGetValue(ss.Key, out var scene);
            bool isNew = scene is null;

            var exElems = scene?.ContentElements.ToList() ?? new List<SceneContentElement>();
            var exColors = scene?.ColorElements.ToList() ?? new List<SceneColorElement>();
            var scElems = ss.Elements ?? new List<ScanElement>();
            var scColors = ss.Colors ?? new List<ScanColor>();

            var exElemByKey = exElems.ToDictionary(e => e.Key, StringComparer.Ordinal);
            var exColorByKey = exColors.ToDictionary(c => c.ElementKey, StringComparer.Ordinal);
            var scElemKeys = new HashSet<string>(scElems.Select(e => e.Key), StringComparer.Ordinal);
            var scColorKeys = new HashSet<string>(scColors.Select(c => c.ElementKey), StringComparer.Ordinal);

            int eAdded = scElems.Count(e => !exElemByKey.ContainsKey(e.Key));
            int eChanged = scElems.Count(e => exElemByKey.TryGetValue(e.Key, out var ex) && ElementDiffers(e, ex));
            int eRemoved = exElems.Count(e => !scElemKeys.Contains(e.Key));
            int cAdded = scColors.Count(c => !exColorByKey.ContainsKey(c.ElementKey));
            int cChanged = scColors.Count(c => exColorByKey.TryGetValue(c.ElementKey, out var ex) && ColorDiffers(c, ex));
            int cRemoved = exColors.Count(c => !scColorKeys.Contains(c.ElementKey));

            string status;
            if (isNew) { status = "added"; sAdded++; }
            else if (eAdded + eChanged + eRemoved + cAdded + cChanged + cRemoved > 0 || SceneHeaderDiffers(ss, scene!))
            { status = "changed"; sChanged++; }
            else { status = "unchanged"; sUnchanged++; }

            diffs.Add(new SceneDiff(ss.Key, ss.Title ?? ss.Key, status,
                eAdded, eChanged, eRemoved, cAdded, cChanged, cRemoved));

            if (apply is not null)
            {
                if (isNew)
                {
                    scene = new Scene { ProjectId = projectId, Key = ss.Key, Sort = ss.Sort ?? sortBase++ };
                    db.Scenes.Add(scene);
                }
                else if (scene!.DeletedAt is not null)
                {
                    scene.DeletedAt = null; // revive a soft-deleted scene the scan re-discovered
                }
                ApplySceneHeader(scene!, ss, isNew, apply);
                ApplyElements(scene!, scElems, exElemByKey, scElemKeys, apply);
                ApplyColors(scene!, scColors, exColorByKey, scColorKeys, apply);
            }
        }

        // Orphans = currently-active scenes the scan no longer contains (ignore
        // already soft-deleted rows so they don't inflate the diff).
        var orphans = existing.Where(s => s.DeletedAt == null && !scanKeys.Contains(s.Key)).ToList();
        if (apply is not null && apply.RemoveOrphanScenes)
            foreach (var s in orphans) s.DeletedAt = DateTime.UtcNow;

        var (shAdded, shChanged, shRemoved) = MergeShared(projectId, scan.SharedColors ?? new List<ScanColor>(), existingShared, apply);

        if (apply is not null) await db.SaveChangesAsync();

        return new ImportDiff(
            apply is not null,
            sAdded, sChanged, sUnchanged, orphans.Count,
            shAdded, shChanged, shRemoved,
            diffs, orphans.Select(o => o.Key).ToList());
    }

    // ── scene header ─────────────────────────────────────────────────────────
    private static bool SceneHeaderDiffers(ScanScene ss, Scene e) =>
        (ss.SceneType is not null && ParseScene(ss.SceneType) != e.SceneType)
        || (ss.DefaultDurationSec is not null && ss.DefaultDurationSec != e.DefaultDurationSec)
        || (ss.Title is not null && ss.Title != e.Title);

    private static void ApplySceneHeader(Scene scene, ScanScene ss, bool isNew, ScanApplyOptions o)
    {
        if (isNew || o.OverwriteExisting)
        {
            scene.Title = ss.Title ?? scene.Title ?? ss.Key;
            if (ss.SceneType is not null) scene.SceneType = ParseScene(ss.SceneType);
            if (ss.DefaultDurationSec is not null) scene.DefaultDurationSec = ss.DefaultDurationSec;
            if (ss.MinDurationSec is not null) scene.MinDurationSec = ss.MinDurationSec;
            if (ss.MaxDurationSec is not null) scene.MaxDurationSec = ss.MaxDurationSec;
            if (ss.Sort is not null) scene.Sort = ss.Sort.Value;
            scene.UpdatedAt = DateTime.UtcNow;
        }
        if (isNew && string.IsNullOrEmpty(scene.Title)) scene.Title = ss.Key;
    }

    // ── content elements ──────────────────────────────────────────────────────
    private static bool ElementDiffers(ScanElement s, SceneContentElement e) =>
        (s.Type is not null && ParseElemType(s.Type) != e.Type)
        || (s.DefaultValue is not null && s.DefaultValue != e.DefaultValue)
        || (s.FontFace is not null && s.FontFace != e.FontFace)
        || (s.FontSize is not null && s.FontSize != e.FontSize)
        || (s.Justify is not null && ParseJustify(s.Justify) != e.Justify)
        || (s.IsHidden is not null && s.IsHidden != e.IsHidden);

    private void ApplyElements(Scene scene, List<ScanElement> scElems,
        Dictionary<string, SceneContentElement> exByKey, HashSet<string> scKeys, ScanApplyOptions o)
    {
        int sort = 0;
        foreach (var se in scElems)
        {
            if (exByKey.TryGetValue(se.Key, out var e))
            {
                if (o.OverwriteExisting) WriteElement(e, se, scene.Id, sort);
            }
            else
            {
                var ne = new SceneContentElement { SceneId = scene.Id, Key = se.Key };
                WriteElement(ne, se, scene.Id, sort);
                db.SceneContentElements.Add(ne);
            }
            sort++;
        }
        if (o.RemoveOrphanElements)
            foreach (var e in exByKey.Values.Where(e => !scKeys.Contains(e.Key)))
                db.SceneContentElements.Remove(e);
    }

    private static void WriteElement(SceneContentElement e, ScanElement s, Guid sceneId, int sort)
    {
        e.SceneId = sceneId;
        e.Title = s.Title ?? (string.IsNullOrEmpty(e.Title) ? s.Key : e.Title);
        if (s.Type is not null) e.Type = ParseElemType(s.Type);
        if (s.DefaultValue is not null) e.DefaultValue = s.DefaultValue;
        if (s.FontFace is not null) { e.FontFace = s.FontFace; e.DefaultFontFace = s.FontFace; }
        if (s.FontFaceName is not null) e.FontFaceName = s.FontFaceName;
        if (s.FontSize is not null) { e.FontSize = s.FontSize; e.DefaultFontSize = s.FontSize; }
        if (s.Justify is not null) e.Justify = ParseJustify(s.Justify);
        if (s.PositionInContainer is not null) e.PositionInContainer = s.PositionInContainer.Value;
        if (s.IsTextBox is not null) e.IsTextBox = s.IsTextBox.Value;
        if (s.MaxSize is not null) e.MaxSize = s.MaxSize;
        if (s.VideoSupport is not null) e.VideoSupport = s.VideoSupport.Value;
        if (s.Width is not null) e.Width = s.Width;
        if (s.Height is not null) e.Height = s.Height;
        if (s.IsHidden is not null) e.IsHidden = s.IsHidden.Value;
        if (s.DirectionLayerKey is not null) e.DirectionLayerKey = s.DirectionLayerKey;
        e.Sort = s.Sort ?? sort;
        e.UpdatedAt = DateTime.UtcNow;
    }

    // ── colour elements (per scene) ───────────────────────────────────────────
    private static bool ColorDiffers(ScanColor s, SceneColorElement e) =>
        (s.DefaultColor is not null && !ColorEq(s.DefaultColor, e.DefaultColor))
        || (s.AttrValue is not null && ParseAttr(s.AttrValue) != e.AttrValue)
        || (s.Title is not null && s.Title != e.Title);

    private void ApplyColors(Scene scene, List<ScanColor> scColors,
        Dictionary<string, SceneColorElement> exByKey, HashSet<string> scKeys, ScanApplyOptions o)
    {
        int sort = 0;
        foreach (var sc in scColors)
        {
            if (exByKey.TryGetValue(sc.ElementKey, out var e))
            {
                if (o.OverwriteExisting) WriteColor(e, sc, scene.Id, sort);
            }
            else
            {
                var ne = new SceneColorElement { SceneId = scene.Id, ElementKey = sc.ElementKey, DefaultColor = sc.DefaultColor ?? "#000000" };
                WriteColor(ne, sc, scene.Id, sort);
                db.SceneColorElements.Add(ne);
            }
            sort++;
        }
        if (o.RemoveOrphanElements)
            foreach (var e in exByKey.Values.Where(e => !scKeys.Contains(e.ElementKey)))
                db.SceneColorElements.Remove(e);
    }

    private static void WriteColor(SceneColorElement e, ScanColor s, Guid sceneId, int sort)
    {
        e.SceneId = sceneId;
        e.Title = s.Title ?? (string.IsNullOrEmpty(e.Title) ? s.ElementKey : e.Title);
        if (s.AttrValue is not null) e.AttrValue = ParseAttr(s.AttrValue);
        if (s.DefaultColor is not null) e.DefaultColor = s.DefaultColor;
        e.Sort = s.Sort ?? sort;
        e.UpdatedAt = DateTime.UtcNow;
    }

    // ── shared colours (project-level) ────────────────────────────────────────
    private (int added, int changed, int removed) MergeShared(
        Guid projectId, List<ScanColor> scan, List<SharedColor> existing, ScanApplyOptions? apply)
    {
        var byKey = existing.ToDictionary(c => c.ElementKey, StringComparer.Ordinal);
        var scanKeys = new HashSet<string>(scan.Select(c => c.ElementKey), StringComparer.Ordinal);
        int added = 0, changed = 0, removed = 0, sort = 0;

        foreach (var sc in scan)
        {
            if (byKey.TryGetValue(sc.ElementKey, out var e))
            {
                if (ColorDiffersShared(sc, e)) changed++;
                if (apply is not null && apply.OverwriteExisting)
                {
                    e.Title = sc.Title ?? e.Title;
                    if (sc.AttrValue is not null) e.AttrValue = ParseAttr(sc.AttrValue);
                    if (sc.DefaultColor is not null) e.DefaultColor = sc.DefaultColor;
                    e.Sort = sc.Sort ?? e.Sort;
                    e.UpdatedAt = DateTime.UtcNow;
                }
            }
            else
            {
                added++;
                if (apply is not null)
                    db.SharedColors.Add(new SharedColor
                    {
                        ProjectId = projectId, ElementKey = sc.ElementKey, Title = sc.Title ?? sc.ElementKey,
                        AttrValue = ParseAttr(sc.AttrValue), DefaultColor = sc.DefaultColor ?? "#000000", Sort = sc.Sort ?? sort,
                    });
            }
            sort++;
        }

        var orphans = existing.Where(c => !scanKeys.Contains(c.ElementKey)).ToList();
        removed = orphans.Count;
        if (apply is not null && apply.RemoveOrphanElements)
            foreach (var c in orphans) db.SharedColors.Remove(c);

        return (added, changed, removed);
    }

    private static bool ColorDiffersShared(ScanColor s, SharedColor e) =>
        (s.DefaultColor is not null && !ColorEq(s.DefaultColor, e.DefaultColor))
        || (s.AttrValue is not null && ParseAttr(s.AttrValue) != e.AttrValue)
        || (s.Title is not null && s.Title != e.Title);

    // ── parsing helpers ───────────────────────────────────────────────────────
    private static bool ColorEq(string? a, string? b) =>
        string.Equals((a ?? "").Trim().TrimStart('#'), (b ?? "").Trim().TrimStart('#'), StringComparison.OrdinalIgnoreCase);

    private static SceneKind ParseScene(string? v) =>
        Enum.TryParse<SceneKind>(v, true, out var k) ? k : SceneKind.Normal;

    private static ContentElementType ParseElemType(string? v) =>
        Enum.TryParse<ContentElementType>(v, true, out var t) ? t : ContentElementType.Text;

    private static JustifyKind ParseJustify(string? v) =>
        Enum.TryParse<JustifyKind>(v, true, out var j) ? j : JustifyKind.CENTER_JUSTIFY;

    private static AttrValueKind ParseAttr(string? v) =>
        Enum.TryParse<AttrValueKind>(v, true, out var a) ? a : AttrValueKind.fill;
}
