using System.Globalization;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using JobTrackerApi.Models;

namespace JobTrackerApi.Services;

public sealed record ThemedCvRenderResult(string ThemeId, string SuggestedFileName, string Html);

public interface IThemedCvRenderer
{
    ThemedCvRenderResult Render(CvRenderModel model, CvTheme theme, CvVariantSettings settings);
}

// ONE render path for every theme. The theme (data) drives layout, palette, typography and spacing;
// the variant settings apply a few runtime overrides (accent, fonts, density, page size, photo,
// icons, page numbers). Adding a theme never touches this file. docs/architecture/cv-theme-engine.md.
public sealed class ThemedCvRenderer : IThemedCvRenderer
{
    public ThemedCvRenderResult Render(CvRenderModel model, CvTheme theme, CvVariantSettings settings)
    {
        settings = CvVariantSettingsJson.Normalize(settings);
        var accent = Override(settings.AccentColor, theme.Accent);
        var headerInk = ContrastInk(accent);
        var ink = Override(settings.TextColor, theme.Ink);
        var muted = Override(settings.MutedColor, theme.Muted);
        var paper = Override(settings.BackgroundColor, theme.Paper);
        var headingColor = Override(settings.HeadingColor, theme.HeadingColor ?? accent);
        var headingFont = Override(settings.HeadingFont, theme.HeadingFont);
        var bodyFont = Override(settings.BodyFont, theme.BodyFont);
        var density = DensityScale(settings.Density);
        var layout = Override(settings.Layout, theme.Layout);
        var headingStyle = Override(settings.HeadingStyle, theme.HeadingStyle);
        var headerStyle = Override(settings.HeaderStyle, theme.HeaderStyle);
        var sidebarSections = settings.SidebarSections ?? theme.SidebarSections;
        var pageSize = string.Equals(Override(settings.PageSize, "a4"), "letter", StringComparison.OrdinalIgnoreCase) ? "Letter" : "A4";
        var pageDims = pageSize == "Letter" ? ("215.9mm", "279.4mm") : ("210mm", "297mm");
        var showIcons = settings.ShowIcons && theme.DefaultIcons;

        var twoColumn = layout is "sidebar-left" or "sidebar-right";
        var (sidebarHtml, mainHtml) = twoColumn
            ? SplitColumns(model, theme, sidebarSections, settings)
            : (string.Empty, RenderSections(model.Sections, theme, settings));

        var css = BuildCss(theme, settings, accent, headerInk, ink, muted, paper, headingColor, headingFont, bodyFont, headingStyle, density, pageDims, layout, twoColumn);
        var header = RenderHeader(model, theme, showIcons, twoColumn, headerStyle);
        var body = layout switch
        {
            "sidebar-left" => $@"<div class=""cols"">{Sidebar(model, sidebarHtml, theme, showIcons, sidebarSections, headerStyle)}<section class=""main"">{mainHtml}</section></div>",
            "sidebar-right" => $@"<div class=""cols""><section class=""main"">{mainHtml}</section>{Sidebar(model, sidebarHtml, theme, showIcons, sidebarSections, headerStyle)}</div>",
            _ => $@"{header}<section class=""main"">{mainHtml}</section>",
        };
        var html = $@"<!DOCTYPE html>
<html lang=""{Attr(settings.Language ?? "en")}"">
<head>
<meta charset=""utf-8"" />
<title>{Enc(model.FullName)} — {Enc(theme.Name)}</title>
<style>{css}</style>
</head>
<body>
<main class=""page"">{body}</main>
</body>
</html>";

        var fileName = Slug($"{model.FullName}-{theme.Id}") + ".pdf";
        return new ThemedCvRenderResult(theme.Id, fileName, html);
    }

