using System.Text;

namespace Mbproxy.Admin;

/// <summary>
/// Renders a <see cref="StatusResponse"/> as a self-contained HTML page.
///
/// <para>Constraints (see <c>docs/Operations/StatusPage.md</c>):</para>
/// <list type="bullet">
///   <item>No external assets (CSS/JS/fonts/favicons) — firewalled networks only.</item>
///   <item><c>&lt;meta http-equiv="refresh" content="5"&gt;</c> for auto-refresh.</item>
///   <item>Page weight ≤ 50 KB for a 54-PLC fleet.</item>
///   <item>Listener state colour-coded: bound=green, recovering=orange, stopped=grey.</item>
///   <item>Connected clients rendered as compact <c>[remote (n PDUs)]</c> list (not nested table).</item>
/// </list>
/// </summary>
internal static class StatusHtmlRenderer
{
    private const string Css = """
        body{font-family:monospace;font-size:13px;margin:1em}
        h1{font-size:1.1em;margin-bottom:.3em}
        .meta{color:#555;margin-bottom:.8em;font-size:12px}
        table{border-collapse:collapse;width:100%}
        th,td{border:1px solid #ccc;padding:3px 6px;white-space:nowrap}
        th{background:#f0f0f0;text-align:left}
        tr:nth-child(even)td{background:#fafafa}
        .bound{color:green;font-weight:bold}
        .recovering{color:darkorange;font-weight:bold}
        .stopped{color:grey}
        .err{font-size:11px;color:#a00}
        .clients{font-size:11px;color:#333}
        """;

