// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.

/**
 * Deterministic exploit collector → markdown renderer.
 *
 * Single entry point renderExploitDeliverable(vulnClass, state, idToType)
 * covers all 5 exploitation agents (injection, xss, auth, ssrf, authz). The
 * per-class deltas are limited to title and ID prefix; every section, label,
 * and sort rule is class-agnostic. Section headers and bolded field labels
 * mirror the prescribed-Markdown skeleton from the existing exploit-*.txt
 * prompts so downstream report-executive — which reads prose with bolded
 * labels — sees the same structure it sees today.
 *
 * Field-label drift across the 5 prompts ("Evidence of Vulnerability" vs
 * "Why We Believe This Is Vulnerable"; "Attempted Exploitation" vs
 * "What We Tried") is canonicalized here to a single label per field across
 * all classes.
 *
 * Sort order is owned by the renderer:
 *   - Successfully Exploited: severity desc (critical → low), then ID asc.
 *   - Potential / Validation Blocked: confidence desc (high → low), then ID asc.
 *
 * ## Unprocessed Vulnerabilities surfaces queue IDs the collector did not see —
 * the v1 stand-in for required-call enforcement. The activity passes idToType
 * (queue ID → vulnerability_type, built from queue.json) so each entry renders
 * as `- {ID} ({vulnerability_type})`. Omitted when every queue ID was emitted.
 */

import type { AddExploitInput, VulnClass } from '../collectors/exploit-collector.js';

// ============================================================================
// PER-CLASS CONSTANTS
// ============================================================================

const TITLES: Record<VulnClass, string> = {
  injection: 'Injection Exploitation Evidence',
  xss: 'Cross-Site Scripting (XSS) Exploitation Evidence',
  auth: 'Authentication Exploitation Evidence',
  ssrf: 'SSRF Exploitation Evidence',
  authz: 'Authorization Exploitation Evidence',
};

// ============================================================================
// SORT ORDER
// ============================================================================

const SEVERITY_ORDER: Record<'critical' | 'high' | 'medium' | 'low', number> = {
  critical: 0,
  high: 1,
  medium: 2,
  low: 3,
};

const CONFIDENCE_ORDER: Record<'high' | 'medium' | 'low', number> = {
  high: 0,
  medium: 1,
  low: 2,
};

type ExploitedEntry = Extract<AddExploitInput, { status: 'exploited' }>;
type BlockedEntry = Extract<AddExploitInput, { status: 'blocked' }>;

function sortExploited(entries: readonly ExploitedEntry[]): ExploitedEntry[] {
  return [...entries].sort((a, b) => {
    const sevDiff = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
    if (sevDiff !== 0) return sevDiff;
    return a.vulnerability_id.localeCompare(b.vulnerability_id);
  });
}

function sortBlocked(entries: readonly BlockedEntry[]): BlockedEntry[] {
  return [...entries].sort((a, b) => {
    const confDiff = CONFIDENCE_ORDER[a.confidence] - CONFIDENCE_ORDER[b.confidence];
    if (confDiff !== 0) return confDiff;
    return a.vulnerability_id.localeCompare(b.vulnerability_id);
  });
}

// ============================================================================
// FIELD FORMATTERS
// ============================================================================

function capitalize(value: string): string {
  if (value.length === 0) return value;
  return value[0]!.toUpperCase() + value.slice(1);
}

function renderNumberedList(steps: readonly string[]): string {
  return steps.map((step, idx) => `${idx + 1}. ${step}`).join('\n\n');
}

// ============================================================================
// PER-FINDING RENDERERS
// ============================================================================

function renderExploitedFinding(entry: ExploitedEntry): string {
  const lines: string[] = [];
  lines.push(`### ${entry.vulnerability_id}: ${entry.title}`);
  lines.push('');
  lines.push('**Summary:**');
  lines.push(`- **Vulnerable location:** ${entry.vulnerable_location}`);
  lines.push(`- **Overview:** ${entry.overview}`);
  lines.push(`- **Impact:** ${entry.impact}`);
  lines.push(`- **Severity:** ${capitalize(entry.severity)}`);
  lines.push('');
  if (entry.prerequisites != null && entry.prerequisites.length > 0) {
    lines.push('**Prerequisites:**');
    lines.push(entry.prerequisites);
    lines.push('');
  }
  lines.push('**Exploitation Steps:**');
  lines.push(renderNumberedList(entry.exploitation_steps));
  lines.push('');
  lines.push('**Proof of Impact:**');
  lines.push(entry.proof_of_impact);
  if (entry.notes != null && entry.notes.length > 0) {
    lines.push('');
    lines.push('**Notes:**');
    lines.push(entry.notes);
  }
  return lines.join('\n');
}

