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

namespace SharepointToolbox.Services.Export;

/// <summary>
/// Exports user access audit results to a self-contained interactive HTML report.
/// Produces a single HTML file with dual-view toggle (by-user / by-site),
/// collapsible groups, sortable columns, filter input, and risk highlighting.
/// No external CSS/JS dependencies — everything is inline.
/// </summary>
public class UserAccessHtmlExportService
{
    /// <summary>
    /// Builds a self-contained HTML string from the supplied user access entries.
    /// When <paramref name="mergePermissions"/> is true, renders a consolidated by-user
    /// report with an expandable Sites column instead of the dual by-user/by-site view.
    /// </summary>
    /// <summary>
    /// Split-aware HTML export. Single → one file.
    /// BySite/ByUser + SeparateFiles → one file per site/user.
    /// BySite/ByUser + SingleTabbed → one file with per-partition iframe tabs.
    /// </summary>
    public async Task WriteAsync(
        IReadOnlyList<UserAccessEntry> entries,
        string basePath,
        ReportSplitMode splitMode,
        HtmlSplitLayout layout,
        CancellationToken ct,
        bool mergePermissions = false,
        ReportBranding? branding = null)
    {
        if (splitMode == ReportSplitMode.Single)
        {
            await WriteAsync(entries, basePath, ct, mergePermissions, branding);
            return;
        }

        IEnumerable<(string Label, IReadOnlyList<UserAccessEntry> Entries)> partitions;
        if (splitMode == ReportSplitMode.BySite)
        {
            partitions = entries
                .GroupBy(e => (e.SiteUrl, e.SiteTitle))
                .Select(g => (
                    Label: ReportSplitHelper.DeriveSiteLabel(g.Key.SiteUrl, g.Key.SiteTitle),
                    Entries: (IReadOnlyList<UserAccessEntry>)g.ToList()));
        }
        else // ByUser
        {
            partitions = entries
                .GroupBy(e => e.UserLogin)
                .Select(g => (
                    Label: ReportSplitHelper.SanitizeFileName(g.Key),
                    Entries: (IReadOnlyList<UserAccessEntry>)g.ToList()));
        }

        var partList = partitions.ToList();
        if (layout == HtmlSplitLayout.SingleTabbed)
        {
            var parts = partList
                .Select(p => (p.Label, Html: BuildHtml(p.Entries, mergePermissions, branding)))
                .ToList();
            var title = TranslationSource.Instance["report.title.user_access"];
            var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, title);
            await ExportFileWriter.WriteHtmlAsync(basePath, tabbed, ct);
            return;
        }