    private static (string sidebar, string main) SplitColumns(CvRenderModel model, CvTheme theme, IReadOnlyCollection<string> sidebarKeys, CvVariantSettings settings)
    {
        var sidebar = new StringBuilder();
        var mainSections = new List<CvRenderSection>();
        foreach (var section in model.Sections)
        {
            if (sidebarKeys.Contains(section.Key, StringComparer.OrdinalIgnoreCase))
                sidebar.Append(RenderSection(section, theme, settings));
            else
                mainSections.Add(section);
        }
        return (sidebar.ToString(), RenderSections(mainSections, theme, settings));
    }

    private static string Sidebar(CvRenderModel model, string sectionsHtml, CvTheme theme, bool showIcons, IReadOnlyCollection<string> sidebarSections, string headerStyle)
    {
        var header = RenderHeader(model, theme, showIcons, twoColumn: true, headerStyle);
        var contact = sidebarSections.Contains("contact", StringComparer.OrdinalIgnoreCase)
            ? RenderContactBlock(model.Contact, showIcons, sidebar: true)
            : string.Empty;
        return $@"<aside class=""sidebar"">{header}{contact}{sectionsHtml}</aside>";
    }

    private static string RenderHeader(CvRenderModel model, CvTheme theme, bool showIcons, bool twoColumn, string headerStyle)
    {
        var photo = RenderPhoto(model.PhotoDataUrl, theme.PhotoShape);
        var kicker = headerStyle == "kicker" ? @"<div class=""kicker"">Curriculum Vitae</div>" : string.Empty;
        var name = $@"<h1 class=""name"">{Enc(model.FullName)}</h1>";
        var headline = string.IsNullOrWhiteSpace(model.Headline) ? string.Empty : $@"<div class=""headline"">{Enc(model.Headline)}</div>";
        var contact = twoColumn ? string.Empty : RenderContactBlock(model.Contact, showIcons, sidebar: false);
        var headerClass = twoColumn ? "hero" : $"header header-{headerStyle}";
        return $@"<header class=""{headerClass}"">{photo}<div class=""head-text"">{kicker}{name}{headline}{contact}</div></header>";
    }

    private static string RenderPhoto(string? dataUrl, string shape)
    {
        if (shape == "none" || string.IsNullOrWhiteSpace(dataUrl)) return string.Empty;
        return $@"<div class=""photo photo-{shape}""><img src=""{Attr(dataUrl)}"" alt=""Profile photo"" /></div>";
    }

    private static string RenderContactBlock(List<CvContactItem> contact, bool showIcons, bool sidebar)
    {
        if (contact.Count == 0) return string.Empty;
        var items = new StringBuilder();
        foreach (var c in contact)
        {
            var icon = showIcons ? Icon(c.Icon) : string.Empty;
            var text = c.Href is null ? Enc(c.Value) : $@"<a href=""{Attr(c.Href)}"">{Enc(c.Value)}</a>";
            items.Append($@"<span class=""contact-item"">{icon}{text}</span>");
        }
        return $@"<div class=""contact {(sidebar ? "contact-stacked" : "contact-inline")}"">{items}</div>";
    }

    private static string RenderSections(IEnumerable<CvRenderSection> sections, CvTheme theme, CvVariantSettings settings)
    {
        var sb = new StringBuilder();
        foreach (var section in sections) sb.Append(RenderSection(section, theme, settings));
        return sb.ToString();
    }

