// Copyright 2026 Exochain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//! Live status dashboard — single-page HTML served at `/`.
//!
//! A real-time dashboard that polls the node's own API endpoints
//! (`/health`, `/api/v1/governance/status`, `/metrics`) and renders
//! live consensus state, validator set, and network topology.
//!
//! No JavaScript frameworks — just vanilla HTML/CSS/JS with a 3-second
//! refresh cycle.

use axum::{Router, response::Html, routing::get};

/// Build a router containing the dashboard root route.
pub fn dashboard_router() -> Router {
    Router::new().route("/", get(handle_dashboard))
}

async fn handle_dashboard() -> Html<&'static str> {
    Html(DASHBOARD_HTML)
}

const DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>exochain</title>
<style>
  :root {
    --bg: #0a0e17;
    --surface: #111827;
    --border: #1e293b;
    --text: #e2e8f0;
    --text-dim: #94a3b8;
    --accent: #38bdf8;
    --accent-dim: #0c4a6e;
    --green: #22c55e;
    --green-dim: #064e3b;
    --amber: #f59e0b;
    --amber-dim: #78350f;
    --red: #ef4444;
    --red-dim: #7f1d1d;
    --mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
    --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }

  /* Header */
  header {
    border-bottom: 1px solid var(--border);
    padding: 1.25rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem;
  }

  .logo {
    font-family: var(--mono);
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--accent);
  }

  .logo span {
    color: var(--text-dim);
    font-weight: 400;
  }

  .status-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.75rem;
    border-radius: 9999px;
    font-size: 0.8125rem;
    font-weight: 500;
    font-family: var(--mono);
  }

  .status-badge.ok {
    background: var(--green-dim);
    color: var(--green);
  }

  .status-badge.degraded {
    background: var(--amber-dim);
    color: var(--amber);
  }

  .status-badge.offline {
    background: var(--red-dim);
    color: var(--red);
  }

  .pulse {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    animation: pulse 2s ease-in-out infinite;
  }

  .ok .pulse { background: var(--green); }
  .degraded .pulse { background: var(--amber); }
  .offline .pulse { background: var(--red); }

  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }

  /* Main grid */
  main {
    flex: 1;
    padding: 2rem;
    max-width: 1200px;
    margin: 0 auto;
    width: 100%;
  }

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.25rem;
    margin-bottom: 1.5rem;
  }

  /* Metric cards */
  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.25rem 1.5rem;
    transition: border-color 0.2s;
  }

  .card:hover {
    border-color: var(--accent-dim);
  }

  .card-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    margin-bottom: 0.5rem;
  }

  .card-value {
    font-family: var(--mono);
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--text);
  }

  .card-value.accent { color: var(--accent); }

  .card-sub {
    font-size: 0.8125rem;
    color: var(--text-dim);
    margin-top: 0.375rem;
    font-family: var(--mono);
  }

  /* Validator list */
  .validators-section {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
  }

  .section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
  }

  .section-title {
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim);
  }

  .quorum-badge {
    font-family: var(--mono);
    font-size: 0.75rem;
    padding: 0.25rem 0.625rem;
    border-radius: 6px;
    background: var(--accent-dim);
    color: var(--accent);
  }

  .validator-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .validator-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.875rem;
    background: var(--bg);
    border-radius: 8px;
    font-family: var(--mono);
    font-size: 0.8125rem;
    color: var(--text);
    border: 1px solid transparent;
    transition: border-color 0.2s;
  }

  .validator-item:hover {
    border-color: var(--border);
  }

  .validator-item.self {
    border-color: var(--accent-dim);
  }

  .validator-item-placeholder {
    color: var(--text-dim);
  }

  .validator-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--green);
    flex-shrink: 0;
  }

  .validator-item.self .validator-dot {
    background: var(--accent);
  }

  .self-tag {
    font-size: 0.6875rem;
    padding: 0.125rem 0.5rem;
    border-radius: 4px;
    background: var(--accent-dim);
    color: var(--accent);
    margin-left: auto;
    flex-shrink: 0;
  }

  /* Activity log */
  .activity-section {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 1.5rem;
  }

  .activity-log {
    font-family: var(--mono);
    font-size: 0.75rem;
    color: var(--text-dim);
    max-height: 200px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
  }

  .log-entry {
    padding: 0.25rem 0;
    border-bottom: 1px solid var(--border);
    display: flex;
    gap: 0.75rem;
  }

  .log-entry:last-child { border-bottom: none; }

  .log-time { color: var(--text-dim); white-space: nowrap; }
  .log-msg { color: var(--text); }
  .log-msg.advance { color: var(--accent); }
  .log-msg.commit { color: var(--green); }
  .log-msg.warn { color: var(--amber); }

  /* Footer */
  footer {
    border-top: 1px solid var(--border);
    padding: 1rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
    font-size: 0.75rem;
    color: var(--text-dim);
    font-family: var(--mono);
  }

  footer a {
    color: var(--accent);
    text-decoration: none;
  }

  footer a:hover { text-decoration: underline; }

  .update-indicator {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
  }

  .update-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    opacity: 0;
    transition: opacity 0.15s;
  }

  .update-dot.flash { opacity: 1; }

  .dashboard-meta {
    font-size: 0.75rem;
    color: var(--text-dim);
    font-family: var(--mono);
  }

  .dashboard-node-did {
    user-select: all;
  }

  /* Responsive */
  @media (max-width: 640px) {
    header { padding: 1rem; }
    main { padding: 1rem; }
    .card-value { font-size: 1.5rem; }
    .validator-item { font-size: 0.6875rem; }
    footer { padding: 0.75rem 1rem; }
  }
