using System.Text.Json;
using System.Text.Json.Serialization;

namespace JobTrackerApi.Models;

// The "lens" a CvVariant stores. Deliberately all-optional + defaulted so an empty settings object
// renders a sensible default CV straight from the master profile. Overrides are keyed by the master
// profile's stable ItemKeys so career data is referenced, never copied.
public sealed class CvVariantSettings
{
    public string ThemeId { get; set; } = "modern";

    // Theme overrides (null => use the theme's own value).
    public string? AccentColor { get; set; }
    public string? HeadingFont { get; set; }
    public string? BodyFont { get; set; }
    public string? Density { get; set; }      // compact | balanced | roomy
    public string? PageSize { get; set; }     // a4 | letter
    public string? DateFormat { get; set; }   // display hint for the frontend; renderer uses source strings
    public string? Language { get; set; }
    public string? Headline { get; set; }     // override the contact headline for this variant

    // Normalized design overrides. Null keeps the selected theme's value, which means old variants
    // retain their exact appearance while new editor controls can be added without a schema change.
    public string? TextColor { get; set; }
    public string? MutedColor { get; set; }
    public string? HeadingColor { get; set; }
    public string? BackgroundColor { get; set; }
    public double? BaseFontSizePt { get; set; }
    public double? HeadingSizePt { get; set; }
    public double? LineHeight { get; set; }
    public double? PageMarginMm { get; set; }
    public double? SectionGapMm { get; set; }
    public double? EntryGapMm { get; set; }
    public string? HeadingStyle { get; set; } // caps-rule | underline | plain | bar
    public string? HeaderStyle { get; set; }  // plain | band | centered | kicker
    public string? SkillsStyle { get; set; }  // tags | text | bullets | grouped
    public List<CvSkillGroupSetting>? SkillGroups { get; set; }
    public string? Layout { get; set; }       // single | sidebar-left | sidebar-right | header-band
    public double? SidebarWidthMm { get; set; }
    public List<string>? SidebarSections { get; set; }

    public bool ShowPhoto { get; set; }
    public bool ShowPageNumbers { get; set; }
    public bool ShowIcons { get; set; } = true;

    // Section order + visibility + optional renamed heading. Empty => default order, all visible.
    public List<CvSectionSetting> Sections { get; set; } = new();

    // Per-item overrides keyed by ItemKey (job/education/project/certification stable id).
    public Dictionary<string, CvItemOverride> Overrides { get; set; } = new();

    // Extra sections unique to this variant (not in the master profile).
    public List<CvCustomSectionSetting> CustomSections { get; set; } = new();
}

public sealed class CvSectionSetting
{
    public string Key { get; set; } = string.Empty; // summary|skills|experience|education|projects|certifications|languages|interests|links|custom:<k>
    public bool Hidden { get; set; }
    public string? Title { get; set; } // renamed heading
    // Explicit entry order for this section, as a list of item ItemKeys. Entries not listed keep their
    // master order and come last. Null => master profile order. Lets a variant reorder entries without
    // touching the master profile.
    public List<string>? ItemOrder { get; set; }
    // Optional CV-specific content for bullet/tag sections. Null uses master data; an empty list is
    // an intentional empty override. Entry sections continue to use stable per-item overrides.
    public List<string>? Items { get; set; }
    // Optional presentation hint consumed by the shared renderer, so preview and PDF always agree.
    // Unsupported combinations safely fall back to the normal row layout.
    public string? Presentation { get; set; } // rows | grid | compact | bubble
    public int? Columns { get; set; }         // 1 | 2
}

public sealed class CvItemOverride
{
    public bool Hidden { get; set; }
    public string? Title { get; set; }
    public string? Subtitle { get; set; }
    public List<string>? Bullets { get; set; } // null => use master bullets; non-null => replace for this variant only
}

public sealed class CvCustomSectionSetting
{
    public string Key { get; set; } = string.Empty;
    public string? Title { get; set; }
    public List<string> Items { get; set; } = new();
    public bool Hidden { get; set; }
    public string? PresetKey { get; set; }
    public string? ContentType { get; set; } // paragraphs | bullets | entries
}

public sealed class CvSkillGroupSetting
{
    public string Name { get; set; } = string.Empty;
    public List<string> Items { get; set; } = new();
}

public static class CvVariantSettingsJson
{
    private static readonly HashSet<string> AllowedFonts = new(StringComparer.Ordinal)
    {
        "'Segoe UI', Roboto, Arial, sans-serif",
        "Arial, Helvetica, sans-serif",
        "Georgia, 'Times New Roman', serif",
        "'Helvetica Neue', Arial, sans-serif",
        "'Roboto', Arial, sans-serif",
        "'Poppins', 'Segoe UI', Arial, sans-serif",
    };

    private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

    public static string Serialize(CvVariantSettings? settings) =>
        JsonSerializer.Serialize(Normalize(settings), Options);