    /// <summary>
    /// Renders the status page as a complete HTML document string.
    /// May allocate; intended for the status-page read path only.
    /// </summary>
    public static string Render(StatusResponse status)
    {
        var sb = new StringBuilder(4096);

        sb.Append("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">");
        sb.Append("<meta http-equiv=\"refresh\" content=\"5\">");
        sb.Append("<title>mbproxy status</title>");
        sb.Append("<style>").Append(Css).Append("</style>");
        sb.Append("</head><body>");

        // ── Header ────────────────────────────────────────────────────────────
        sb.Append("<h1>mbproxy status</h1>");
        sb.Append("<div class=\"meta\">");
        sb.Append("Version: ").Append(HtmlEncode(status.Service.Version));
        sb.Append(" &nbsp;|&nbsp; Uptime: ").Append(FormatUptime(status.Service.UptimeSeconds));
        sb.Append(" &nbsp;|&nbsp; Listeners: ")
          .Append(status.Listeners.Bound).Append('/').Append(status.Listeners.Configured)
          .Append(" bound");
        if (status.Service.ConfigLastReloadUtc.HasValue)
        {
            sb.Append(" &nbsp;|&nbsp; Last reload: ")
              .Append(HtmlEncode(status.Service.ConfigLastReloadUtc.Value.ToString("yyyy-MM-dd HH:mm:ss") + "Z"));
        }
        sb.Append(" &nbsp;|&nbsp; Reloads: ").Append(status.Service.ConfigReloadCount);
        if (status.Service.ConfigReloadRejectedCount > 0)
            sb.Append(" (").Append(status.Service.ConfigReloadRejectedCount).Append(" rejected)");
        sb.Append("</div>");

        // ── PLC table ─────────────────────────────────────────────────────────
        if (status.Plcs.Count == 0)
        {
            sb.Append("<p><em>No PLCs configured.</em></p>");
        }
        else
        {
            sb.Append("<table>");
            sb.Append("<thead><tr>");
            sb.Append("<th>Name</th><th>Host</th><th>Port</th><th>State</th>");
            sb.Append("<th>Clients</th><th>PDUs fwd</th><th>FC03</th><th>FC04</th>");
            sb.Append("<th>FC06</th><th>FC16</th><th>FC?</th><th>BCD slots</th>");
            sb.Append("<th>Partial BCD</th><th>Invalid BCD</th><th>Ex 01</th><th>Ex 02</th><th>Ex 03</th><th>Ex 04</th><th>Ex ?</th>");
            sb.Append("<th>RTT ms</th><th>Bytes in</th><th>Bytes out</th>");
            // Multiplexer telemetry columns.
            sb.Append("<th>In-flight</th><th>Max in-flight</th><th>TxId wraps</th>");
            sb.Append("<th>Cascades</th><th>Queue</th>");
            // Coalescing column. Single cell carries hit / (hit + miss) ratio as a
            // percentage plus the raw hit count for context. Kept compact (one cell) to
            // stay under the 50 KB page-weight budget.
            sb.Append("<th>Coal</th>");
            // Cache column. Single cell carries hit-ratio percent plus raw hit count;
            // an em-dash when no cache-eligible reads have occurred. Page-weight budget
            // assertion stays under 50 KB for the 54-PLC fleet.
            sb.Append("<th>Cache</th>");
            sb.Append("</tr></thead><tbody>");

            foreach (var plc in status.Plcs)
            {
                sb.Append("<tr>");
                sb.Append("<td>").Append(HtmlEncode(plc.Name)).Append("</td>");
                sb.Append("<td>").Append(HtmlEncode(plc.Host)).Append("</td>");
                sb.Append("<td>").Append(plc.ListenPort).Append("</td>");

                // State cell with colour coding
                string stateClass = plc.Listener.State switch
                {
                    "bound"      => "bound",
                    "recovering" => "recovering",
                    _            => "stopped",
                };
                sb.Append("<td><span class=\"").Append(stateClass).Append("\">")
                  .Append(HtmlEncode(plc.Listener.State)).Append("</span>");
                if (plc.Listener.State == "recovering" && plc.Listener.LastBindError is { } err)
                {
                    sb.Append("<br><span class=\"err\">")
                      .Append(HtmlEncode(err))
                      .Append(" (attempt ").Append(plc.Listener.RecoveryAttempts).Append(")")
                      .Append("</span>");
                }
                sb.Append("</td>");

                // Connected clients
                sb.Append("<td><span class=\"clients\">");
                sb.Append(plc.Clients.Connected);
                if (plc.Clients.RemoteEndpoints.Count > 0)
                {
                    sb.Append("<br>");
                    bool first = true;
                    foreach (var c in plc.Clients.RemoteEndpoints)
                    {
                        if (!first) sb.Append(", ");
                        sb.Append(HtmlEncode(c.Remote))
                          .Append(" (").Append(c.PdusForwarded).Append(')');
                        first = false;
                    }
                }
                sb.Append("</span></td>");

                // Counter cells
                sb.Append("<td>").Append(plc.Pdus.Forwarded).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.ByFc.Fc03).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.ByFc.Fc04).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.ByFc.Fc06).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.ByFc.Fc16).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.ByFc.Other).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.RewrittenSlots).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.PartialBcdWarnings).Append("</td>");
                sb.Append("<td>").Append(plc.Pdus.InvalidBcdWarnings).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.ExceptionsByCode.Code01).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.ExceptionsByCode.Code02).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.ExceptionsByCode.Code03).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.ExceptionsByCode.Code04).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.ExceptionsByCode.CodeOther).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.LastRoundTripMs.ToString("F1")).Append("</td>");
                sb.Append("<td>").Append(plc.Bytes.UpstreamIn).Append("</td>");
                sb.Append("<td>").Append(plc.Bytes.UpstreamOut).Append("</td>");
                // Multiplexer telemetry cells.
                sb.Append("<td>").Append(plc.Backend.InFlight).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.MaxInFlight).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.TxIdWraps).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.DisconnectCascades).Append("</td>");
                sb.Append("<td>").Append(plc.Backend.QueueDepth).Append("</td>");
                // Coalescing ratio cell — "<pct>% (<hit>)". When no coalesced reads have
                // been seen, render an em-dash to keep the cell narrow.
                long coalHit  = plc.Backend.CoalescedHitCount;
                long coalMiss = plc.Backend.CoalescedMissCount;
                sb.Append("<td>");
                if (coalHit + coalMiss == 0)
                {
                    sb.Append("&mdash;");
                }
                else
                {
                    int pct = (int)Math.Round(100.0 * coalHit / (coalHit + coalMiss));
                    sb.Append(pct).Append("% (").Append(coalHit).Append(')');
                }
                sb.Append("</td>");
                // Cache ratio cell — same pattern as coalescing.
                long cacheHit  = plc.Backend.CacheHitCount;
                long cacheMiss = plc.Backend.CacheMissCount;
                sb.Append("<td>");
                if (cacheHit + cacheMiss == 0)
                {
                    sb.Append("&mdash;");
                }
                else
                {
                    int pct = (int)Math.Round(100.0 * cacheHit / (cacheHit + cacheMiss));
                    sb.Append(pct).Append("% (").Append(cacheHit).Append(')');
                }
                sb.Append("</td>");
                sb.Append("</tr>");
            }

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

        sb.Append("</body></html>");
        return sb.ToString();
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private static string FormatUptime(long seconds)
    {
        var ts = TimeSpan.FromSeconds(seconds);
        if (ts.TotalHours >= 1)
            return $"{(int)ts.TotalHours}h {ts.Minutes:D2}m {ts.Seconds:D2}s";
        if (ts.TotalMinutes >= 1)
            return $"{ts.Minutes}m {ts.Seconds:D2}s";
        return $"{seconds}s";
    }

    private static string HtmlEncode(string s)
    {
        // Fast path: no special chars.
        if (!ContainsHtmlSpecial(s)) return s;

        return s
            .Replace("&", "&amp;")
            .Replace("<", "&lt;")
            .Replace(">", "&gt;")
            .Replace("\"", "&quot;");
    }

    private static bool ContainsHtmlSpecial(string s)
    {
        foreach (char c in s)
            if (c is '&' or '<' or '>' or '"') return true;
        return false;
    }
}