</style>
</head>
<body>
  <header>
    <div class="logo">exochain <span>node</span></div>
    <div id="status-badge" class="status-badge offline">
      <div class="pulse"></div>
      <span id="status-text">connecting...</span>
    </div>
  </header>

  <main>
    <div class="grid">
      <div class="card">
        <div class="card-label">Consensus Round</div>
        <div class="card-value accent" id="round">—</div>
        <div class="card-sub" id="round-rate"></div>
      </div>

      <div class="card">
        <div class="card-label">Committed Height</div>
        <div class="card-value" id="height">—</div>
        <div class="card-sub" id="dag-nodes"></div>
      </div>

      <div class="card">
        <div class="card-label">Connected Peers</div>
        <div class="card-value" id="peers">—</div>
        <div class="card-sub" id="sync-status"></div>
      </div>

      <div class="card">
        <div class="card-label">Uptime</div>
        <div class="card-value" id="uptime">—</div>
        <div class="card-sub" id="version"></div>
      </div>
    </div>

    <div class="validators-section">
      <div class="section-header">
        <span class="section-title">Validator Set</span>
        <span class="quorum-badge" id="quorum-info">—</span>
      </div>
      <ul class="validator-list" id="validator-list">
        <li class="validator-item validator-item-placeholder"><span>loading...</span></li>
      </ul>
    </div>

    <div class="activity-section">
      <div class="section-header">
        <span class="section-title">Activity</span>
        <span class="dashboard-meta" id="update-count"></span>
      </div>
      <div class="activity-log" id="activity-log"></div>
    </div>
  </main>

  <footer>
    <div>
      <span class="dashboard-node-did" id="node-did">—</span>
    </div>
    <div class="update-indicator">
      <div class="update-dot" id="update-dot"></div>
      <span>refreshes every 3s</span>
      &middot;
      <a href="/health">/health</a>
      &middot;
      <a href="/ready">/ready</a>
      &middot;
      <a href="/metrics">/metrics</a>
      &middot;
      <a href="/api/v1/governance/status">/api</a>
      &middot;
      <a href="https://github.com/exochain/exochain" target="_blank" rel="noopener">github</a>
    </div>
  </footer>