function renderBlockedFinding(entry: BlockedEntry): string {
  const lines: string[] = [];
  lines.push(`### ${entry.vulnerability_id}: ${entry.title}`);
  lines.push('');
  lines.push('**Summary:**');
  lines.push(`- **Vulnerable location:** ${entry.vulnerable_location}`);
  lines.push(`- **Overview:** ${entry.overview}`);
  lines.push(`- **Current Blocker:** ${entry.current_blocker}`);
  lines.push(`- **Potential Impact:** ${entry.potential_impact}`);
  lines.push(`- **Confidence:** ${entry.confidence.toUpperCase()}`);
  lines.push('');
  if (entry.prerequisites != null && entry.prerequisites.length > 0) {
    lines.push('**Prerequisites:**');
    lines.push(entry.prerequisites);
    lines.push('');
  }
  lines.push('**Evidence of Vulnerability:**');
  lines.push(entry.evidence_of_vulnerability);
  lines.push('');
  lines.push('**What We Tried:**');
  lines.push(entry.what_we_tried);
  lines.push('');
  lines.push('**How This Would Be Exploited:**');
  lines.push(renderNumberedList(entry.how_this_would_be_exploited));
  lines.push('');
  lines.push('**Expected Impact:**');
  lines.push(entry.expected_impact);
  if (entry.notes != null && entry.notes.length > 0) {
    lines.push('');
    lines.push('**Notes:**');
    lines.push(entry.notes);
  }
  return lines.join('\n');
}

// ============================================================================
// SECTION RENDERERS
// ============================================================================

function renderExploitedSection(entries: readonly ExploitedEntry[]): string {
  const heading = '## Successfully Exploited Vulnerabilities';
  if (entries.length === 0) {
    return [heading, '', '*No findings reached a definitive verdict in this category.*'].join('\n');
  }
  const blocks = sortExploited(entries).map(renderExploitedFinding);
  return [heading, '', blocks.join('\n\n')].join('\n');
}

function renderBlockedSection(entries: readonly BlockedEntry[]): string {
  const heading = '## Potential Vulnerabilities (Validation Blocked)';
  if (entries.length === 0) {
    return [heading, '', '*No findings reached a definitive verdict in this category.*'].join('\n');
  }
  const blocks = sortBlocked(entries).map(renderBlockedFinding);
  return [heading, '', blocks.join('\n\n')].join('\n');
}

function renderUnprocessedSection(missingIds: readonly string[], idToType: ReadonlyMap<string, string>): string {
  const heading = '## Unprocessed Vulnerabilities';
  const sortedIds = [...missingIds].sort((a, b) => a.localeCompare(b));
  const lines = sortedIds.map((id) => {
    const type = idToType.get(id);
    return type ? `- ${id} (${type})` : `- ${id}`;
  });
  return [
    heading,
    '',
    'The following queue vulnerabilities did not receive a definitive verdict during this run:',
    '',
    lines.join('\n'),
  ].join('\n');
}

// ============================================================================
// PUBLIC ENTRY POINT
// ============================================================================

export function renderExploitDeliverable(
  vulnClass: VulnClass,
  state: readonly AddExploitInput[],
  idToType: ReadonlyMap<string, string>,
): string {
  const title = `# ${TITLES[vulnClass]}`;

  if (state.length === 0 && idToType.size === 0) {
    const body = '*No vulnerabilities were available in the queue for exploitation.*';
    return `${title}\n\n${body}\n`;
  }

  const exploited = state.filter((e): e is ExploitedEntry => e.status === 'exploited');
  const blocked = state.filter((e): e is BlockedEntry => e.status === 'blocked');

  const emittedIds = new Set(state.map((e) => e.vulnerability_id));
  const missingIds = [...idToType.keys()].filter((id) => !emittedIds.has(id));

  const sections: string[] = [title, '', renderExploitedSection(exploited), '', renderBlockedSection(blocked)];

  if (missingIds.length > 0) {
    sections.push('');
    sections.push(renderUnprocessedSection(missingIds, idToType));
  }

  return `${sections.join('\n').trimEnd()}\n`;
}