    public static CvVariantSettings Deserialize(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return new CvVariantSettings();
        try { return Normalize(JsonSerializer.Deserialize<CvVariantSettings>(json, Options)); }
        catch { return new CvVariantSettings(); }
    }

    public static CvVariantSettings Normalize(CvVariantSettings? s)
    {
        s ??= new CvVariantSettings();
        s.ThemeId = string.IsNullOrWhiteSpace(s.ThemeId) ? "modern" : s.ThemeId.Trim().ToLowerInvariant();
        s.AccentColor = NormalizeColor(s.AccentColor);
        s.TextColor = NormalizeColor(s.TextColor);
        s.MutedColor = NormalizeColor(s.MutedColor);
        s.HeadingColor = NormalizeColor(s.HeadingColor);
        s.BackgroundColor = NormalizeColor(s.BackgroundColor);
        s.HeadingFont = NormalizeFont(s.HeadingFont);
        s.BodyFont = NormalizeFont(s.BodyFont);
        s.BaseFontSizePt = Clamp(s.BaseFontSizePt, 7, 13);
        s.HeadingSizePt = Clamp(s.HeadingSizePt, 9, 20);
        s.LineHeight = Clamp(s.LineHeight, 1.1, 1.8);
        s.PageMarginMm = Clamp(s.PageMarginMm, 8, 28);
        s.SectionGapMm = Clamp(s.SectionGapMm, 2, 14);
        s.EntryGapMm = Clamp(s.EntryGapMm, 1, 10);
        s.SidebarWidthMm = Clamp(s.SidebarWidthMm, 45, 85);
        s.HeadingStyle = NormalizeChoice(s.HeadingStyle, "caps-rule", "underline", "plain", "bar");
        s.HeaderStyle = NormalizeChoice(s.HeaderStyle, "plain", "band", "centered", "kicker");
        s.SkillsStyle = NormalizeChoice(s.SkillsStyle, "tags", "text", "bullets", "grouped");
        s.Layout = NormalizeChoice(s.Layout, "single", "sidebar-left", "sidebar-right", "header-band");
        s.DateFormat = NormalizeChoice(s.DateFormat, "long", "short", "numeric", "year");
        if (!string.IsNullOrWhiteSpace(s.Language))
        {
            var language = s.Language.Trim().ToLowerInvariant();
            s.Language = language[..Math.Min(12, language.Length)];
        }
        else s.Language = null;
        s.SidebarSections = s.SidebarSections?
            .Where(key => !string.IsNullOrWhiteSpace(key))
            .Select(key => key.Trim().ToLowerInvariant())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Take(20)
            .ToList();
        s.Sections ??= new();
        s.Overrides ??= new();
        s.CustomSections ??= new();
        s.SkillGroups = s.SkillGroups?
            .Where(group => !string.IsNullOrWhiteSpace(group.Name) || group.Items.Any(item => !string.IsNullOrWhiteSpace(item)))
            .Take(30)
            .Select(group => new CvSkillGroupSetting
            {
                Name = (group.Name ?? string.Empty).Trim(),
                Items = group.Items.Where(item => !string.IsNullOrWhiteSpace(item)).Select(item => item.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).Take(100).ToList(),
            })
            .ToList();
        foreach (var section in s.Sections)
        {
            section.Presentation = NormalizeChoice(section.Presentation, "rows", "grid", "compact", "bubble");
            section.Columns = section.Columns is 1 or 2 ? section.Columns : null;
            section.ItemOrder = section.ItemOrder?
                .Where(key => !string.IsNullOrWhiteSpace(key))
                .Select(key => key.Trim())
                .Distinct(StringComparer.Ordinal)
                .Take(500)
                .ToList();
        }
        foreach (var section in s.CustomSections)
        {
            section.ContentType = NormalizeChoice(section.ContentType, "paragraphs", "bullets", "entries") ?? "bullets";
            section.PresetKey = string.IsNullOrWhiteSpace(section.PresetKey) ? null : section.PresetKey.Trim().ToLowerInvariant();
            section.Items ??= new();
        }
        return s;
    }

    private static string? NormalizeColor(string? value)
    {
        var candidate = value?.Trim();
        return candidate is { Length: 7 }
            && candidate[0] == '#'
            && candidate.Skip(1).All(Uri.IsHexDigit)
                ? candidate.ToLowerInvariant()
                : null;
    }

    private static string? NormalizeFont(string? value)
    {
        var candidate = value?.Trim();
        return candidate is not null && AllowedFonts.Contains(candidate) ? candidate : null;
    }

    private static double? Clamp(double? value, double min, double max) =>
        value is null || double.IsNaN(value.Value) || double.IsInfinity(value.Value)
            ? null
            : Math.Clamp(value.Value, min, max);

    private static string? NormalizeChoice(string? value, params string[] choices)
    {
        var candidate = value?.Trim().ToLowerInvariant();
        return candidate is not null && choices.Contains(candidate, StringComparer.Ordinal) ? candidate : null;
    }
}