    private static string RenderSection(CvRenderSection section, CvTheme theme, CvVariantSettings? settings = null)
    {
        if (section.IsEmpty) return string.Empty;
        var inner = section.Kind switch
        {
            "bullets" => $@"<ul class=""bullets"">{Items(section.Bullets)}</ul>",
            "paragraphs" => $@"<div class=""paragraphs"">{string.Join("", section.Bullets.Select(item => $@"<p>{Inline(item)}</p>"))}</div>",
            "skill-groups" => $@"<div class=""skill-groups"">{string.Join("", section.SkillGroups.Select(RenderSkillGroup))}</div>",
            "tags" when section.Key == "skills" && settings?.SkillsStyle == "bullets" => $@"<ul class=""bullets skill-bullets"">{Items(section.Tags)}</ul>",
            "tags" when section.Key == "skills" && settings?.SkillsStyle == "text" => $@"<p class=""skills-text"">{string.Join(" · ", section.Tags.Select(Enc))}</p>",
            "tags" => $@"<ul class=""tags"">{string.Join("", section.Tags.Select(t => $@"<li class=""tag"">{Enc(t)}</li>"))}</ul>",
            _ => string.Join("", section.Entries.Select(RenderEntry)),
        };
        var flowClass = IsFlowingSection(section) ? " section-flow" : string.Empty;
        var presentation = section.Presentation is "grid" or "compact" or "bubble" ? section.Presentation : "rows";
        var columns = presentation == "grid" && section.Columns == 2 ? 2 : 1;
        return $@"<section class=""section section-{Attr(section.Key)}{flowClass} section-presentation-{presentation}"" style=""--cv-section-columns:{columns}""><h2 class=""section-title""><span class=""section-title-text"">{Enc(section.Title)}</span></h2>{inner}</section>";
    }

    private static string RenderSkillGroup(CvRenderSkillGroup group)
    {
        var name = string.IsNullOrWhiteSpace(group.Name) ? string.Empty : $@"<div class=""skill-group-name"">{Enc(group.Name)}</div>";
        return $@"<div class=""skill-group"">{name}<ul class=""skill-group-items"">{string.Join("", group.Items.Select(item => $@"<li>{Enc(item)}</li>"))}</ul></div>";
    }

    private static string RenderEntry(CvRenderEntry entry)
    {
        var sb = new StringBuilder();
        sb.Append(IsFlowingEntry(entry) ? @"<article class=""entry entry-flow"">" : @"<article class=""entry"">");
        var hasMeta = !string.IsNullOrWhiteSpace(entry.Meta);
        sb.Append(@"<div class=""entry-head"">");
        sb.Append($@"<div class=""entry-title"">{Enc(entry.Title)}</div>");
        if (hasMeta) sb.Append($@"<div class=""entry-meta"">{Enc(entry.Meta)}</div>");
        sb.Append("</div>");
        if (!string.IsNullOrWhiteSpace(entry.Subtitle)) sb.Append($@"<div class=""entry-subtitle"">{Enc(entry.Subtitle)}</div>");
        if (entry.Bullets.Count > 0) sb.Append($@"<ul class=""bullets"">{Items(entry.Bullets)}</ul>");
        if (entry.Tags.Count > 0) sb.Append($@"<ul class=""tags entry-tags"">{string.Join("", entry.Tags.Select(t => $@"<li class=""tag"">{Enc(t)}</li>"))}</ul>");
        sb.Append("</article>");
        return sb.ToString();
    }

    private static string Items(IEnumerable<string> items) =>
        string.Join("", items.Select(i => $"<li{(IsFlowingItem(i) ? " class=\"item-flow\"" : string.Empty)}>{Inline(i)}</li>"));

    // Short entries stay together. Large entries must be allowed to paginate between bullets or
    // paragraphs; forcing an entry taller than the printable area to remain whole clips content in
    // Chromium. The renderer only selects the pagination strategy—it never shrinks the text.
    private static bool IsFlowingEntry(CvRenderEntry entry)
    {
        var textLength = (entry.Title?.Length ?? 0)
            + (entry.Subtitle?.Length ?? 0)
            + (entry.Meta?.Length ?? 0)
            + entry.Bullets.Sum(item => item?.Length ?? 0)
            + entry.Tags.Sum(item => item?.Length ?? 0);
        return entry.Bullets.Count > 5 || entry.Bullets.Any(IsFlowingItem) || textLength > 900;
    }

