using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using System.IO;
using System.Text;

namespace SharepointToolbox.Services.Export;

/// <summary>
/// Exports StorageNode tree to a self-contained HTML file with collapsible subfolder rows.
/// Port of PS Export-StorageToHTML (PS lines 1621-1780).
/// Uses a toggle(i) JS pattern where each collapsible row has id="sf-{i}".
/// </summary>
public class StorageHtmlExportService
{
    private int _togIdx;
    private string[] _kindLabels = Array.Empty<string>();
    private string[] _kindLabelsHtml = Array.Empty<string>();

    /// <summary>
    /// Builds a self-contained HTML report with one collapsible row per
    /// library and indented child folders. Library-only variant — use the
    /// overload that accepts <see cref="FileTypeMetric"/>s when a file-type
    /// breakdown section is desired.
    /// </summary>
    public string BuildHtml(IReadOnlyList<StorageNode> nodes, ReportBranding? branding = null)
    {
        var sb = new StringBuilder(3072 + nodes.Count * 340);
        BuildHtmlCore(sb, nodes, branding);
        return sb.ToString();
    }

    private void BuildHtmlCore(StringBuilder sb, IReadOnlyList<StorageNode> nodes, ReportBranding? branding)
    {
        var T = TranslationSource.Instance;
        _togIdx = 0;
        _kindLabels = BuildKindLabelCache();
        _kindLabelsHtml = BuildHtmlEncodedCache(_kindLabels);

        sb.AppendLine("<!DOCTYPE html>");
        sb.AppendLine("<html lang=\"en\">");
        sb.AppendLine("<head>");
        sb.AppendLine("<meta charset=\"UTF-8\">");
        sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
        sb.AppendLine($"<title>{T["report.title.storage"]}</title>");
        sb.AppendLine("""
            <style>
              body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; background: #f5f5f5; }
              h1 { color: #0078d4; }
              table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.15); }
              th { background: #0078d4; color: #fff; padding: 8px 12px; text-align: left; font-weight: 600; }
              td { padding: 6px 12px; border-bottom: 1px solid #e0e0e0; vertical-align: top; }
              tr:hover { background: #f0f7ff; }
              .toggle-btn { background: none; border: 1px solid #0078d4; color: #0078d4; border-radius: 3px;
                            cursor: pointer; font-size: 11px; padding: 1px 6px; margin-right: 6px; }
              .toggle-btn:hover { background: #e5f1fb; }
              .sf-tbl { width: 100%; border: none; box-shadow: none; margin: 0; }
              .sf-tbl td { background: #fafcff; font-size: 12px; }
              .num { text-align: right; font-variant-numeric: tabular-nums; }
              .generated { font-size: 11px; color: #888; margin-top: 12px; }
            </style>
            <script>
              function toggle(i) {
                var row = document.getElementById('sf-' + i);
                if (row) row.style.display = (row.style.display === 'none' || row.style.display === '') ? 'table-row' : 'none';
              }
            </script>
            </head>
            <body>
            """);
        sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
        sb.AppendLine($"<h1>{T["report.title.storage"]}</h1>");

        // Single-pass root aggregation: replaces 4 separate enumerations
        // (.Where().ToList() + 3× .Sum() + a final .Where() during render).
        var rootNodes0 = new List<StorageNode>(Math.Min(nodes.Count, 64));
        long siteTotal0 = 0, versionTotal0 = 0, fileTotal0 = 0;
        foreach (var n in nodes)
        {
            if (n.IndentLevel != 0) continue;
            rootNodes0.Add(n);
            siteTotal0    += n.TotalSizeBytes;
            versionTotal0 += n.VersionSizeBytes;
            fileTotal0    += n.TotalFileCount;
        }

        sb.AppendLine($"""
            <div style="display:flex;gap:16px;margin:16px 0;flex-wrap:wrap">
              <div style="background:#fff;border-radius:8px;padding:14px 20px;min-width:160px;box-shadow:0 1px 4px rgba(0,0,0,.1)"><div style="font-size:1.8rem;font-weight:700;color:#0078d4">{FormatSize(siteTotal0)}</div><div style="font-size:.8rem;color:#666">{T["report.stat.total_size"]}</div></div>
              <div style="background:#fff;border-radius:8px;padding:14px 20px;min-width:160px;box-shadow:0 1px 4px rgba(0,0,0,.1)"><div style="font-size:1.8rem;font-weight:700;color:#0078d4">{FormatSize(versionTotal0)}</div><div style="font-size:.8rem;color:#666">{T["report.stat.version_size"]}</div></div>
              <div style="background:#fff;border-radius:8px;padding:14px 20px;min-width:160px;box-shadow:0 1px 4px rgba(0,0,0,.1)"><div style="font-size:1.8rem;font-weight:700;color:#0078d4">{fileTotal0:N0}</div><div style="font-size:.8rem;color:#666">{T["report.stat.files"]}</div></div>
            </div>
            """);

        sb.AppendLine($"""
            <table>
              <thead>
                <tr>
                  <th>{T["report.col.library_folder"]}</th>
                  <th>{T["stor.col.kind"]}</th>
                  <th>{T["report.col.site"]}</th>
                  <th class="num">{T["report.stat.files"]}</th>
                  <th class="num">{T["report.stat.total_size"]}</th>
                  <th class="num">{T["report.stat.version_size"]}</th>
                  <th>{T["report.col.last_modified"]}</th>
                </tr>
              </thead>
              <tbody>
            """);

        // Render only the pre-materialized root list — recursing into
        // Children handles descendants. Iterating the flat list would render
        // every descendant a second time as a top-level row.
        foreach (var node in rootNodes0)
        {
            RenderNode(sb, node);
        }

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

        sb.AppendLine($"<p class=\"generated\">{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
        sb.AppendLine("</body></html>");
    }

    /// <summary>
    /// Builds an HTML report including a file-type breakdown chart section.
    /// </summary>
    public string BuildHtml(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, ReportBranding? branding = null)
    {
        var sb = new StringBuilder(4096 + nodes.Count * 340 + fileTypeMetrics.Count * 220);
        BuildHtmlCore(sb, nodes, fileTypeMetrics, branding);
        return sb.ToString();
    }

    private void BuildHtmlCore(StringBuilder sb, IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, ReportBranding? branding)
    {
        var T = TranslationSource.Instance;
        _togIdx = 0;
        _kindLabels = BuildKindLabelCache();
        _kindLabelsHtml = BuildHtmlEncodedCache(_kindLabels);

        sb.AppendLine("<!DOCTYPE html>");
        sb.AppendLine("<html lang=\"en\">");
        sb.AppendLine("<head>");
        sb.AppendLine("<meta charset=\"UTF-8\">");
        sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
        sb.AppendLine($"<title>{T["report.title.storage"]}</title>");
        sb.AppendLine("""
            <style>
              body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; background: #f5f5f5; }
              h1 { color: #0078d4; }
              h2 { color: #333; margin-top: 24px; }
              table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.15); }
              th { background: #0078d4; color: #fff; padding: 8px 12px; text-align: left; font-weight: 600; }
              td { padding: 6px 12px; border-bottom: 1px solid #e0e0e0; vertical-align: top; }
              tr:hover { background: #f0f7ff; }
              .toggle-btn { background: none; border: 1px solid #0078d4; color: #0078d4; border-radius: 3px;
                            cursor: pointer; font-size: 11px; padding: 1px 6px; margin-right: 6px; }
              .toggle-btn:hover { background: #e5f1fb; }
              .sf-tbl { width: 100%; border: none; box-shadow: none; margin: 0; }
              .sf-tbl td { background: #fafcff; font-size: 12px; }
              .num { text-align: right; font-variant-numeric: tabular-nums; }
              .generated { font-size: 11px; color: #888; margin-top: 12px; }
              .chart-section { margin: 20px 0; padding: 16px; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.15); }
              .bar-row { display: flex; align-items: center; margin: 4px 0; }
              .bar-label { width: 80px; font-size: 12px; font-weight: 600; text-align: right; padding-right: 10px; }
              .bar-track { flex: 1; background: #eee; border-radius: 4px; height: 22px; position: relative; }
              .bar-fill { height: 100%; border-radius: 4px; background: #0078d4; min-width: 2px; }
              .bar-value { font-size: 11px; color: #555; padding-left: 8px; white-space: nowrap; min-width: 140px; }
              .stats { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }
              .stat-card { background: #fff; border-radius: 8px; padding: 14px 20px; min-width: 160px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
              .stat-card .value { font-size: 1.8rem; font-weight: 700; color: #0078d4; }
              .stat-card .label { font-size: .8rem; color: #666; margin-top: 2px; }
            </style>
            <script>
              function toggle(i) {
                var row = document.getElementById('sf-' + i);
                if (row) row.style.display = (row.style.display === 'none' || row.style.display === '') ? 'table-row' : 'none';
              }
            </script>
            </head>
            <body>
            """);
        sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
        sb.AppendLine($"<h1>{T["report.title.storage"]}</h1>");

        // ── Summary cards (single-pass aggregation) ──
        var rootNodes = new List<StorageNode>(Math.Min(nodes.Count, 64));
        long siteTotal = 0, versionTotal = 0, fileTotal = 0;
        foreach (var n in nodes)
        {
            if (n.IndentLevel != 0) continue;
            rootNodes.Add(n);
            siteTotal    += n.TotalSizeBytes;
            versionTotal += n.VersionSizeBytes;
            fileTotal    += n.TotalFileCount;
        }

        sb.AppendLine("<div class=\"stats\">");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{FormatSize(siteTotal)}</div><div class=\"label\">{T["report.stat.total_size"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{FormatSize(versionTotal)}</div><div class=\"label\">{T["report.stat.version_size"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{fileTotal:N0}</div><div class=\"label\">{T["report.stat.files"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{rootNodes.Count}</div><div class=\"label\">{T["report.stat.libraries"]}</div></div>");
        sb.AppendLine("</div>");

        // ── File type chart section ──
        if (fileTypeMetrics.Count > 0)
        {
            var maxSize = fileTypeMetrics.Max(m => m.TotalSizeBytes);
            var totalSize = fileTypeMetrics.Sum(m => m.TotalSizeBytes);
            var totalFiles = fileTypeMetrics.Sum(m => m.FileCount);

            sb.AppendLine("<div class=\"chart-section\">");
            sb.AppendLine($"<h2>{T["report.section.storage_by_file_type"]} ({totalFiles:N0} {T["report.text.files_unit"]}, {FormatSize(totalSize)})</h2>");

            var colors = new[] { "#0078d4", "#2b88d8", "#106ebe", "#005a9e", "#004578",
                                 "#00bcf2", "#009e49", "#8cbd18", "#ffb900", "#d83b01" };

            int idx = 0;
            foreach (var m in fileTypeMetrics.Take(15))
            {
                double pct = maxSize > 0 ? m.TotalSizeBytes * 100.0 / maxSize : 0;
                string color = colors[idx % colors.Length];
                string label = string.IsNullOrEmpty(m.Extension) ? T["report.text.no_ext"] : m.Extension;

                sb.AppendLine($"""
                    <div class="bar-row">
                      <span class="bar-label">{HtmlEncode(label)}</span>
                      <div class="bar-track"><div class="bar-fill" style="width:{pct:F1}%;background:{color}"></div></div>
                      <span class="bar-value">{FormatSize(m.TotalSizeBytes)} &middot; {m.FileCount:N0} {T["report.text.files_unit"]}</span>
                    </div>
                    """);
                idx++;
            }

            sb.AppendLine("</div>");
        }

        // ── Storage table ──
        sb.AppendLine($"<h2>{T["report.section.library_details"]}</h2>");
        sb.AppendLine($"""
            <table>
              <thead>
                <tr>
                  <th>{T["report.col.library_folder"]}</th>
                  <th>{T["stor.col.kind"]}</th>
                  <th>{T["report.col.site"]}</th>
                  <th class="num">{T["report.stat.files"]}</th>
                  <th class="num">{T["report.stat.total_size"]}</th>
                  <th class="num">{T["report.stat.version_size"]}</th>
                  <th>{T["report.col.last_modified"]}</th>
                </tr>
              </thead>
              <tbody>
            """);

        // Render only the pre-materialized root list — recursing into
        // Children handles descendants. Iterating the flat list would render
        // every descendant a second time as a top-level row.
        foreach (var node in rootNodes)
        {
            RenderNode(sb, node);
        }

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

        sb.AppendLine($"<p class=\"generated\">{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
        sb.AppendLine("</body></html>");
    }

    /// <summary>Writes the library-only HTML report to <paramref name="filePath"/>.</summary>
    public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, string filePath, CancellationToken ct, ReportBranding? branding = null)
    {
        // Build into StringBuilder, stream chunks straight to disk —
        // skips a full-document char-array copy from sb.ToString().
        var sb = new StringBuilder(3072 + nodes.Count * 340);
        BuildHtmlCore(sb, nodes, branding);
        await ExportFileWriter.WriteHtmlChunksAsync(filePath, sb, ct);
    }

