// ── Canonical UI glossary (spec 017 + audit 4.2/4.3/4.4) ──────────────
// Single source of truth for user-facing UI term definitions. The
// audit caught terminology drift across views (incident / event /
// attacker / threat / alert / break-in / flagged / suspicious used
// interchangeably). The fix is: every operator-visible mention of
// these terms attaches a `title=` tooltip rendered from this map,
// so the canonical definition follows the term wherever it lands.
var GLOSSARY = {
  threat:           'A detected security event that may pose risk.',
  incident:         'A threat recorded by the backend (same concept, internal name).',
  unresolved:       'A threat that has not been handled automatically and awaits your review.',
  contained:        'A threat that has been blocked, killed, monitored, or suspended automatically.',
  open:             'A threat with no containment action taken yet.',
  resolved:         'A threat that has been closed — either contained or dismissed.',
  noise:            'A low-signal detection the system chose not to act on.',
  // Operator-visible severity labels.
  severity:         'How dangerous the detection signal is. Critical / High / Medium / Low / Info, ranked highest to lowest.',
  critical:         'Highest-severity detection. Active compromise indicators (kill chain, reverse shell, ransomware).',
  high:             'Imminent risk. Requires action soon (port scan from known-bad IP, suspicious sudo, persistence attempt).',
  medium:           'Suspicious but not immediately dangerous. Worth reviewing but rarely emergency.',
  low:              'Background noise that fits the threat model. Logged for forensics, no action expected.',
  info:             'Informational signal, kept for context.',
  // Confidence + risk score (separate axes from severity).
  confidence:       'How sure the AI is about its decision. High / Medium / Low. Independent of severity.',
  risk_score:       'Composite 0-100 risk for an attacker (severity history, recurrence, IOC overlap, geo). Independent of any single incident severity.',
  // Outcome buckets the dashboard groups attackers by.
  outcome:          'What the system did about a threat. One of: blocked / observing / honeypot / needs attention / dismissed / allowlisted.',
  blocked:          'A firewall rule was installed (UFW / iptables / nftables / XDP / pf) so the attacker IP cannot reach the host. Reverts after the configured TTL.',
  observing:        'Watching the attacker without active blocking. Used when severity is below the action threshold or the operator wants more context first.',
  honeypot:         'Attacker is being redirected to a fake-shell honeypot listener. Activity is recorded as forensic intel.',
  needs_attention:  'AI declined to act and escalated to the operator. The system is waiting for your judgement.',
  dismissed:        'The system or operator marked this as a false positive. No action recorded; pattern saved as a label for future Local Warden retraining.',
  allowlisted:      'A trusted entity (operator IP, admin user, etc.) — the AI silenced incidents about it because they are operationally expected, not malicious.',
  // Common verbs the dashboard renders.
  attacker:         'A unique IP that produced at least one incident. Counts dedupe by IP, not by event count.',
  alert:            'A real-time notification surface (toast / Telegram / Slack). Drives operator attention; does not change incident state.',
  flagged:          'The detector decided the event matches its threat model. Synonym for "incident raised".',
  suspicious:       'A signal worth investigating but below confirmed-malicious. The AI may auto-decide or escalate.'
};