    // A section containing several individually short entries can still exceed a printable page.
    // Mark it as flowing so Chromium may move/break individual entries instead of moving the whole
    // oversized section to a fresh page and leaving a large blank area behind.
    private static bool IsFlowingSection(CvRenderSection section)
    {
        if (section.Entries.Count > 2 || section.Entries.Any(IsFlowingEntry)) return true;
        if (section.SkillGroups.Count > 6) return true;
        if (section.Tags.Count > 30) return true;
        return section.Bullets.Count > 8 || section.Bullets.Sum(item => item?.Length ?? 0) > 1_000;
    }

    private static bool IsFlowingItem(string? item) => (item?.Length ?? 0) > 360;

    // Safe inline rich text for bullets/summary. HTML-escape EVERYTHING first (so any user markup is
    // inert), then re-introduce a tiny whitelist: **bold**, *italic*, __underline__, [text](url) with
    // http(s)/mailto only. Because escaping runs first, no user-supplied tag can survive — the only
    // tags in the output are the ones these patterns emit.
    private static readonly Regex LinkRx = new(@"\[([^\]]+)\]\((https?://[^\s)]+|mailto:[^\s)]+)\)", RegexOptions.Compiled);
    private static readonly Regex BoldRx = new(@"\*\*(.+?)\*\*", RegexOptions.Compiled);
    private static readonly Regex UnderlineRx = new(@"__(.+?)__", RegexOptions.Compiled);
    private static readonly Regex ItalicRx = new(@"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", RegexOptions.Compiled);

    private static string Inline(string? value)
    {
        var s = Enc(value);
        s = LinkRx.Replace(s, m => $"<a href=\"{m.Groups[2].Value}\">{m.Groups[1].Value}</a>");
        s = BoldRx.Replace(s, "<strong>$1</strong>");
        s = UnderlineRx.Replace(s, "<u>$1</u>");
        s = ItalicRx.Replace(s, "<em>$1</em>");
        return s;
    }