    /// <summary>Writes the HTML report including the file-type breakdown chart.</summary>
    public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, string filePath, CancellationToken ct, ReportBranding? branding = null)
    {
        var sb = new StringBuilder(4096 + nodes.Count * 340 + fileTypeMetrics.Count * 220);
        BuildHtmlCore(sb, nodes, fileTypeMetrics, branding);
        await ExportFileWriter.WriteHtmlChunksAsync(filePath, sb, ct);
    }

    /// <summary>
    /// Split-aware HTML export for storage metrics.
    /// Single → one file. BySite + SeparateFiles → one file per site.
    /// BySite + SingleTabbed → one HTML with per-site iframe tabs. File-type
    /// metrics are replicated across partitions because they are not
    /// attributed per-site by the scanner.
    /// </summary>
    public async Task WriteAsync(
        IReadOnlyList<StorageNode> nodes,
        IReadOnlyList<FileTypeMetric> fileTypeMetrics,
        string basePath,
        ReportSplitMode splitMode,
        HtmlSplitLayout layout,
        CancellationToken ct,
        ReportBranding? branding = null)
    {
        if (splitMode != ReportSplitMode.BySite)
        {
            await WriteAsync(nodes, fileTypeMetrics, basePath, ct, branding);
            return;
        }

        var partitions = StorageCsvExportService.PartitionBySite(nodes).ToList();
        if (layout == HtmlSplitLayout.SingleTabbed)
        {
            var parts = partitions
                .Select(p => (p.Label, Html: BuildHtml(p.Partition, fileTypeMetrics, branding)))
                .ToList();
            var title = TranslationSource.Instance["report.title.storage"];
            var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, title);
            await File.WriteAllTextAsync(basePath, tabbed, Encoding.UTF8, ct);
            return;
        }

        foreach (var (label, partNodes) in partitions)
        {
            ct.ThrowIfCancellationRequested();
            var path = ReportSplitHelper.BuildPartitionPath(basePath, label);
            await WriteAsync(partNodes, fileTypeMetrics, path, ct, branding);
        }
    }

    // ── Private rendering ────────────────────────────────────────────────────

    private void RenderNode(StringBuilder sb, StorageNode node)
    {
        bool hasChildren = node.Children.Count > 0;
        int myIdx = hasChildren ? ++_togIdx : 0;

        string nameCell = hasChildren
            ? $"<button class=\"toggle-btn\" onclick=\"toggle({myIdx})\">&#9654;</button>{HtmlEncode(node.Name)}"
            : $"<span style=\"margin-left:{node.IndentLevel * 16}px\">{HtmlEncode(node.Name)}</span>";

        AppendRow(sb, node, nameCell);

        if (hasChildren)
        {
            sb.AppendLine($"<tr id=\"sf-{myIdx}\" style=\"display:none\"><td colspan=\"7\" style=\"padding:0\">");
            sb.AppendLine("<table class=\"sf-tbl\"><tbody>");
            foreach (var child in node.Children)
            {
                RenderChildNode(sb, child);
            }
            sb.AppendLine("</tbody></table>");
            sb.AppendLine("</td></tr>");
        }
    }

    private void RenderChildNode(StringBuilder sb, StorageNode node)
    {
        bool hasChildren = node.Children.Count > 0;
        int myIdx = hasChildren ? ++_togIdx : 0;

        string indent = $"margin-left:{(node.IndentLevel + 1) * 16}px";
        string nameCell = hasChildren
            ? $"<span style=\"{indent}\"><button class=\"toggle-btn\" onclick=\"toggle({myIdx})\">&#9654;</button>{HtmlEncode(node.Name)}</span>"
            : $"<span style=\"{indent}\">{HtmlEncode(node.Name)}</span>";

        AppendRow(sb, node, nameCell);

        if (hasChildren)
        {
            sb.AppendLine($"<tr id=\"sf-{myIdx}\" style=\"display:none\"><td colspan=\"7\" style=\"padding:0\">");
            sb.AppendLine("<table class=\"sf-tbl\"><tbody>");
            foreach (var child in node.Children)
            {
                RenderChildNode(sb, child);
            }
            sb.AppendLine("</tbody></table>");
            sb.AppendLine("</td></tr>");
        }
    }

    /// <summary>
    /// Appends one data row given the pre-rendered name cell. Hot path:
    /// pulls localized kind labels from <see cref="_kindLabelsHtml"/> instead
    /// of going through <c>ResourceManager.GetString</c> + <c>HtmlEncode</c>
    /// per row.
    /// </summary>
    private void AppendRow(StringBuilder sb, StorageNode node, string nameCell)
    {
        int kindIdx = (int)node.Kind;
        string kindLabel = (uint)kindIdx < (uint)_kindLabelsHtml.Length
            ? _kindLabelsHtml[kindIdx]
            : HtmlEncode(node.Kind.ToString());
        string lastMod = node.LastModified.HasValue
            ? node.LastModified.Value.ToString("yyyy-MM-dd")
            : string.Empty;

        sb.AppendLine($"""
              <tr>
                <td>{nameCell}</td>
                <td>{kindLabel}</td>
                <td>{HtmlEncode(node.SiteTitle)}</td>
                <td class="num">{node.TotalFileCount:N0}</td>
                <td class="num">{FormatSize(node.TotalSizeBytes)}</td>
                <td class="num">{FormatSize(node.VersionSizeBytes)}</td>
                <td>{lastMod}</td>
              </tr>
            """);
    }

    private static string FormatSize(long bytes)
    {
        if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB";
        if (bytes >= 1_048_576L)     return $"{bytes / 1_048_576.0:F2} MB";
        if (bytes >= 1024L)          return $"{bytes / 1024.0:F2} KB";
        return $"{bytes} B";
    }

    private static string HtmlEncode(string value)
        => System.Net.WebUtility.HtmlEncode(value ?? string.Empty);

    private static string KindLabel(StorageNodeKind kind)
    {
        var T = TranslationSource.Instance;
        return kind switch
        {
            StorageNodeKind.Library          => T["stor.kind.library"],
            StorageNodeKind.HiddenLibrary    => T["stor.kind.hidden"],
            StorageNodeKind.PreservationHold => T["stor.kind.preservation"],
            StorageNodeKind.ListAttachments  => T["stor.kind.attachments"],
            StorageNodeKind.RecycleBin       => T["stor.kind.recyclebin"],
            StorageNodeKind.Subsite          => T["stor.kind.subsite"],
            _ => kind.ToString()
        };
    }

    /// <summary>
    /// Pre-resolves localized labels for every <see cref="StorageNodeKind"/>
    /// once per export. Cached array index lookup avoids
    /// <c>ResourceManager.GetString</c> per row in hot rendering loops.
    /// </summary>
    private static string[] BuildKindLabelCache()
    {
        var values = (StorageNodeKind[])Enum.GetValues(typeof(StorageNodeKind));
        int max = 0;
        foreach (var v in values) { int i = (int)v; if (i > max) max = i; }
        var cache = new string[max + 1];
        for (int i = 0; i < cache.Length; i++) cache[i] = ((StorageNodeKind)i).ToString();
        foreach (var v in values) cache[(int)v] = KindLabel(v);
        return cache;
    }

    /// <summary>HTML-encodes each entry of <paramref name="raw"/> once.</summary>
    private static string[] BuildHtmlEncodedCache(string[] raw)
    {
        var encoded = new string[raw.Length];
        for (int i = 0; i < raw.Length; i++) encoded[i] = HtmlEncode(raw[i]);
        return encoded;
    }
}
