// Asset Naming Manager — turning a scan into something you can send to someone.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace AssetNamingManager
{
    /// <summary>
    /// Writes a <see cref="ScanReport"/> to disk. The HTML export is a single self-contained file
    /// with no external assets, so it can be attached to a CI build or mailed to an artist as-is.
    /// </summary>
    public static class ReportExporter
    {
        /// <summary>The file extension a format writes, dot included.</summary>
        public static string ExtensionFor(ReportFormat format)
        {
            switch (format)
            {
                case ReportFormat.Csv:      return ".csv";
                case ReportFormat.Json:     return ".json";
                case ReportFormat.Markdown: return ".md";
                default:                    return ".html";
            }
        }

        /// <summary>Writes the report and returns the absolute path that was written.</summary>
        public static string Write(ScanReport report, ReportFormat format, string path)
        {
            if (report == null) throw new ArgumentNullException("report");
            if (string.IsNullOrEmpty(path)) throw new ArgumentException("A path is required.", "path");

            string directory = Path.GetDirectoryName(path);
            if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            File.WriteAllText(path, Build(report, format), new UTF8Encoding(false));
            return Path.GetFullPath(path);
        }

        /// <summary>Renders the report to a string without touching the disk.</summary>
        public static string Build(ScanReport report, ReportFormat format)
        {
            switch (format)
            {
                case ReportFormat.Csv:      return BuildCsv(report);
                case ReportFormat.Json:     return BuildJson(report);
                case ReportFormat.Markdown: return BuildMarkdown(report);
                default:                    return BuildHtml(report);
            }
        }

        /// <summary>
        /// A timestamped file name such as <c>naming-2026-09-15-1432.html</c>, so yesterday's
        /// report is still there to compare against.
        /// </summary>
        public static string SuggestFileName(ReportFormat format, DateTime when)
        {
            return "naming-" + when.ToString("yyyy-MM-dd-HHmm", CultureInfo.InvariantCulture) + ExtensionFor(format);
        }

        // ================================================================ CSV

        static string BuildCsv(ScanReport report)
        {
            var sb = new StringBuilder();
            sb.AppendLine("Severity,Problem,Rule,Asset Path,Asset Name,Type,Message,Suggested Name,Suggested Folder");

            foreach (var v in report.violations)
            {
                sb.Append(Csv(v.severity.ToString())).Append(',');
                sb.Append(Csv(v.KindLabel)).Append(',');
                sb.Append(Csv(v.ruleName)).Append(',');
                sb.Append(Csv(v.assetPath)).Append(',');
                sb.Append(Csv(v.assetName)).Append(',');
                sb.Append(Csv(v.typeName)).Append(',');
                sb.Append(Csv(v.message)).Append(',');
                sb.Append(Csv(v.suggestedName)).Append(',');
                sb.Append(Csv(v.suggestedFolder)).AppendLine();
            }

            return sb.ToString();
        }

        static string Csv(string value)
        {
            if (string.IsNullOrEmpty(value)) return string.Empty;
            bool needsQuotes = value.IndexOf(',') >= 0 || value.IndexOf('"') >= 0 ||
                               value.IndexOf('\n') >= 0 || value.IndexOf('\r') >= 0;
            string escaped = value.Replace("\"", "\"\"").Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ");
            return needsQuotes ? "\"" + escaped + "\"" : escaped;
        }

        // ================================================================ JSON

        static string BuildJson(ScanReport report)
        {
            var sb = new StringBuilder();
            sb.AppendLine("{");
            sb.Append("  \"tool\": \"Asset Naming Manager\",\n");
            sb.Append("  \"version\": \"").Append(NamingManagerAbout.Version).Append("\",\n");
            sb.Append("  \"ruleSet\": ").Append(Json(report.ruleSetName)).Append(",\n");
            sb.Append("  \"timestampUtc\": ").Append(Json(report.timestampUtc.ToString("o", CultureInfo.InvariantCulture))).Append(",\n");
            sb.Append("  \"durationSeconds\": ").Append(report.durationSeconds.ToString("0.###", CultureInfo.InvariantCulture)).Append(",\n");
            sb.Append("  \"cancelled\": ").Append(report.cancelled ? "true" : "false").Append(",\n");
            sb.Append("  \"assetsScanned\": ").Append(report.assetsScanned).Append(",\n");
            sb.Append("  \"foldersScanned\": ").Append(report.foldersScanned).Append(",\n");
            sb.Append("  \"assetsIgnored\": ").Append(report.assetsIgnored).Append(",\n");
            sb.Append("  \"healthScore\": ").Append(report.HealthScore.ToString("0.0", CultureInfo.InvariantCulture)).Append(",\n");
            sb.Append("  \"errors\": ").Append(report.ErrorCount).Append(",\n");
            sb.Append("  \"warnings\": ").Append(report.WarningCount).Append(",\n");
            sb.Append("  \"info\": ").Append(report.InfoCount).Append(",\n");
            sb.AppendLine("  \"violations\": [");

            for (int i = 0; i < report.violations.Count; i++)
            {
                var v = report.violations[i];
                sb.Append("    {");
                sb.Append("\"severity\": ").Append(Json(v.severity.ToString())).Append(", ");
                sb.Append("\"kind\": ").Append(Json(v.kind.ToString())).Append(", ");
                sb.Append("\"problem\": ").Append(Json(v.KindLabel)).Append(", ");
                sb.Append("\"rule\": ").Append(Json(v.ruleName)).Append(", ");
                sb.Append("\"path\": ").Append(Json(v.assetPath)).Append(", ");
                sb.Append("\"guid\": ").Append(Json(v.guid)).Append(", ");
                sb.Append("\"type\": ").Append(Json(v.typeName)).Append(", ");
                sb.Append("\"message\": ").Append(Json(v.message)).Append(", ");
                sb.Append("\"suggestedName\": ").Append(Json(v.suggestedName)).Append(", ");
                sb.Append("\"suggestedFolder\": ").Append(Json(v.suggestedFolder)).Append(", ");
                sb.Append("\"autoFixable\": ").Append(v.CanAutoFix ? "true" : "false");
                sb.Append(i < report.violations.Count - 1 ? "},\n" : "}\n");
            }

            sb.AppendLine("  ]");
            sb.AppendLine("}");
            return sb.ToString();
        }

        static string Json(string value)
        {
            if (value == null) return "null";

            var sb = new StringBuilder(value.Length + 2);
            sb.Append('"');
            foreach (char c in value)
            {
                switch (c)
                {
                    case '"':  sb.Append("\\\""); break;
                    case '\\': sb.Append("\\\\"); break;
                    case '\b': sb.Append("\\b");  break;
                    case '\f': sb.Append("\\f");  break;
                    case '\n': sb.Append("\\n");  break;
                    case '\r': sb.Append("\\r");  break;
                    case '\t': sb.Append("\\t");  break;
                    default:
                        if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
                        else sb.Append(c);
                        break;
                }
            }
            sb.Append('"');
            return sb.ToString();
        }

        // ================================================================ Markdown

        static string BuildMarkdown(ScanReport report)
        {
            var sb = new StringBuilder();
            sb.AppendLine("# Asset naming report");
            sb.AppendLine();
            sb.AppendLine("Rule set **" + report.ruleSetName + "** — " +
                          report.timestampUtc.ToLocalTime().ToString("dd MMM yyyy HH:mm", CultureInfo.InvariantCulture));
            sb.AppendLine();
            sb.AppendLine("| | |");
            sb.AppendLine("|---|---|");
            sb.AppendLine("| Health score | **" + report.HealthScore.ToString("0", CultureInfo.InvariantCulture) +
                          " / 100** (" + report.HealthLabel + ") |");
            sb.AppendLine("| Assets scanned | " + report.assetsScanned + " |");
            sb.AppendLine("| Folders scanned | " + report.foldersScanned + " |");
            sb.AppendLine("| Findings | " + report.violations.Count + " |");
            sb.AppendLine("| Errors / Warnings / Info | " + report.ErrorCount + " / " + report.WarningCount + " / " + report.InfoCount + " |");
            sb.AppendLine("| Auto-fixable | " + report.AutoFixableCount + " |");
            sb.AppendLine("| Scan time | " + report.durationSeconds.ToString("0.00", CultureInfo.InvariantCulture) + "s |");
            sb.AppendLine();

            if (report.violations.Count == 0)
            {
                sb.AppendLine("No findings. Every asset matches the convention.");
                return sb.ToString();
            }

            var byRule = ScanReport.Sorted(report.CountBy(v => string.IsNullOrEmpty(v.ruleName) ? "Project-wide" : v.ruleName), 10);
            sb.AppendLine("## Worst offenders");
            sb.AppendLine();
            sb.AppendLine("| Rule | Findings |");
            sb.AppendLine("|---|---:|");
            foreach (var pair in byRule) sb.AppendLine("| " + Md(pair.Key) + " | " + pair.Value + " |");
            sb.AppendLine();

            sb.AppendLine("## All findings");
            sb.AppendLine();
            sb.AppendLine("| Severity | Problem | Asset | Message | Suggested |");
            sb.AppendLine("|---|---|---|---|---|");
            foreach (var v in report.violations)
            {
                sb.Append("| ").Append(v.severity)
                  .Append(" | ").Append(Md(v.KindLabel))
                  .Append(" | `").Append(Md(v.assetPath)).Append("`")
                  .Append(" | ").Append(Md(v.message))
                  .Append(" | ").Append(v.CanAutoFix ? "`" + Md(NamingScanner.GetFileName(v.ResolvedPath)) + "`" : "—")
                  .AppendLine(" |");
            }

            return sb.ToString();
        }

        static string Md(string value)
        {
            return string.IsNullOrEmpty(value) ? string.Empty : value.Replace("|", "\\|").Replace("\n", " ");
        }

        // ================================================================ HTML

        static string BuildHtml(ScanReport report)
        {
            var sb = new StringBuilder();
            string generated = report.timestampUtc.ToLocalTime().ToString("dd MMM yyyy, HH:mm", CultureInfo.InvariantCulture);
            float score = report.HealthScore;
            string scoreClass = score >= 95f ? "good" : score >= 75f ? "ok" : "bad";

            sb.AppendLine("<!doctype html>");
            sb.AppendLine("<html lang=\"en\"><head><meta charset=\"utf-8\">");
            sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">");
            sb.AppendLine("<title>Asset naming report — " + Html(report.ruleSetName) + "</title>");
            sb.AppendLine("<style>" + HtmlCss + "</style></head><body>");

            sb.AppendLine("<header><div class=\"wrap\">");
            sb.AppendLine("<div class=\"brand\"><span class=\"badge\">ANM</span><div><h1>Asset naming report</h1>");
            sb.AppendLine("<p class=\"sub\">Rule set <strong>" + Html(report.ruleSetName) + "</strong> &middot; " + generated + "</p></div></div>");
            sb.AppendLine("<div class=\"score " + scoreClass + "\"><span class=\"num\">" +
                          score.ToString("0", CultureInfo.InvariantCulture) + "</span><span class=\"lbl\">" +
                          Html(report.HealthLabel) + "</span></div>");
            sb.AppendLine("</div></header>");

            sb.AppendLine("<main class=\"wrap\">");

            sb.AppendLine("<section class=\"cards\">");
            sb.Append(Card("Assets scanned", report.assetsScanned.ToString(CultureInfo.InvariantCulture), ""));
            sb.Append(Card("Findings", report.violations.Count.ToString(CultureInfo.InvariantCulture), ""));
            sb.Append(Card("Errors", report.ErrorCount.ToString(CultureInfo.InvariantCulture), "err"));
            sb.Append(Card("Warnings", report.WarningCount.ToString(CultureInfo.InvariantCulture), "warn"));
            sb.Append(Card("Info", report.InfoCount.ToString(CultureInfo.InvariantCulture), "info"));
            sb.Append(Card("Auto-fixable", report.AutoFixableCount.ToString(CultureInfo.InvariantCulture), "good"));
            sb.AppendLine("</section>");

            if (report.violations.Count == 0)
            {
                sb.AppendLine("<section class=\"empty\"><h2>Nothing to report</h2>" +
                              "<p>Every asset in the scanned folders matches the convention.</p></section>");
            }
            else
            {
                sb.Append(BarSection("Findings per rule",
                    ScanReport.Sorted(report.CountBy(v => string.IsNullOrEmpty(v.ruleName) ? "Project-wide" : v.ruleName), 8)));
                sb.Append(BarSection("Findings per problem",
                    ScanReport.Sorted(report.CountBy(v => v.KindLabel), 8)));
                sb.Append(BarSection("Worst folders",
                    ScanReport.Sorted(report.CountBy(v => v.Directory), 8)));

                sb.AppendLine("<section><h2>All findings <span class=\"count\">" + report.violations.Count + "</span></h2>");
                sb.AppendLine("<input id=\"q\" type=\"search\" placeholder=\"Filter by name, path, rule or message\" oninput=\"filterRows()\">");
                sb.AppendLine("<table id=\"t\"><thead><tr><th>Severity</th><th>Problem</th><th>Asset</th>" +
                              "<th>Rule</th><th>Message</th><th>Suggested</th></tr></thead><tbody>");

                foreach (var v in report.violations)
                {
                    string sev = v.severity.ToString().ToLowerInvariant();
                    sb.Append("<tr><td><span class=\"pill ").Append(sev).Append("\">").Append(v.severity).Append("</span></td>");
                    sb.Append("<td>").Append(Html(v.KindLabel)).Append("</td>");
                    sb.Append("<td><code title=\"").Append(Html(v.assetPath)).Append("\">")
                      .Append(Html(v.assetName)).Append(Html(v.extension)).Append("</code>")
                      .Append("<div class=\"path\">").Append(Html(v.Directory)).Append("</div></td>");
                    sb.Append("<td>").Append(Html(string.IsNullOrEmpty(v.ruleName) ? "—" : v.ruleName)).Append("</td>");
                    sb.Append("<td>").Append(Html(v.message)).Append("</td>");
                    sb.Append("<td>").Append(v.CanAutoFix
                        ? "<code class=\"fix\">" + Html(NamingScanner.GetFileName(v.ResolvedPath)) + "</code>"
                        : "<span class=\"muted\">manual</span>").Append("</td></tr>\n");
                }

                sb.AppendLine("</tbody></table></section>");
            }

            sb.AppendLine("</main>");
            sb.AppendLine("<footer class=\"wrap\">Generated by Asset Naming Manager " + NamingManagerAbout.Version +
                          " &middot; " + report.assetsScanned + " assets and " + report.foldersScanned +
                          " folders in " + report.durationSeconds.ToString("0.00", CultureInfo.InvariantCulture) + "s" +
                          (report.assetsIgnored > 0 ? " &middot; " + report.assetsIgnored + " ignored" : "") +
                          "</footer>");
            sb.AppendLine("<script>" + HtmlJs + "</script>");
            sb.AppendLine("</body></html>");
            return sb.ToString();
        }

        static string Card(string label, string value, string cls)
        {
            return "<div class=\"card " + cls + "\"><span class=\"v\">" + value + "</span><span class=\"l\">" + label + "</span></div>\n";
        }

        static string BarSection(string title, List<KeyValuePair<string, int>> rows)
        {
            if (rows.Count == 0) return string.Empty;

            int max = 1;
            foreach (var r in rows) if (r.Value > max) max = r.Value;

            var sb = new StringBuilder();
            sb.AppendLine("<section class=\"bars\"><h2>" + Html(title) + "</h2>");
            foreach (var r in rows)
            {
                int pct = (int)Math.Round(100.0 * r.Value / max);
                sb.Append("<div class=\"bar\"><span class=\"k\" title=\"").Append(Html(r.Key)).Append("\">")
                  .Append(Html(r.Key)).Append("</span>")
                  .Append("<span class=\"track\"><span class=\"fill\" style=\"width:").Append(pct).Append("%\"></span></span>")
                  .Append("<span class=\"n\">").Append(r.Value).Append("</span></div>\n");
            }
            sb.AppendLine("</section>");
            return sb.ToString();
        }

        static string Html(string value)
        {
            if (string.IsNullOrEmpty(value)) return string.Empty;
            return value.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
        }

        const string HtmlCss =
            ":root{--bg:#f7f8fa;--fg:#1b1f24;--mut:#6b7280;--line:#e3e6ea;--card:#fff;" +
            "--err:#d64545;--warn:#c98a10;--info:#3b7ddd;--good:#2f9e6d;--accent:#5b53d4}" +
            "@media(prefers-color-scheme:dark){:root{--bg:#14161a;--fg:#e8eaed;--mut:#9aa1ab;--line:#2a2e35;--card:#1c1f25}}" +
            "*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);" +
            "font:14px/1.55 -apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}" +
            ".wrap{max-width:1180px;margin:0 auto;padding:0 20px}" +
            "header{background:var(--card);border-bottom:1px solid var(--line);padding:22px 0}" +
            "header .wrap{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap}" +
            ".brand{display:flex;align-items:center;gap:14px}" +
            ".badge{width:44px;height:44px;border-radius:11px;background:var(--accent);color:#fff;font-weight:700;" +
            "display:flex;align-items:center;justify-content:center;letter-spacing:.5px;flex:none}" +
            "h1{font-size:19px;margin:0}h2{font-size:15px;margin:0 0 12px;letter-spacing:.2px}" +
            ".sub{margin:2px 0 0;color:var(--mut);font-size:13px}" +
            ".score{text-align:right;line-height:1}.score .num{font-size:40px;font-weight:700;display:block}" +
            ".score .lbl{font-size:12px;color:var(--mut);text-transform:uppercase;letter-spacing:.8px}" +
            ".score.good .num{color:var(--good)}.score.ok .num{color:var(--warn)}.score.bad .num{color:var(--err)}" +
            "main{padding:24px 0 40px}section{margin-bottom:28px}" +
            ".cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}" +
            ".card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:2px}" +
            ".card .v{font-size:26px;font-weight:650}.card .l{font-size:12px;color:var(--mut)}" +
            ".card.err .v{color:var(--err)}.card.warn .v{color:var(--warn)}.card.info .v{color:var(--info)}.card.good .v{color:var(--good)}" +
            ".bars{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px}" +
            ".bar{display:grid;grid-template-columns:230px 1fr 44px;align-items:center;gap:12px;margin:7px 0;font-size:13px}" +
            ".bar .k{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--mut)}" +
            ".bar .track{background:var(--line);height:8px;border-radius:99px;overflow:hidden}" +
            ".bar .fill{display:block;height:100%;background:var(--accent);border-radius:99px}" +
            ".bar .n{text-align:right;font-variant-numeric:tabular-nums}" +
            "input#q{width:100%;padding:10px 12px;margin-bottom:12px;border:1px solid var(--line);" +
            "border-radius:8px;background:var(--card);color:var(--fg);font-size:14px}" +
            "table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}" +
            "th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--mut);" +
            "padding:10px 12px;border-bottom:1px solid var(--line);white-space:nowrap}" +
            "td{padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top}" +
            "tr:last-child td{border-bottom:0}tbody tr:hover{background:rgba(127,127,127,.06)}" +
            "code{font:12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;word-break:break-all}" +
            "code.fix{color:var(--good)}.path{font-size:11px;color:var(--mut);word-break:break-all}" +
            ".muted{color:var(--mut)}.count{color:var(--mut);font-weight:400}" +
            ".pill{display:inline-block;padding:2px 9px;border-radius:99px;font-size:11px;font-weight:600;color:#fff}" +
            ".pill.error{background:var(--err)}.pill.warning{background:var(--warn)}.pill.info{background:var(--info)}" +
            ".empty{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:40px;text-align:center}" +
            ".empty p{color:var(--mut);margin:6px 0 0}" +
            "footer{color:var(--mut);font-size:12px;padding:16px 20px 32px;border-top:1px solid var(--line)}";

        const string HtmlJs =
            "function filterRows(){var q=document.getElementById('q').value.toLowerCase();" +
            "var rows=document.querySelectorAll('#t tbody tr');" +
            "for(var i=0;i<rows.length;i++){rows[i].style.display=" +
            "rows[i].textContent.toLowerCase().indexOf(q)>-1?'':'none';}}";
    }
}