    private static string BuildCss(CvTheme t, CvVariantSettings settings, string accent, string headerInk, string ink, string muted, string paper, string headingColor, string headingFont, string bodyFont, string headingStyle, double density, (string w, string h) page, string layout, bool twoColumn)
    {
        var margin = F((settings.PageMarginMm ?? t.PageMarginMm) * density);
        var sectionGap = F((settings.SectionGapMm ?? t.SectionGapMm) * density);
        var entryGap = F((settings.EntryGapMm ?? t.EntryGapMm) * density);
        var compactEntryGap = F(Math.Max(1.5, (settings.EntryGapMm ?? t.EntryGapMm) * density * .55));
        var bodySize = settings.BaseFontSizePt ?? t.BodySizePt;
        var headingSize = settings.HeadingSizePt ?? t.HeadingSizePt;
        var lineHeight = settings.LineHeight ?? t.LineHeight;
        var sidebarWidth = settings.SidebarWidthMm ?? t.SidebarWidthMm;
        var headingCss = headingStyle switch
        {
            "underline" => ".section-title{border-bottom:1.5px solid var(--cv-accent-soft);padding-bottom:1.5mm;}",
            "plain" => ".section-title{letter-spacing:.01em;}",
            "bar" => $".section-title{{padding-left:2.5mm;border-left:3px solid {accent};}}",
            _ => $".section-title{{text-transform:uppercase;letter-spacing:.14em;font-size:{F(headingSize * 0.85)}pt;color:var(--cv-accent-color);border-bottom:1px solid var(--cv-accent-soft);padding-bottom:1.4mm;}}",
        };
        var columnTemplate = layout == "sidebar-right"
            ? $"minmax(0,1fr) {F(sidebarWidth)}mm"
            : $"{F(sidebarWidth)}mm minmax(0,1fr)";
        var layoutCss = twoColumn
            ? $@".cols{{display:grid;grid-template-columns:{columnTemplate};min-height:{page.h};}}
.sidebar{{background:var(--cv-accent-color);color:var(--cv-accent-ink);padding:{margin}mm;}}
.sidebar .section-title{{color:var(--cv-accent-ink);border-color:color-mix(in srgb,var(--cv-accent-ink) 38%,transparent);}}
.sidebar .tag{{border-color:color-mix(in srgb,var(--cv-accent-ink) 45%,transparent);}}
.sidebar .contact,.sidebar .headline,.sidebar .entry-meta,.sidebar .entry-subtitle{{color:var(--cv-accent-ink);}}
.sidebar a{{color:inherit;}}
.main{{padding:{margin}mm;}}
.hero .name{{color:var(--cv-accent-ink);}}"
            : $@".main{{padding:0 {margin}mm {margin}mm {margin}mm;}}
.header{{padding:{margin}mm {margin}mm {F(t.SectionGapMm * density)}mm {margin}mm;display:flex;gap:6mm;align-items:center;}}
.header-band{{background:{accent};color:{headerInk};}}
.header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{{color:{headerInk};}}
.header-centered{{flex-direction:column;text-align:center;justify-content:center;}}
.header-centered .contact{{justify-content:center;}}
.header-plain{{border-bottom:2px solid {accent};}}";

        var numberedSections = t.NumberSections
            ? $@".main{{counter-reset:cv-section;}} .main>.section>.section-title{{counter-increment:cv-section;font-family:{t.UtilityFont};font-weight:600;}} .main>.section>.section-title::before{{content:counter(cv-section,decimal-leading-zero) ' — ';color:var(--cv-accent-color);}}"
            : string.Empty;

        return $@"
*{{box-sizing:border-box;}}
:root{{--cv-accent-color:{accent};--cv-accent-ink:{headerInk};--cv-accent-soft:color-mix(in srgb,{accent} 28%,transparent);--cv-ink:{ink};--cv-muted:{muted};--cv-paper:{paper};--cv-page-margin:{margin}mm;}}
html,body{{min-width:0;}}
body{{margin:0;background:#e9edf2;color:var(--cv-ink);font-family:{bodyFont};font-size:{F(bodySize)}pt;line-height:{F(lineHeight)};-webkit-print-color-adjust:exact;print-color-adjust:exact;}}
.page{{width:{page.w};min-height:{page.h};margin:0 auto;background:var(--cv-paper);overflow:visible;overflow-wrap:anywhere;word-break:normal;}}
h1,h2{{font-family:{headingFont};}}
.name{{margin:0;font-size:{F(t.NameSizePt)}pt;color:{ink};line-height:1.1;overflow-wrap:anywhere;}}
.kicker{{text-transform:uppercase;letter-spacing:.3em;font-size:7.5pt;color:{accent};margin-bottom:1.5mm;}}
.headline{{margin-top:1.5mm;color:{muted};font-size:{F(bodySize + 0.5)}pt;}}
.head-text,.main,.sidebar,.cols>*{{min-width:0;}}
.head-text{{flex:1;}}
.photo{{width:30mm;height:30mm;overflow:hidden;flex:0 0 auto;border:1px solid var(--cv-accent-soft);}}
.photo-square{{border-radius:2mm;}}
.photo-rounded{{border-radius:5mm;}}
.photo-circle{{border-radius:50%;}}
.photo img{{width:100%;height:100%;object-fit:cover;display:block;}}
.contact{{display:flex;gap:3mm;flex-wrap:wrap;color:{muted};font-size:{F(bodySize - 0.5)}pt;margin-top:2.5mm;}}
.contact-stacked{{flex-direction:column;gap:1.8mm;}}
.contact-item{{display:inline-flex;align-items:center;gap:1.2mm;min-width:0;max-width:100%;overflow-wrap:anywhere;}}
.contact a{{color:inherit;text-decoration:none;min-width:0;overflow-wrap:anywhere;word-break:break-word;}}
.main a{{color:var(--cv-accent-color);text-decoration-color:var(--cv-accent-soft);}}
.contact svg{{width:3.2mm;height:3.2mm;flex:0 0 auto;opacity:.85;}}
.hero{{margin-bottom:{sectionGap}mm;}}
.hero .name{{font-size:{F(t.NameSizePt - 3)}pt;}}
.section{{margin-top:{sectionGap}mm;}}
.section:first-child{{margin-top:0;}}
.section-title{{margin:0 0 {F(2.6 * density)}mm 0;font-size:{F(headingSize)}pt;font-weight:700;color:{headingColor};}}
{headingCss}
.section-presentation-grid{{display:grid;grid-template-columns:repeat(var(--cv-section-columns,1),minmax(0,1fr));column-gap:4mm;align-items:start;}}
.section-presentation-grid>.section-title{{grid-column:1/-1;}}
.section-presentation-grid>.entry{{min-width:0;}}
.section-presentation-compact>.entry{{margin-bottom:{compactEntryGap}mm;}}
.section-presentation-compact>.bullets li{{margin-bottom:{F(.75 * density)}mm;}}
.section-presentation-bubble>.tags,.section-presentation-bubble>.bullets{{list-style:none;padding:0;display:flex;flex-wrap:wrap;gap:1.4mm;}}
.section-presentation-bubble>.bullets li,.section-presentation-bubble>.tags li{{margin:0;border:1px solid var(--cv-accent-soft);border-radius:999px;padding:.7mm 2.2mm;}}
.section-presentation-bubble>.bullets li::marker{{content:"";}}
.bullets{{margin:0;padding-left:4.5mm;}}
.bullets li{{margin:0 0 {F(1.6 * density)}mm 0;}}
.bullets li::marker{{color:var(--cv-accent-color);}}
.tags{{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:1.8mm;}}
.skills-text{{margin:0;overflow-wrap:anywhere;}}
.tag{{border:1px solid var(--cv-accent-soft);border-radius:999px;padding:.7mm 2.2mm;font-size:{F(bodySize - 0.5)}pt;max-width:100%;overflow-wrap:anywhere;}}
.paragraphs p{{margin:0 0 {F(1.8 * density)}mm;white-space:pre-line;}}
.skill-groups{{display:grid;gap:{F(1.7 * density)}mm;}}
.skill-group{{display:grid;grid-template-columns:minmax(24mm,.3fr) minmax(0,1fr);gap:3mm;break-inside:avoid-page;page-break-inside:avoid;}}
.skill-group-name{{font-family:{t.UtilityFont};font-weight:700;color:var(--cv-accent-color);overflow-wrap:anywhere;}}
.skill-group-items{{display:flex;flex-wrap:wrap;gap:.8mm 3mm;list-style:none;margin:0;padding:0;}}
.skill-group-items li{{overflow-wrap:anywhere;}}
.entry{{margin-bottom:{entryGap}mm;}}
.entry:last-child{{margin-bottom:0;}}
.entry-head{{display:flex;justify-content:space-between;gap:1.5mm 4mm;align-items:baseline;flex-wrap:wrap;break-after:avoid-page;page-break-after:avoid;}}
.entry-title{{font-weight:700;font-size:{F(bodySize + 1)}pt;min-width:0;flex:1 1 50mm;overflow-wrap:anywhere;}}
.entry-meta{{color:{muted};font-size:{F(bodySize - 0.5)}pt;white-space:normal;text-align:right;max-width:100%;overflow-wrap:anywhere;}}
.entry-subtitle{{color:{muted};font-size:{F(bodySize)}pt;margin:.4mm 0 1.2mm 0;}}
.entry-tags{{margin-top:1.4mm;}}
{layoutCss}
{numberedSections}
/* Print quality: keep normal entries whole, but allow intentionally classified long entries and
   long list items to flow. An unsplittable block taller than a page is otherwise clipped. */
.entry{{break-inside:avoid-page;page-break-inside:avoid;}}
.entry-flow{{break-inside:auto;page-break-inside:auto;}}
.section-title{{break-after:avoid-page;page-break-after:avoid;}}
.section-title + .entry,.section-title + .bullets,.section-title + .tags,.section-title + .paragraphs,.section-title + .skill-groups{{break-before:avoid-page;page-break-before:avoid;}}
.section{{break-inside:avoid-page;page-break-inside:avoid;}}
.section-flow{{break-inside:auto;page-break-inside:auto;}}
.tag,.contact-item{{break-inside:avoid;}}
.bullets li{{break-inside:avoid-page;page-break-inside:avoid;orphans:2;widows:2;overflow-wrap:anywhere;}}
.bullets li.item-flow{{break-inside:auto;page-break-inside:auto;}}
/* Physical-page margins belong to paged media rather than an individual template box. This makes
   the same top/bottom safe area repeat on every page while themes continue to own horizontal
   layout and density. Screen preview mirrors these bounds in CvBuilderEditor. */
@media print{{
body{{background:transparent;}}
.page{{width:auto;min-height:0;margin:0;}}
.cols{{min-height:0;}}
.header{{padding-top:0;}}
.main,.sidebar{{padding-top:0;padding-bottom:0;}}
}}
@page{{size:{page.w} {page.h};margin:{margin}mm 0;}}
";
    }

    private static double DensityScale(string? density) => (density ?? "balanced").Trim().ToLowerInvariant() switch
    {
        "compact" => 0.82,
        "roomy" => 1.18,
        _ => 1.0,
    };

    private static string Icon(string kind) => kind switch
    {
        "email" => Svg("M2 4h12v8H2z M2 4l6 4 6-4"),
        "phone" => Svg("M3 3c0 5 5 10 10 10l1-3-3-1-1 1c-2-1-4-3-5-5l1-1-1-3z"),
        "location" => Svg("M8 1a4 4 0 0 0-4 4c0 3 4 8 4 8s4-5 4-8a4 4 0 0 0-4-4z M8 5v.01"),
        "web" => Svg("M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1z M1 8h14 M8 1c2 2 2 12 0 14 M8 1c-2 2-2 12 0 14"),
        "linkedin" => Svg("M3 6v7 M3 3v.01 M7 13V6 M7 9c0-3 5-3 5 0v4"),
        _ => string.Empty,
    };

    private static string Svg(string path) =>
        $@"<svg viewBox=""0 0 16 16"" fill=""none"" stroke=""currentColor"" stroke-width=""1.3"" stroke-linecap=""round"" stroke-linejoin=""round""><path d=""{path}""/></svg>";

    private static string Override(string? value, string fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();

    private static string ContrastInk(string background)
    {
        if (background.Length != 7 || background[0] != '#' || !background.Skip(1).All(Uri.IsHexDigit)) return "#000";

        var r = Convert.ToInt32(background.Substring(1, 2), 16) / 255d;
        var g = Convert.ToInt32(background.Substring(3, 2), 16) / 255d;
        var b = Convert.ToInt32(background.Substring(5, 2), 16) / 255d;
        static double Channel(double value) => value <= 0.04045 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4);
        var luminance = 0.2126 * Channel(r) + 0.7152 * Channel(g) + 0.0722 * Channel(b);
        var whiteContrast = 1.05 / (luminance + 0.05);
        var blackContrast = (luminance + 0.05) / 0.05;
        return whiteContrast >= blackContrast ? "#fff" : "#000";
    }

    private static string F(double v) => v.ToString("0.##", CultureInfo.InvariantCulture);
    private static string Enc(string? v) => WebUtility.HtmlEncode(v ?? string.Empty);
    private static string Attr(string? v) => WebUtility.HtmlEncode(v ?? string.Empty).Replace("'", "&#39;", StringComparison.Ordinal);

    private static string Slug(string v)
    {
        var cleaned = new string((v ?? string.Empty).ToLowerInvariant().Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray());
        while (cleaned.Contains("--", StringComparison.Ordinal)) cleaned = cleaned.Replace("--", "-", StringComparison.Ordinal);
        return cleaned.Trim('-');
    }
}