// Returns a `title="..."` attribute fragment for a given GLOSSARY
// term so renderers can attach the canonical tooltip without
// re-typing the definition. Empty string when the term is unknown
// so callers can safely concatenate without producing `title=""`.
function glossaryTitle(term) {
  if (!term) return '';
  var def = GLOSSARY[String(term).toLowerCase()];
  if (!def) return '';
  return ' title="' + def.replace(/"/g, '&quot;') + '"';
}

// ── Confidence system helpers ─────────────────────────────────────────
function getUnresolved() {
  var ov = window._lastOverview || {};
  var confirmed = ov.ai_confirmed || 0;
  var responded = ov.ai_responded || 0;
  var unresolved = ov.unresolved_count != null ? ov.unresolved_count : Math.max(confirmed - responded, 0);
  return { total: confirmed, unresolved: unresolved, handled: responded };
}

// Human-readable labels for sensor collectors (spec 017 Change 6).
// Home's Data Collection section renders these instead of raw slugs
// so the primary operator sees "Network traffic" rather than
// "tcp_stream". Unknown slugs fall back to humanLabel().
var COLLECTOR_LABELS = {
  tcp_stream:         'Network traffic',
  http_capture:       'Web requests',
  dns_capture:        'DNS lookups',
  tls_fingerprint:    'TLS fingerprints',
  auth_log:           'Login attempts',
  auditd:             'System audit log',
  journald:           'System journal',
  docker:             'Docker events',
  proc_maps:          'Process memory',
  fanotify:           'File changes',
  kernel_integrity:   'Kernel integrity',
  cgroup:             'Resource usage',
  ebpf:               'Kernel system calls',
  firmware_integrity: 'Firmware integrity',
  nginx_access:       'Web server access',
  nginx_error:        'Web server errors',
  syslog_firewall:    'Firewall log',
  macos_log:          'macOS log',
  cloudtrail:         'AWS CloudTrail',
  integrity:          'File integrity'
};

function collectorLabel(slug) {
  return COLLECTOR_LABELS[slug] || humanLabel(slug);
}

var DETECTOR_LABELS = {
  ssh_bruteforce: 'SSH login attempts', credential_stuffing: 'Credential testing',
  host_drift: 'Unexpected process', execution_guard: 'Command monitoring',
  port_scan: 'Port scan', web_scan: 'Web vulnerability scan',
  user_agent_scanner: 'Automated scanner', network_sniffer: 'Network monitor detected',
  network_sniffing: 'Network monitoring tool', kernel_module: 'Kernel module loaded',
  dns_tunnel: 'DNS tunnel attempt', dns_c2: 'DNS command-and-control',
  reverse_shell: 'Reverse shell attempt', fileless_exec: 'Memory-only execution',
  sudo_abuse: 'Privilege escalation attempt', search_abuse: 'Search abuse',
  service_stop: 'Service stopped', container_escape: 'Container escape attempt',
  rootkit: 'Rootkit detection', log_tampering: 'Log tampering',
  proto_anomaly: 'Suspicious connection', threat_intel: 'Known malicious IP',
  packet_flood: 'Packet flood', discovery_burst: 'Reconnaissance burst',
  data_exfil_cmd: 'Data exfiltration attempt', suspicious_execution: 'Suspicious command',
  suspicious_archive: 'Suspicious archive creation', logging_config_change: 'Logging config changed',
  timing_anomaly: 'Timing anomaly'
};

// ── Severity helpers (spec 017 shared foundation) ─────────────────────
// Map severity strings to CSS classes, human labels, and numeric ranks.
// Used by every page spec that renders severity-scaled tone.
function severityClass(sev) {
  var s = (sev || '').toString().toLowerCase();
  if (s === 'critical') return 'alert-critical';
  if (s === 'high')     return 'alert-high';
  if (s === 'medium')   return 'alert-medium';
  if (s === 'low')      return 'alert-low';
  return 'alert-info';
}

function severityLabel(sev) {
  var s = (sev || '').toString().toLowerCase();
  if (s === 'critical') return 'Critical';
  if (s === 'high')     return 'High';
  if (s === 'medium')   return 'Medium';
  if (s === 'low')      return 'Low';
  return 'Info';
}

function severityRank(sev) {
  var s = (sev || '').toString().toLowerCase();
  if (s === 'critical') return 4;
  if (s === 'high')     return 3;
  if (s === 'medium')   return 2;
  if (s === 'low')      return 1;
  if (s === 'info')     return 0;
  return -1;
}

// Return the highest severity string found in a list of objects
// with .severity or .effective_severity. Prefers effective_severity.
function maxSeverity(list) {
  var best = -1;
  var bestName = 'info';
  (list || []).forEach(function(item) {
    var sev = item && (item.effective_severity || item.severity);
    var rank = severityRank(sev);
    if (rank > best) { best = rank; bestName = (sev || '').toString().toLowerCase() || 'info'; }
  });
  return best >= 0 ? bestName : 'info';
}

function humanLabel(slug) {
  return DETECTOR_LABELS[slug] || slug.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
}

function aggregateIncidents(incidents) {
  var groups = {};
  (incidents || []).forEach(function(inc) {
    var slug = (inc.incident_id || '').split(':')[0] || 'unknown';
    var outcome = inc.outcome || 'open';
    var key = slug + '|' + outcome;
    if (!groups[key]) {
      groups[key] = { slug: slug, outcome: outcome, count: 0,
        severity: inc.severity, latest: inc, ips: {} };
    }
    groups[key].count++;
    var ip = (inc.entities || []).find(function(e) { return e.type === 'Ip' || e.type === 'ip'; });
    if (ip) groups[key].ips[ip.value] = true;
    if (inc.ts > groups[key].latest.ts) groups[key].latest = inc;
  });
  return Object.values(groups).sort(function(a, b) {
    if (a.outcome === 'open' && b.outcome !== 'open') return -1;
    if (a.outcome !== 'open' && b.outcome === 'open') return 1;
    return b.count - a.count;
  });
}

// Phase 12 (QA fix #1, 2026-04-29): persistent header pill
// (#modeBadge) used to show GUARD/WATCH/MONITOR forever, never
// reflecting runtime health. When SystemHealth flipped to
// AiNotResponding (red Hero alert), pill stayed green
// "PROTECTED" — two contradictory states on one screen. This
// helper syncs the pill from /api/overview.snapshot every time
// the home or threats page refreshes overview.
//
// Priority: critical health alert > medium (backed up / abandoned
// backlog) > steady mode-badge from actionCfg.
function syncModeBadgeFromHealth(overview, modeCfg) {
  var badge = document.getElementById('modeBadge');
  if (!badge) return;
  // 2026-04-30: lucide SVG icons replace the emoji for the four
  // states. setBadgeWithIcon mirrors the helper in actions.js — both
  // write the same html shape so the CSS gap/alignment is consistent.
  var setBadgeWithIcon = function(iconName, text, cls, title) {
    badge.innerHTML = lucideIcon(iconName, { size: 14 }) +
      '<span style="margin-left:6px">' + text + '</span>';
    badge.className = 'status-badge ' + cls;
    badge.title = title || '';
  };
  var health = overview && overview.snapshot && overview.snapshot.health;
  if (health && health.kind === 'ai_not_responding') {
    setBadgeWithIcon(
      'alert-triangle',
      'AI NOT RESPONDING',
      'status-badge-alert',
      'AI provider/Local Warden is not answering. See system health.'
    );
    return;
  }
  if (health && (health.kind === 'backed_up' || health.kind === 'abandoned_backlog')) {
    setBadgeWithIcon(
      'circle-dashed',
      'CATCHING UP',
      'status-badge-medium',
      'AI is processing a backlog. Current protection is unaffected.'
    );
    return;
  }
  // 2026-05-01 audit fix (1.2): "Operating normally with maintenance
  // debt" badge so the persistent header pill stops claiming
  // PROTECTED while the hero shows orphaned/revert-failure/no-
  // executor reasons. First reason is appended to the title so an
  // operator hovering sees the headline without leaving the page.
  if (health && health.kind === 'degraded') {
    var degReasons = Array.isArray(health.reasons) ? health.reasons : [];
    var hover = 'Chronic drift requires operator attention. ' +
      (degReasons[0] || 'See system health for the full list.');
    setBadgeWithIcon(
      'alert-circle',
      'OPERATIONAL DEBT',
      'status-badge-medium',
      hover
    );
    return;
  }
  // Steady state: restore the mode badge from cached actionCfg.
  // Source of truth is actions.js — we mirror it here.
  if (modeCfg) {
    if (modeCfg.enabled) {
      if (modeCfg.dry_run) {
        setBadgeWithIcon('eye', 'WATCHING', 'status-badge-watch');
      } else {
        setBadgeWithIcon('shield-check', 'PROTECTED', 'status-badge-guard');
      }
    } else {
      setBadgeWithIcon('book-open', 'MONITOR', 'status-badge-read');
    }
  }
}

function outcomeBadgeHtml(outcome) {
  if (outcome === 'blocked' || outcome === 'killed' || outcome === 'contained' || outcome === 'suspended')
    return '<span class="badge-contained">BLOCKED</span>';
  if (outcome === 'ignored' || outcome === 'dismissed') return '<span class="badge-noise">DISMISSED</span>';
  if (outcome === 'monitoring' || outcome === 'monitored' || outcome === 'open' || outcome === 'active')
    return '<span class="badge-monitor" style="font-size:0.62rem;padding:2px 7px;border-radius:4px">OBSERVING</span>';
  if (outcome === 'honeypot') return '<span style="font-size:0.62rem;padding:2px 7px;border-radius:4px;background:rgba(255,140,66,0.12);color:var(--orange);font-weight:600">HONEYPOT</span>';
  if (outcome === 'needs_attention') return '<span class="badge-unresolved">NEEDS ATTENTION</span>';
  return '';
}

// Phase 4 (audit RC-4): render the kernel-evidence block_state shipped
// by the backend (PR #335) into a small operator-visible badge.
//
// `state` matches the BlockState enum from threat_contract.rs:
//   { kind: "blocked_now",      since_ms, ttl_secs, expires_in_secs }
//   { kind: "blocked_historical", last_block_ms }
//   { kind: "open" }
//
// Returns '' for "open" and for missing state — the existing outcome
// badge already conveys "no kernel block" so we don't add visual
// clutter for the steady state. Only the two affirmative kernel
// states get a dedicated badge.
function blockStateBadgeHtml(state) {
  if (!state || !state.kind) return '';
  if (state.kind === 'blocked_now') {
    var ttl = Number(state.expires_in_secs);
    var label = lucideIcon('shield-check',{size:12}) + ' KERNEL';
    var tip = 'Kernel-level block currently active.';
    if (Number.isFinite(ttl) && ttl > 0) {
      var humanTtl = ttl >= 3600
        ? Math.floor(ttl / 3600) + 'h'
        : ttl >= 60
        ? Math.floor(ttl / 60) + 'm'
        : ttl + 's';
      label += ' · ' + humanTtl;
      tip = 'Kernel-level block active. Expires in ~' + humanTtl + '.';
    }
    return '<span class="card-badge badge-kernel-active" title="' + tip + '">' + label + '</span>';
  }
  if (state.kind === 'blocked_historical') {
    return '<span class="card-badge badge-kernel-expired" title="Kernel-level block has expired (TTL elapsed). The agent remembers issuing it but the kernel no longer enforces it.">' + lucideIcon('alert-triangle',{size:12}) + ' EXPIRED</span>';
  }
  return '';
}

function humanIncidentTitle(detector, rawTitle, ip) {
  var label = humanLabel(detector);
  if (ip) label += ' \u2014 ' + ip;
  return label;
}

function contextLine(outcome, severity) {
  switch (outcome) {
    case 'blocked': case 'killed': case 'contained': case 'suspended':
      return { text: 'Handled automatically \u2014 no action needed', cls: '' };
    case 'ignored':
      return { text: 'Classified as noise \u2014 no action needed', cls: '' };
    case 'monitored':
      return { text: 'Being monitored \u2014 system watching for escalation', cls: '' };
    case 'honeypot':
      return { text: 'Redirected to honeypot \u2014 attacker contained safely', cls: '' };
    default:
      if (severity === 'critical' || severity === 'high')
        return { text: 'Needs review \u2014 no automated response taken', cls: 'needs-action' };
      return { text: 'Awaiting analysis', cls: '' };
  }
}

function entryOutcomeClass(entry) {
  var d = entry.data || {};
  // For decisions, use the action to infer outcome
  if (entry.kind === 'decision') {
    var at = d.action_type || '';
    if (['block_ip','kill_process','block_container','suspend_user_sudo'].includes(at)) return 'entry-contained';
    if (at === 'ignore') return 'entry-noise';
    return '';
  }
  // For incidents, check if there's an outcome hint or use severity
  // We don't have outcome directly on the entry, so we check if the journey has it
  return '';
}

function toggleDetail(btn) {
  // Find .detail-body within the same evidence-card, not by sibling
  // position (the header/title/context divs sit between the button
  // and the detail body, breaking nextElementSibling).
  var card = btn.closest('.evidence-card');
  var body = card ? card.querySelector('.detail-body') : null;
  if (!body) return;
  body.classList.toggle('open');
  btn.textContent = body.classList.contains('open') ? 'Hide details' : 'Show details';
}


function isPrivateIp(ip) {
  return ip.startsWith('10.') || ip.startsWith('127.') || ip.startsWith('192.168.') ||
    ip.startsWith('169.254.') || ip === '::1' || ip.startsWith('fc') || ip.startsWith('fd') ||
    /^172\.(1[6-9]|2\d|3[01])\./.test(ip);
}

// Unified incident-trust check (spec 017 Change 6).
// Returns true when the incident should be hidden while
// state.hideAllowlisted is on; false when it should be shown.
//
// Rule 1 (severity gate): critical/high are NEVER filtered by trust —
//   they must always reach the operator regardless of entity shape.
//
// Rule 2 (entity walk, for medium/low/info): inspect entities. If any
//   external non-trusted IP is present, show. If any non-trusted user
//   is present, show. Otherwise hide (handles allowlisted-only and
//   no-entity noise like host_drift sudo or kill_chain forming).
function isIncidentTrusted(inc) {
  var sev = ((inc && (inc.effective_severity || inc.severity)) || '').toString().toLowerCase();
  if (sev === 'critical' || sev === 'high') return false;

  var entities = (inc && inc.entities) || [];
  var sawExternalIp = false;
  var allIpsTrusted = true;
  var sawUntrustedUser = false;

  for (var i = 0; i < entities.length; i++) {
    var e = entities[i];
    var eType = (typeof e === 'string') ? (e.split(':')[0] || '') : (e.type || '');
    var eVal  = (typeof e === 'string') ? (e.split(':').slice(1).join(':') || '') : (e.value || '');
    eType = eType.toLowerCase();

    if (eType === 'ip') {
      sawExternalIp = true;
      if (!isIpTrusted(eVal) && !isPrivateIp(eVal)) {
        allIpsTrusted = false;
      }
    }
    if (eType === 'user') {
      if (_trustedUsers.indexOf(eVal) < 0) sawUntrustedUser = true;
    }
  }

  if (sawExternalIp && !allIpsTrusted) return false;
  if (sawUntrustedUser) return false;
  return true;
}


// 2026-04-29 (audit Phase 2): `loadHomeState`, `setHomeKpi`,
// `updateStatusHero`, and `buildActivityFeed` were dead code.
// They wrote to DOM IDs that no longer exist in `index.html`
// (`h-events`, `h-incidents`, `h-decisions`, `h-blocks`,
// `statusHero`, `heroIcon`, `heroTitle`, `heroSub`, `activityFeed`)
// and the only call site (`loadHomeState`) was orphaned (no
// trigger ever invoked it). The Home tab is now driven entirely by
// `home.js::loadHome` which writes to the actual `homeHero`/
// `homeHeroIcon`/etc IDs. Removing the dead functions kept this
// file from misleading future readers; the single point of truth
// for Home rendering is `home.js`.

function timeAgo(ts) {
  if (!ts) return '';
  const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
  if (diff < 60) return diff + 's ago';
  if (diff < 3600) return Math.floor(diff/60) + 'm ago';
  if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
  return Math.floor(diff/86400) + 'd ago';
}

// Temporal window label helper (spec 017 shared foundation).
// Returns a canonical English label for a KPI time window.
// Unknown kinds return '' so callers can omit the label gracefully.
function formatWindow(kind) {
  switch ((kind || '').toString()) {
    case 'live':        return 'Live';
    case 'today':       return 'Today';
    case 'last_24h':    return 'Last 24h';
    case 'last_6h':     return 'Last 6h';
    case 'last_hour':   return 'Last hour';
    case 'since_start': return 'Since startup';
    default:            return '';
  }
}

function handleCardClickByValue(type, value) {
  // Find the card with this value and click it, or load journey directly
  const cards = document.querySelectorAll('.attacker-card');
  for (const card of cards) {
    if (card.dataset.subjectValue === value && card.dataset.subjectType === type) {
      card.click();
      return;
    }
  }
  // Direct load
  loadJourney(type, value);
}

function showHomeState() {
  document.getElementById('homeState').style.display = '';
  document.getElementById('journeyContent').style.display = 'none';
  document.getElementById('journeyContent').innerHTML = '';
  // Deselect active card
  document.querySelectorAll('.attacker-card.active').forEach(c => c.classList.remove('active'));
  state.currentSubject = null;
  // Spec 037 Threats UX bundle: refresh the empty-state copy from
  // /api/threats/diagnostic so the operator sees WHY the right panel
  // is empty instead of a generic "Select a threat to investigate".
  if (typeof refreshThreatsDiagnostic === 'function') {
    refreshThreatsDiagnostic();
  }
}

// Spec 037 Threats UX bundle: render diagnostic-aware empty state.
// Fetches /api/threats/diagnostic with the current date filter so the
// message reflects the same scope the pivots use. Falls back to the
// generic copy on fetch failure.
function refreshThreatsDiagnostic() {
  var msg = document.getElementById('homeStateMessage');
  var hint = document.getElementById('homeStateHint');
  var icon = document.getElementById('homeStateIcon');
  if (!msg || !hint || !icon) {
    return; // home view (not the threats view); nothing to do
  }
  // Spec 037 Threats UX bundle: pull the same filter set the pivots
  // use so the diagnostic agrees with what the IP/User/Detector
  // pivots return. `state.filters` is owned by threats.js and may
  // be undefined on the home view -- defaults are fine.
  var filters = (typeof state !== 'undefined' && state.filters) ? state.filters : {};
  var qs = '';
  if (filters.date) qs += (qs ? '&' : '?') + 'date=' + encodeURIComponent(filters.date);
  if (filters.severity_min) qs += (qs ? '&' : '?') + 'severity_min=' + encodeURIComponent(filters.severity_min);
  if (filters.detector) qs += (qs ? '&' : '?') + 'detector=' + encodeURIComponent(filters.detector);
  loadJson('/api/threats/diagnostic' + qs)
    .then(function(d) {
      hint.style.display = 'none';
      hint.textContent = '';
      if (!d.has_incidents && !d.scope_mismatch) {
        icon.innerHTML = lucideIcon('flame',{size:24});
        msg.textContent = 'No threats in scope. Either nothing fired today or the filter is too narrow.';
        return;
      }
      if (!d.has_incidents && d.scope_mismatch) {
        icon.innerHTML = lucideIcon('clipboard-list',{size:24});
        msg.textContent = 'No incidents on this date. The graph has incidents on other days.';
        hint.textContent = 'Try the previous day in the Date filter.';
        hint.style.display = '';
        return;
      }
      if (d.has_incidents && !d.has_entities) {
        icon.innerHTML = lucideIcon('alert-triangle',{size:24});
        msg.textContent = d.incidents_in_scope + ' incident(s) found, but no IP/User entities linked.';
        if (d.detector_pivot_count > 0) {
          hint.textContent = 'Try the Detector pivot to drill in by detection rule.';
          hint.style.display = '';
        }
        return;
      }
      icon.innerHTML = lucideIcon('search',{size:24});
      msg.textContent = 'Select a threat to investigate';
    })
    .catch(function() {
      // Diagnostic endpoint unavailable: keep the original generic copy.
      icon.innerHTML = lucideIcon('search',{size:24});
      msg.textContent = 'Select a threat to investigate';
      hint.style.display = 'none';
    });
}

function investigateTopThreat() {
  // Click the first attacker card if one exists, else no-op
  const first = document.querySelector('.attacker-card');
  if (first) { first.click(); return; }
  // Show investigate tab in case we're in a different view
  showView('investigate');
}

// 2026-05-15 slim-down: removed toggleAdvFilters — the #advFilters
// collapsible block was deleted from the Cases sidebar along with the
// hour/severity/detector/status/compare-date filters it contained.