        foreach (var (label, partEntries) in partList)
        {
            ct.ThrowIfCancellationRequested();
            var path = ReportSplitHelper.BuildPartitionPath(basePath, label);
            await WriteAsync(partEntries, path, ct, mergePermissions, branding);
        }
    }

    /// <summary>
    /// Builds the user-access HTML report. Default layout is a per-entry
    /// grouped-by-user table; when <paramref name="mergePermissions"/> is true
    /// entries are consolidated via <see cref="PermissionConsolidator"/> into
    /// a single-row-per-user format with a Locations column.
    /// </summary>
    public string BuildHtml(IReadOnlyList<UserAccessEntry> entries, bool mergePermissions = false, ReportBranding? branding = null)
    {
        if (mergePermissions)
        {
            var consolidated = PermissionConsolidator.Consolidate(entries);
            return BuildConsolidatedHtml(consolidated, entries, branding);
        }

        var T = TranslationSource.Instance;

        // Compute stats
        var totalAccesses    = entries.Count;
        var usersAudited     = entries.Select(e => e.UserLogin).Distinct().Count();
        var sitesScanned     = entries.Select(e => e.SiteUrl).Distinct().Count();
        var highPrivCount    = entries.Count(e => e.IsHighPrivilege);
        var externalCount    = entries.Count(e => e.IsExternalUser);

        var sb = new StringBuilder();

        // ── HTML HEAD ──────────────────────────────────────────────────────────
        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.user_access"]}</title>");
        sb.AppendLine("<style>");
        sb.AppendLine(@"
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
h1 { padding: 20px 24px 10px; font-size: 1.5rem; color: #1a1a2e; }
.stats { display: flex; gap: 16px; padding: 0 24px 16px; 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: 2rem; font-weight: 700; color: #1a1a2e; }
.stat-card .label { font-size: .8rem; color: #666; margin-top: 2px; }
/* View toggle */
.view-toggle { display: flex; gap: 8px; padding: 0 24px 12px; }
.view-toggle button { padding: 7px 18px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer; font-size: .9rem; }
.view-toggle button.active { background: #1a1a2e; color: #fff; border-color: #1a1a2e; }
/* Filter */
.filter-wrap { padding: 0 24px 12px; }
#filter { width: 320px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: .95rem; }
/* Per-user summary cards */
.user-summary { padding: 0 24px 16px; display: flex; gap: 12px; flex-wrap: wrap; }
.user-card { background: #fff; border-radius: 8px; padding: 12px 16px; min-width: 200px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
.user-card .user-name { font-weight: 600; font-size: .95rem; color: #1a1a2e; margin-bottom: 4px; }
.user-card .user-stats { font-size: .8rem; color: #555; }
.user-card.has-high-priv { border-left: 4px solid #dc2626; }
/* Table wrap */
.table-wrap { overflow-x: auto; padding: 0 24px 32px; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
th { background: #1a1a2e; color: #fff; padding: 10px 14px; text-align: left; font-size: .85rem; white-space: nowrap; cursor: pointer; user-select: none; }
th:hover { background: #2d2d4e; }
td { padding: 9px 14px; border-bottom: 1px solid #eee; vertical-align: top; font-size: .875rem; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafafa; }
/* Group headers */
.group-header { cursor: pointer; background: #f0f0f0; padding: 10px 14px; font-weight: 600; font-size: .875rem; }
.group-header:hover { background: #e8e8e8; }
.group-header td { background: inherit !important; font-weight: 600; }
/* Badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: .75rem; font-weight: 600; white-space: nowrap; }
/* Access type badges */
.access-direct    { background: #dbeafe; color: #1e40af; }
.access-group     { background: #dcfce7; color: #166534; }
.access-inherited { background: #f3f4f6; color: #374151; }
/* High privilege */
.high-priv { font-weight: 700; }
/* Guest badge */
.guest-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: .75rem; font-weight: 600; background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; margin-left: 4px; }
/* User pills */
.user-pill { display: inline-block; background: #e0e7ff; color: #3730a3; border-radius: 12px; padding: 2px 10px; font-size: .75rem; margin: 2px 3px 2px 0; white-space: nowrap; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.hidden { display: none; }
");
        sb.AppendLine("</style>");
        sb.AppendLine("</head>");

        // ── BODY ───────────────────────────────────────────────────────────────
        sb.AppendLine("<body>");
        sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
        sb.AppendLine($"<h1>{T["report.title.user_access"]}</h1>");

        // Stats cards
        sb.AppendLine("<div class=\"stats\">");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{totalAccesses}</div><div class=\"label\">{T["report.stat.total_accesses"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{usersAudited}</div><div class=\"label\">{T["report.stat.users_audited"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{sitesScanned}</div><div class=\"label\">{T["report.stat.sites_scanned"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{highPrivCount}</div><div class=\"label\">{T["report.stat.high_privilege"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{externalCount}</div><div class=\"label\">{T["report.stat.external_users"]}</div></div>");
        sb.AppendLine("</div>");

        // Per-user summary cards
        sb.AppendLine("<div class=\"user-summary\">");
        var userGroups = entries.GroupBy(e => e.UserLogin).OrderBy(g => g.Key).ToList();
        foreach (var ug in userGroups)
        {
            var uName     = HtmlEncode(ug.First().UserDisplayName);
            var uLogin    = HtmlEncode(ug.Key);
            var uTotal    = ug.Count();
            var uSites    = ug.Select(e => e.SiteUrl).Distinct().Count();
            var uHighPriv = ug.Count(e => e.IsHighPrivilege);
            var uIsExt    = ug.First().IsExternalUser;
            var cardClass = uHighPriv > 0 ? "user-card has-high-priv" : "user-card";

            sb.AppendLine($"  <div class=\"{cardClass}\">");
            sb.AppendLine($"    <div class=\"user-name\">{uName}{(uIsExt ? $" <span class=\"guest-badge\">{T["report.badge.guest"]}</span>" : "")}</div>");
            sb.AppendLine($"    <div class=\"user-stats\">{uLogin}</div>");
            sb.AppendLine($"    <div class=\"user-stats\">{uTotal} {T["report.text.accesses"]} &bull; {uSites} {T["report.text.sites_parens"]}{(uHighPriv > 0 ? $" &bull; <span style=\"color:#dc2626\">{uHighPriv} {T["report.text.high_priv"]}</span>" : "")}</div>");
            sb.AppendLine("  </div>");
        }
        sb.AppendLine("</div>");

        // View toggle buttons
        sb.AppendLine("<div class=\"view-toggle\">");
        sb.AppendLine($"  <button id=\"btn-user\" class=\"active\" onclick=\"toggleView('user')\">{T["report.view.by_user"]}</button>");
        sb.AppendLine($"  <button id=\"btn-site\" onclick=\"toggleView('site')\">{T["report.view.by_site"]}</button>");
        sb.AppendLine("</div>");

        // Filter input
        sb.AppendLine("<div class=\"filter-wrap\">");
        sb.AppendLine($"  <input type=\"text\" id=\"filter\" placeholder=\"{T["report.filter.placeholder_results"]}\" oninput=\"filterTable()\" />");
        sb.AppendLine("</div>");

        // ── BY-USER VIEW ───────────────────────────────────────────────────────
        sb.AppendLine("<div id=\"view-user\" class=\"table-wrap\">");
        sb.AppendLine("<table id=\"tbl-user\">");
        sb.AppendLine("<thead><tr>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',0)\">{T["report.col.site"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',1)\">{T["report.col.object_type"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',2)\">{T["report.col.object"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',3)\">{T["report.col.permission_level"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',4)\">{T["report.col.access_type"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('user',5)\">{T["report.col.granted_through"]}</th>");
        sb.AppendLine("</tr></thead>");
        sb.AppendLine("<tbody id=\"tbody-user\">");

        int userGroupIdx = 0;
        foreach (var ug in userGroups)
        {
            var groupId  = $"ugrp{userGroupIdx++}";
            var uName    = HtmlEncode(ug.First().UserDisplayName);
            var uIsExt   = ug.First().IsExternalUser;
            var uCount   = ug.Count();
            var guestBadge = uIsExt ? $" <span class=\"guest-badge\">{T["report.badge.guest"]}</span>" : "";

            sb.AppendLine($"<tr class=\"group-header\" onclick=\"toggleGroup('{groupId}')\">");
            sb.AppendLine($"  <td colspan=\"6\">{uName}{guestBadge} &mdash; {uCount} {T["report.text.access_es"]}</td>");
            sb.AppendLine("</tr>");

            foreach (var entry in ug)
            {
                var rowClass   = entry.IsHighPrivilege ? " class=\"high-priv\"" : "";
                var accessBadge = AccessTypeBadge(entry.AccessType);
                var highIcon   = entry.IsHighPrivilege ? " &#9888;" : "";

                var objectCell = IsRedundantObjectTitle(entry.SiteTitle, entry.ObjectTitle)
                    ? "&mdash;"
                    : HtmlEncode(entry.ObjectTitle);

                sb.AppendLine($"<tr data-group=\"{groupId}\">");
                sb.AppendLine($"  <td{rowClass}>{HtmlEncode(entry.SiteTitle)}</td>");
                sb.AppendLine($"  <td>{HtmlEncode(entry.ObjectType)}</td>");
                sb.AppendLine($"  <td>{objectCell}</td>");
                sb.AppendLine($"  <td{rowClass}>{HtmlEncode(entry.PermissionLevel)}{highIcon}</td>");
                sb.AppendLine($"  <td>{accessBadge}</td>");
                sb.AppendLine($"  <td>{PermissionHtmlFragments.BuildGrantedThroughCell(entry.GrantedThrough, entry.TargetUrl, entry.TargetLabel, entry.SharingLinkType)}</td>");
                sb.AppendLine("</tr>");
            }
        }

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

        // ── BY-SITE VIEW ───────────────────────────────────────────────────────
        sb.AppendLine("<div id=\"view-site\" class=\"table-wrap hidden\">");
        sb.AppendLine("<table id=\"tbl-site\">");
        sb.AppendLine("<thead><tr>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',0)\">{T["report.col.user"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',1)\">{T["report.col.object_type"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',2)\">{T["report.col.object"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',3)\">{T["report.col.permission_level"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',4)\">{T["report.col.access_type"]}</th>");
        sb.AppendLine($"  <th onclick=\"sortTable('site',5)\">{T["report.col.granted_through"]}</th>");
        sb.AppendLine("</tr></thead>");
        sb.AppendLine("<tbody id=\"tbody-site\">");

        var siteGroups = entries.GroupBy(e => e.SiteUrl).OrderBy(g => g.Key).ToList();
        int siteGroupIdx = 0;
        foreach (var sg in siteGroups)
        {
            var groupId   = $"sgrp{siteGroupIdx++}";
            var siteTitle = HtmlEncode(sg.First().SiteTitle);
            var sCount    = sg.Count();

            sb.AppendLine($"<tr class=\"group-header\" onclick=\"toggleGroup('{groupId}')\">");
            sb.AppendLine($"  <td colspan=\"6\">{siteTitle} &mdash; {sCount} {T["report.text.access_es"]}</td>");
            sb.AppendLine("</tr>");

            foreach (var entry in sg)
            {
                var rowClass   = entry.IsHighPrivilege ? " class=\"high-priv\"" : "";
                var accessBadge = AccessTypeBadge(entry.AccessType);
                var highIcon   = entry.IsHighPrivilege ? " &#9888;" : "";
                var guestBadge = entry.IsExternalUser ? $" <span class=\"guest-badge\">{T["report.badge.guest"]}</span>" : "";

                var objectCell = IsRedundantObjectTitle(entry.SiteTitle, entry.ObjectTitle)
                    ? "&mdash;"
                    : HtmlEncode(entry.ObjectTitle);

                sb.AppendLine($"<tr data-group=\"{groupId}\">");
                sb.AppendLine($"  <td>{HtmlEncode(entry.UserDisplayName)}{guestBadge}</td>");
                sb.AppendLine($"  <td>{HtmlEncode(entry.ObjectType)}</td>");
                sb.AppendLine($"  <td>{objectCell}</td>");
                sb.AppendLine($"  <td{rowClass}>{HtmlEncode(entry.PermissionLevel)}{highIcon}</td>");
                sb.AppendLine($"  <td>{accessBadge}</td>");
                sb.AppendLine($"  <td>{PermissionHtmlFragments.BuildGrantedThroughCell(entry.GrantedThrough, entry.TargetUrl, entry.TargetLabel, entry.SharingLinkType)}</td>");
                sb.AppendLine("</tr>");
            }
        }

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

        // ── INLINE JS ─────────────────────────────────────────────────────────
        sb.AppendLine("<script>");
        sb.AppendLine(@"
var _currentView = 'user';
var _sortState = {};

function toggleView(view) {
  _currentView = view;
  document.getElementById('view-user').classList.toggle('hidden', view !== 'user');
  document.getElementById('view-site').classList.toggle('hidden', view !== 'site');
  document.getElementById('btn-user').classList.toggle('active', view === 'user');
  document.getElementById('btn-site').classList.toggle('active', view === 'site');
  // Re-apply filter to new view
  filterTable();
}

function filterTable() {
  var input = document.getElementById('filter').value.toLowerCase();
  var tbodyId = _currentView === 'user' ? 'tbody-user' : 'tbody-site';
  var tbody = document.getElementById(tbodyId);
  var rows = tbody.querySelectorAll('tr[data-group]');
  var groupsWithVisible = {};

  rows.forEach(function(row) {
    var matches = row.textContent.toLowerCase().indexOf(input) > -1;
    row.style.display = matches ? '' : 'none';
    if (matches) {
      groupsWithVisible[row.getAttribute('data-group')] = true;
    }
  });

  // Show/hide group headers based on whether they have visible children
  var headers = tbody.querySelectorAll('tr.group-header');
  headers.forEach(function(hdr) {
    // find next sibling rows until next header
    var next = hdr.nextElementSibling;
    var groupId = null;
    while (next && next.getAttribute('data-group')) {
      groupId = next.getAttribute('data-group');
      break;
    }
    if (groupId) {
      hdr.style.display = groupsWithVisible[groupId] ? '' : 'none';
    }
  });
}

function toggleGroup(id) {
  var rows = document.querySelectorAll('tr[data-group=""' + id + '""]');
  var isHidden = rows.length > 0 && rows[0].style.display === 'none';
  rows.forEach(function(r) { r.style.display = isHidden ? '' : 'none'; });
}

function sortTable(view, col) {
  var tbodyId = view === 'user' ? 'tbody-user' : 'tbody-site';
  var tbody = document.getElementById(tbodyId);
  var key = view + '_' + col;
  var asc = _sortState[key] !== true;
  _sortState[key] = asc;

  // Sort within each group separately
  var groupHeaders = Array.from(tbody.querySelectorAll('tr.group-header'));
  groupHeaders.forEach(function(hdr) {
    var groupId = null;
    var next = hdr.nextElementSibling;
    while (next && next.getAttribute('data-group')) {
      groupId = next.getAttribute('data-group');
      break;
    }
    if (!groupId) return;
    var groupRows = Array.from(tbody.querySelectorAll('tr[data-group=""' + groupId + '""]'));
    groupRows.sort(function(a, b) {
      var aText = (a.cells[col] ? a.cells[col].textContent : '').trim().toLowerCase();
      var bText = (b.cells[col] ? b.cells[col].textContent : '').trim().toLowerCase();
      return asc ? aText.localeCompare(bText) : bText.localeCompare(aText);
    });
    // Re-insert sorted rows after the group header
    var insertAfter = hdr;
    groupRows.forEach(function(row) {
      tbody.insertBefore(row, insertAfter.nextSibling);
      insertAfter = row;
    });
  });
}
");
        sb.AppendLine("</script>");
        sb.AppendLine("</body>");
        sb.AppendLine("</html>");

        return sb.ToString();
    }

    /// <summary>
    /// Writes the HTML report to the specified file path using UTF-8 without BOM.
    /// </summary>
    public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct, bool mergePermissions = false, ReportBranding? branding = null)
    {
        var html = BuildHtml(entries, mergePermissions, branding);
        await ExportFileWriter.WriteHtmlAsync(filePath, html, ct);
    }

    /// <summary>
    /// Builds the consolidated HTML report: single by-user table with a Sites column.
    /// By-site view and view-toggle are omitted. Uses the same CSS shell as BuildHtml.
    /// </summary>
    private string BuildConsolidatedHtml(
        IReadOnlyList<ConsolidatedPermissionEntry> consolidated,
        IReadOnlyList<UserAccessEntry> entries,
        ReportBranding? branding)
    {
        var T = TranslationSource.Instance;

        // Stats computed from the original flat list for accurate counts
        var totalAccesses = entries.Count;
        var usersAudited  = entries.Select(e => e.UserLogin).Distinct().Count();
        var sitesScanned  = entries.Select(e => e.SiteUrl).Distinct().Count();
        var highPrivCount = entries.Count(e => e.IsHighPrivilege);
        var externalCount = entries.Count(e => e.IsExternalUser);

        var sb = new StringBuilder();

        // ── HTML HEAD (same as BuildHtml) ──────────────────────────────────────
        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.user_access_consolidated"]}</title>");
        sb.AppendLine("<style>");
        sb.AppendLine(@"
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
h1 { padding: 20px 24px 10px; font-size: 1.5rem; color: #1a1a2e; }
.stats { display: flex; gap: 16px; padding: 0 24px 16px; 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: 2rem; font-weight: 700; color: #1a1a2e; }
.stat-card .label { font-size: .8rem; color: #666; margin-top: 2px; }
/* View toggle */
.view-toggle { display: flex; gap: 8px; padding: 0 24px 12px; }
.view-toggle button { padding: 7px 18px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer; font-size: .9rem; }
.view-toggle button.active { background: #1a1a2e; color: #fff; border-color: #1a1a2e; }
/* Filter */
.filter-wrap { padding: 0 24px 12px; }
#filter { width: 320px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; font-size: .95rem; }
/* Per-user summary cards */
.user-summary { padding: 0 24px 16px; display: flex; gap: 12px; flex-wrap: wrap; }
.user-card { background: #fff; border-radius: 8px; padding: 12px 16px; min-width: 200px; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
.user-card .user-name { font-weight: 600; font-size: .95rem; color: #1a1a2e; margin-bottom: 4px; }
.user-card .user-stats { font-size: .8rem; color: #555; }
.user-card.has-high-priv { border-left: 4px solid #dc2626; }
/* Table wrap */
.table-wrap { overflow-x: auto; padding: 0 24px 32px; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
th { background: #1a1a2e; color: #fff; padding: 10px 14px; text-align: left; font-size: .85rem; white-space: nowrap; cursor: pointer; user-select: none; }
th:hover { background: #2d2d4e; }
td { padding: 9px 14px; border-bottom: 1px solid #eee; vertical-align: top; font-size: .875rem; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafafa; }
/* Group headers */
.group-header { cursor: pointer; background: #f0f0f0; padding: 10px 14px; font-weight: 600; font-size: .875rem; }
.group-header:hover { background: #e8e8e8; }
.group-header td { background: inherit !important; font-weight: 600; }
/* Badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: .75rem; font-weight: 600; white-space: nowrap; }
/* Access type badges */
.access-direct    { background: #dbeafe; color: #1e40af; }
.access-group     { background: #dcfce7; color: #166534; }
.access-inherited { background: #f3f4f6; color: #374151; }
/* High privilege */
.high-priv { font-weight: 700; }
/* Guest badge */
.guest-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: .75rem; font-weight: 600; background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; margin-left: 4px; }
/* User pills */
.user-pill { display: inline-block; background: #e0e7ff; color: #3730a3; border-radius: 12px; padding: 2px 10px; font-size: .75rem; margin: 2px 3px 2px 0; white-space: nowrap; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.hidden { display: none; }
");
        sb.AppendLine("</style>");
        sb.AppendLine("</head>");

        // ── BODY ───────────────────────────────────────────────────────────────
        sb.AppendLine("<body>");
        sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
        sb.AppendLine($"<h1>{T["report.title.user_access_consolidated"]}</h1>");

        // Stats cards
        sb.AppendLine("<div class=\"stats\">");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{totalAccesses}</div><div class=\"label\">{T["report.stat.total_accesses"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{usersAudited}</div><div class=\"label\">{T["report.stat.users_audited"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{sitesScanned}</div><div class=\"label\">{T["report.stat.sites_scanned"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{highPrivCount}</div><div class=\"label\">{T["report.stat.high_privilege"]}</div></div>");
        sb.AppendLine($"  <div class=\"stat-card\"><div class=\"value\">{externalCount}</div><div class=\"label\">{T["report.stat.external_users"]}</div></div>");
        sb.AppendLine("</div>");

        // Per-user summary cards (from original flat entries)
        sb.AppendLine("<div class=\"user-summary\">");
        var userGroups = entries.GroupBy(e => e.UserLogin).OrderBy(g => g.Key).ToList();
        foreach (var ug in userGroups)
        {
            var uName     = HtmlEncode(ug.First().UserDisplayName);
            var uLogin    = HtmlEncode(ug.Key);
            var uTotal    = ug.Count();
            var uSites    = ug.Select(e => e.SiteUrl).Distinct().Count();
            var uHighPriv = ug.Count(e => e.IsHighPrivilege);
            var uIsExt    = ug.First().IsExternalUser;
            var cardClass = uHighPriv > 0 ? "user-card has-high-priv" : "user-card";

            sb.AppendLine($"  <div class=\"{cardClass}\">");
            sb.AppendLine($"    <div class=\"user-name\">{uName}{(uIsExt ? $" <span class=\"guest-badge\">{T["report.badge.guest"]}</span>" : "")}</div>");
            sb.AppendLine($"    <div class=\"user-stats\">{uLogin}</div>");
            sb.AppendLine($"    <div class=\"user-stats\">{uTotal} {T["report.text.accesses"]} &bull; {uSites} {T["report.text.sites_parens"]}{(uHighPriv > 0 ? $" &bull; <span style=\"color:#dc2626\">{uHighPriv} {T["report.text.high_priv"]}</span>" : "")}</div>");
            sb.AppendLine("  </div>");
        }
        sb.AppendLine("</div>");

        // View toggle — only By User (By Site is suppressed for consolidated view)
        sb.AppendLine("<div class=\"view-toggle\">");
        sb.AppendLine($"  <button id=\"btn-user\" class=\"active\">{T["report.view.by_user"]}</button>");
        sb.AppendLine("</div>");

        // Filter input
        sb.AppendLine("<div class=\"filter-wrap\">");
        sb.AppendLine($"  <input type=\"text\" id=\"filter\" placeholder=\"{T["report.filter.placeholder_results"]}\" oninput=\"filterTable()\" />");
        sb.AppendLine("</div>");

        // ── CONSOLIDATED BY-USER TABLE ────────────────────────────────────────
        sb.AppendLine("<div id=\"view-user\" class=\"table-wrap\">");
        sb.AppendLine("<table id=\"tbl-user\">");
        sb.AppendLine("<thead><tr>");
        sb.AppendLine($"  <th>{T["report.col.user"]}</th>");
        sb.AppendLine($"  <th>{T["report.col.permission_level"]}</th>");
        sb.AppendLine($"  <th>{T["report.col.access_type"]}</th>");
        sb.AppendLine($"  <th>{T["report.col.granted_through"]}</th>");
        sb.AppendLine($"  <th>{T["report.col.sites"]}</th>");
        sb.AppendLine("</tr></thead>");
        sb.AppendLine("<tbody id=\"tbody-user\">");

        // Group consolidated entries by UserLogin for group headers
        var consolidatedByUser = consolidated
            .GroupBy(c => c.UserLogin)
            .OrderBy(g => g.Key)
            .ToList();

        int grpIdx = 0;
        int locIdx = 0;  // SEPARATE counter for location group IDs — Pitfall 2

        foreach (var cug in consolidatedByUser)
        {
            var groupId    = $"ugrp{grpIdx++}";
            var cuName     = HtmlEncode(cug.First().UserDisplayName);
            var cuIsExt    = cug.First().IsExternalUser;
            var cuCount    = cug.Count();
            var guestBadge = cuIsExt ? $" <span class=\"guest-badge\">{T["report.badge.guest"]}</span>" : "";

            sb.AppendLine($"<tr class=\"group-header\" onclick=\"toggleGroup('{groupId}')\">");
            sb.AppendLine($"  <td colspan=\"5\">{cuName}{guestBadge} &mdash; {cuCount} {T["report.text.permissions_parens"]}</td>");
            sb.AppendLine("</tr>");

            foreach (var entry in cug)
            {
                var rowClass    = entry.IsHighPrivilege ? " class=\"high-priv\"" : "";
                var accessBadge = AccessTypeBadge(entry.AccessType);
                var highIcon    = entry.IsHighPrivilege ? " &#9888;" : "";

                sb.AppendLine($"<tr data-group=\"{groupId}\">");
                sb.AppendLine($"  <td{rowClass}>{HtmlEncode(entry.UserDisplayName)}{guestBadge}</td>");
                sb.AppendLine($"  <td{rowClass}>{HtmlEncode(entry.PermissionLevel)}{highIcon}</td>");
                sb.AppendLine($"  <td>{accessBadge}</td>");
                sb.AppendLine($"  <td>{PermissionHtmlFragments.BuildGrantedThroughCell(entry.GrantedThrough, entry.TargetUrl, entry.TargetLabel, entry.SharingLinkType)}</td>");

                if (entry.LocationCount == 1)
                {
                    // Single location — inline site title + object title
                    var loc0 = entry.Locations[0];
                    var locLabel = IsRedundantObjectTitle(loc0.SiteTitle, loc0.ObjectTitle)
                        ? HtmlEncode(loc0.SiteTitle)
                        : $"{HtmlEncode(loc0.SiteTitle)} &rsaquo; {HtmlEncode(loc0.ObjectTitle)}";
                    sb.AppendLine($"  <td>{locLabel}</td>");
                    sb.AppendLine("</tr>");
                }
                else
                {
                    // Multiple locations — expandable badge
                    var currentLocId = $"loc{locIdx++}";
                    sb.AppendLine($"  <td><span class=\"badge\" onclick=\"toggleGroup('{currentLocId}')\" style=\"cursor:pointer\">{entry.LocationCount} {TranslationSource.Instance["report.text.sites_unit"]}</span></td>");
                    sb.AppendLine("</tr>");

                    // Hidden sub-rows — one per location
                    foreach (var loc in entry.Locations)
                    {
                        var subLabel = IsRedundantObjectTitle(loc.SiteTitle, loc.ObjectTitle)
                            ? $"<a href=\"{HtmlEncode(loc.SiteUrl)}\">{HtmlEncode(loc.SiteTitle)}</a>"
                            : $"<a href=\"{HtmlEncode(loc.SiteUrl)}\">{HtmlEncode(loc.SiteTitle)}</a> &rsaquo; {HtmlEncode(loc.ObjectTitle)}";
                        sb.AppendLine($"<tr data-group=\"{currentLocId}\" style=\"display:none\">");
                        sb.AppendLine($"  <td colspan=\"5\" style=\"padding-left:2em\">");
                        sb.AppendLine($"    {subLabel}");
                        sb.AppendLine("  </td>");
                        sb.AppendLine("</tr>");
                    }
                }
            }
        }

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

        // ── INLINE JS ─────────────────────────────────────────────────────────
        sb.AppendLine("<script>");
        sb.AppendLine(@"
var _sortState = {};

function filterTable() {
  var input = document.getElementById('filter').value.toLowerCase();
  var tbody = document.getElementById('tbody-user');
  var rows = tbody.querySelectorAll('tr[data-group]');
  var groupsWithVisible = {};

  rows.forEach(function(row) {
    // Skip hidden sub-rows that belong to location groups (loc...)
    if (row.getAttribute('data-group').indexOf('loc') === 0) return;
    var matches = row.textContent.toLowerCase().indexOf(input) > -1;
    row.style.display = matches ? '' : 'none';
    if (matches) {
      groupsWithVisible[row.getAttribute('data-group')] = true;
    }
  });

  // Show/hide group headers based on whether they have visible children
  var headers = tbody.querySelectorAll('tr.group-header');
  headers.forEach(function(hdr) {
    var next = hdr.nextElementSibling;
    var groupId = null;
    while (next && next.getAttribute('data-group')) {
      groupId = next.getAttribute('data-group');
      break;
    }
    if (groupId) {
      hdr.style.display = groupsWithVisible[groupId] ? '' : 'none';
    }
  });
}

function toggleGroup(id) {
  var rows = document.querySelectorAll('tr[data-group=""' + id + '""]');
  var isHidden = rows.length > 0 && rows[0].style.display === 'none';
  rows.forEach(function(r) { r.style.display = isHidden ? '' : 'none'; });
}
");
        sb.AppendLine("</script>");
        sb.AppendLine("</body>");
        sb.AppendLine("</html>");

        return sb.ToString();
    }

    /// <summary>Returns a colored badge span for the given access type.</summary>
    private static string AccessTypeBadge(AccessType accessType)
    {
        var T = TranslationSource.Instance;
        return accessType switch
        {
            AccessType.Direct    => $"<span class=\"badge access-direct\">{T["report.badge.direct"]}</span>",
            AccessType.Group     => $"<span class=\"badge access-group\">{T["report.badge.group"]}</span>",
            AccessType.Inherited => $"<span class=\"badge access-inherited\">{T["report.badge.inherited"]}</span>",
            _                    => $"<span class=\"badge\">{HtmlEncode(accessType.ToString())}</span>"
        };
    }

    /// <summary>
    /// Returns true when the ObjectTitle adds no information beyond the SiteTitle:
    /// empty, identical (case-insensitive), or one is a whitespace-trimmed duplicate
    /// of the other. Used to collapse "All Company &rsaquo; All Company" to "All Company".
    /// </summary>
    private static bool IsRedundantObjectTitle(string siteTitle, string objectTitle)
    {
        if (string.IsNullOrWhiteSpace(objectTitle)) return true;
        return string.Equals(
            (siteTitle ?? string.Empty).Trim(),
            objectTitle.Trim(),
            StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Minimal HTML encoding for text content and attribute values.</summary>
    private static string HtmlEncode(string value)
    {
        if (string.IsNullOrEmpty(value)) return string.Empty;
        return value
            .Replace("&",  "&amp;")
            .Replace("<",  "&lt;")
            .Replace(">",  "&gt;")
            .Replace("\"", "&quot;")
            .Replace("'",  "&#39;");
    }
}