<script>
(function() {
  'use strict';

  // State
  let prevRound = null;
  let prevHeight = null;
  let nodeDid = null;
  let pollCount = 0;
  const activityLog = [];
  const MAX_LOG = 50;

  function formatUptime(secs) {
    const d = Math.floor(secs / 86400);
    const h = Math.floor((secs % 86400) / 3600);
    const m = Math.floor((secs % 3600) / 60);
    const s = secs % 60;
    if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
    if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
    if (m > 0) return m + 'm ' + s + 's';
    return s + 's';
  }

  function truncateDid(did) {
    if (!did || did.length < 24) return did;
    return did.slice(0, 16) + '...' + did.slice(-8);
  }

  function addLog(msg, cls) {
    const now = new Date();
    const ts = now.toLocaleTimeString('en-US', { hour12: false });
    activityLog.unshift({ time: ts, msg: msg, cls: cls || '' });
    if (activityLog.length > MAX_LOG) activityLog.pop();
    renderLog();
  }

  function renderLog() {
    const el = document.getElementById('activity-log');
    const entries = activityLog.map(function(e) {
      const row = document.createElement('div');
      row.className = 'log-entry';

      const time = document.createElement('span');
      time.className = 'log-time';
      time.textContent = String(e.time || '');
      row.appendChild(time);

      const msg = document.createElement('span');
      msg.className = 'log-msg';
      if (e.cls === 'advance' || e.cls === 'commit' || e.cls === 'warn') {
        msg.classList.add(e.cls);
      }
      msg.textContent = String(e.msg || '');
      row.appendChild(msg);

      return row;
    });
    el.replaceChildren(...entries);
  }

  function flashDot() {
    const dot = document.getElementById('update-dot');
    dot.classList.add('flash');
    setTimeout(function() { dot.classList.remove('flash'); }, 300);
  }

  function setStatus(status, text) {
    const badge = document.getElementById('status-badge');
    const textEl = document.getElementById('status-text');
    badge.className = 'status-badge ' + status;
    textEl.textContent = text;
  }

  async function fetchJSON(url) {
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(resp.status + '');
    return resp.json();
  }

  async function fetchText(url) {
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(resp.status + '');
    return resp.text();
  }

  function parseMetrics(text) {
    const metrics = {};
    text.split('\n').forEach(function(line) {
      if (line.startsWith('#') || line.trim() === '') return;
      const parts = line.split(' ');
      if (parts.length >= 2) {
        metrics[parts[0]] = parseFloat(parts[1]);
      }
    });
    return metrics;
  }

  async function poll() {
    try {
      const [health, gov, metricsText] = await Promise.all([
        fetchJSON('/health'),
        fetchJSON('/api/v1/governance/status'),
        fetchText('/metrics')
      ]);

      const m = parseMetrics(metricsText);
      pollCount++;

      // Status badge
      if (health.status === 'ok') {
        const peers = m.exochain_peer_count || 0;
        if (peers === 0 && gov.validator_count <= 1) {
          setStatus('ok', 'seed node');
        } else if (peers === 0) {
          setStatus('degraded', 'no peers');
        } else {
          setStatus('ok', 'operational');
        }
      } else {
        setStatus('degraded', health.status);
      }

      // Cards
      document.getElementById('round').textContent = gov.consensus_round.toLocaleString();
      document.getElementById('height').textContent = gov.committed_height.toLocaleString();
      document.getElementById('peers').textContent = (m.exochain_peer_count || 0).toLocaleString();
      document.getElementById('uptime').textContent = formatUptime(health.uptime_seconds);
      document.getElementById('version').textContent = 'v' + health.version;

      // Sub-info
      const dagNodes = m.exochain_dag_nodes_total || 0;
      document.getElementById('dag-nodes').textContent = dagNodes + ' DAG nodes';

      const syncing = m.exochain_sync_in_progress || 0;
      document.getElementById('sync-status').textContent = syncing ? 'syncing...' : 'idle';

      // Round rate
      if (prevRound !== null && gov.consensus_round > prevRound) {
        const delta = gov.consensus_round - prevRound;
        document.getElementById('round-rate').textContent = '+' + delta + ' / 3s';
        if (delta > 0) addLog('round advanced to ' + gov.consensus_round.toLocaleString(), 'advance');
      } else if (prevRound !== null) {
        document.getElementById('round-rate').textContent = 'steady';
      }

      // Height changes
      if (prevHeight !== null && gov.committed_height > prevHeight) {
        addLog('committed height ' + gov.committed_height.toLocaleString(), 'commit');
      }

      prevRound = gov.consensus_round;
      prevHeight = gov.committed_height;

      // Node DID
      if (!nodeDid && gov.validators && gov.validators.length > 0 && gov.is_validator) {
        nodeDid = gov.validators.find(function() { return true; });
        document.getElementById('node-did').textContent = nodeDid || '—';
      } else if (!nodeDid) {
        document.getElementById('node-did').textContent = 'observer';
      }

      // Validators
      const vList = document.getElementById('validator-list');
      const quorum = Math.floor((gov.validator_count * 2) / 3) + 1;
      document.getElementById('quorum-info').textContent =
        gov.validator_count + ' validators / quorum ' + quorum;

      vList.replaceChildren();
      if (gov.validators && gov.validators.length > 0) {
        const validatorItems = gov.validators.map(function(did, i) {
          const isSelf = gov.is_validator && i === 0 && gov.validator_count === 1;
          const item = document.createElement('li');
          item.className = isSelf ? 'validator-item self' : 'validator-item';

          const dot = document.createElement('span');
          dot.className = 'validator-dot';
          item.appendChild(dot);

          const didLabel = document.createElement('span');
          didLabel.title = String(did || '');
          didLabel.textContent = truncateDid(String(did || ''));
          item.appendChild(didLabel);

          if (isSelf) {
            const selfTag = document.createElement('span');
            selfTag.className = 'self-tag';
            selfTag.textContent = 'this node';
            item.appendChild(selfTag);
          }

          return item;
        });
        vList.replaceChildren(...validatorItems);
      }

      // First successful poll
      if (pollCount === 1) {
        addLog('dashboard connected to node', '');
        addLog('version ' + health.version + ' / uptime ' + formatUptime(health.uptime_seconds), '');
        if (gov.is_validator) {
          addLog('node is a consensus validator', 'advance');
        } else {
          addLog('node is an observer', '');
        }
      }

      document.getElementById('update-count').textContent = 'poll #' + pollCount;
      flashDot();

    } catch (err) {
      setStatus('offline', 'unreachable');
      addLog('fetch failed: ' + err.message, 'warn');
      flashDot();
    }
  }

  // Initial poll, then every 3 seconds
  poll();
  setInterval(poll, 3000);
})();
</script>
</body>
</html>
"##;

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use axum::{body::Body, http::Request};
    use tower::ServiceExt;

    use super::*;

    #[tokio::test]
    async fn dashboard_returns_html() {
        let app = dashboard_router();
        let resp = app
            .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(resp.status(), 200);
        let ct = resp
            .headers()
            .get("content-type")
            .unwrap()
            .to_str()
            .unwrap();
        assert!(ct.contains("text/html"));

        let body = axum::body::to_bytes(resp.into_body(), 1 << 20)
            .await
            .unwrap();
        let html = std::str::from_utf8(&body).unwrap();
        assert!(html.contains("<!DOCTYPE html>"));
        assert!(html.contains("exochain"));
        assert!(html.contains("/api/v1/governance/status"));
    }

    #[tokio::test]
    async fn dashboard_html_contains_all_endpoints() {
        let app = dashboard_router();
        let resp = app
            .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
            .await
            .unwrap();

        let body = axum::body::to_bytes(resp.into_body(), 1 << 20)
            .await
            .unwrap();
        let html = std::str::from_utf8(&body).unwrap();
        assert!(html.contains("/health"));
        assert!(html.contains("/metrics"));
        assert!(html.contains("/api/v1/governance/status"));
    }

    #[test]
    fn dashboard_validator_list_does_not_inject_dids_as_html() {
        assert!(
            DASHBOARD_HTML.contains("textContent"),
            "dashboard must write validator DIDs through textContent"
        );
        assert!(
            !DASHBOARD_HTML.contains("vList.innerHTML = gov.validators.map"),
            "validator DID data must not be interpolated into innerHTML"
        );
    }

    #[test]
    fn dashboard_activity_log_does_not_render_dynamic_html() {
        assert!(
            !DASHBOARD_HTML.contains("innerHTML"),
            "dashboard must not render dynamic status, version, error, or DID data through innerHTML"
        );
    }

    #[test]
    fn dashboard_html_uses_no_inline_style_attributes() {
        assert!(
            !DASHBOARD_HTML.contains("style=\""),
            "dashboard HTML should not rely on inline style attributes for CSP safety"
        );
    }
}
