import {
  type SecurityCheckStep,
  type BrowserFinding,
  gateFinding,
  sanitizeRequest,
} from '@hover-dev/probe-engine';
import { vulnInfo } from './vulnInfo.js';

export interface ReportOptions {
  /** Report title, e.g. the engagement / app name. */
  name?: string;
  /** Browser-confirmed findings (XSS via input, DOM-based, UI-level flaws) the
   *  agent recorded via record_finding — attacks driven through the page rather
   *  than via replay_flow. Rendered in the Findings section alongside the
   *  replay-based checks. */
  findings?: BrowserFinding[];
  /** What the scan did NOT cover — surfaced verbatim under "Not tested" so a
   *  reader never mistakes the report for full coverage. Typically: destructive
   *  probes held back, classes/areas out of the scope prompt, blind findings
   *  only confirmable via out-of-band (downgraded to suspected). */
  notTested?: string[];
}

/** A check is noise (never-submit) — same gate the spec writer uses. */
function isNoise(c: SecurityCheckStep): boolean {
  return gateFinding({
    title: c.intent,
    exploitableNow: true,
    impactProven: true,
    alreadyKnown: false,
  }).verdict === 'kill';
}

/** Coarse severity for a finding (a check whose control did NOT hold). */
function severity(c: SecurityCheckStep): 'High' | 'Medium' | 'Low' {
  // Access was granted where it should have been denied → High.
  if (c.observed.status >= 200 && c.observed.status < 300 && c.expectStatus >= 400) return 'High';
  // Server error — ambiguous, likely not a clean exploit.
  if (c.observed.status >= 500) return 'Low';
  return 'Medium';
}

/** Sanitized URL — never leak a query-string token into the report. Even the
 *  fallback (no captured request) is masked: observed.url can carry a token in
 *  its query string, so run it through the same sanitiser. */
function safeUrl(c: SecurityCheckStep): string {
  const req = c.request ?? { method: c.observed.method, url: c.observed.url, headers: {}, bodyText: null };
  return sanitizeRequest(req).url;
}

/**
 * Render a findings report (Markdown) from a session's recorded security
 * checks — the pentest deliverable. The discovery counterpart to
 * @hover-dev/security's spec crystallization: this REPORTS what was found; it
 * does not emit a test. No request bodies/headers/cookies are dumped, and URLs
 * are sanitized, so the report carries no secrets.
 */
export function renderFindingsReport(
  checks: SecurityCheckStep[],
  opts: ReportOptions = {},
): string {
  const name = opts.name ?? 'scan';
  const date = new Date().toISOString().slice(0, 10);

  const suppressed = checks.filter(isNoise);
  const kept = checks.filter(c => !isNoise(c));
  const replayFindings = kept.filter(c => !c.matched);
  const controls = kept.filter(c => c.matched);
  // Browser-confirmed findings (record_finding) — attacks landed in the page,
  // no replayed request. Oldest-id first for stable, readable ordering.
  const browserFindings = [...(opts.findings ?? [])].sort((a, b) => a.id - b.id);
  const totalFindings = replayFindings.length + browserFindings.length;

  let target = '';
  try {
    target = kept.length > 0 ? new URL(safeUrl(kept[0])).host : '';
  } catch {
    /* leave blank */
  }

  const lines: string[] = [];
  lines.push(`# Security scan — ${name}`);
  lines.push('');
  lines.push(`Generated ${date}.${target ? ` Target: ${target}.` : ''}`);
  lines.push('');
  lines.push(
    `${totalFindings} finding(s), ${controls.length} verified control(s).` +
      (suppressed.length > 0 ? ` ${suppressed.length} noise check(s) suppressed.` : ''),
  );

  if (totalFindings > 0) {
    lines.push('', '## Findings');
    // Browser-confirmed findings first (the agent saw them happen in the page).
    for (const f of browserFindings) {
      const info = vulnInfo(f.class);
      lines.push('', `### [${f.severity}] ${f.intent}`);
      if (f.class) lines.push(`- **Type:** ${f.class}`);
      if (f.location) lines.push(`- **Location:** \`${f.location}\``);
      lines.push(`- **Evidence:** ${f.evidence}`);
      lines.push(`- **Impact:** ${info.impact}`);
      lines.push(`- **Recommendation:** ${info.recommendation}`);
    }
    for (const c of replayFindings) {
      const info = vulnInfo(c.class);
      lines.push('', `### [${severity(c)}] ${c.intent}`);
      lines.push(`- **Endpoint:** \`${c.observed.method} ${safeUrl(c)}\``);
      lines.push(`- **Observed:** ${c.observed.status} (control should deny — expected ${c.expectStatus}).`);
      if (c.crossIdentity) {
        lines.push(`- **Identity:** replayed as a second identity (\`${c.crossIdentity.identityB}\`) — it reached the first identity's resource.`);
      }
      lines.push(`- **Impact:** ${info.impact}`);
      lines.push(`- **Recommendation:** ${info.recommendation}`);
      lines.push(
        `- **PoC:** replay \`${c.observed.method} ${safeUrl(c)}\`` +
          (c.request?.bodyText ? ' with the captured body' : '') +
          (c.crossIdentity ? " under the second identity's session." : '.'),
      );
    }
  }

  if (controls.length > 0) {
    lines.push('', '## Verified controls');
    for (const c of controls) {
      lines.push(`- ${c.intent} — \`${c.observed.method} ${safeUrl(c)}\` → ${c.observed.status} (expected ${c.expectStatus}) ✓`);
    }
  }

  // Coverage — never let the report read as "everything was tested".
  const notTested = opts.notTested ?? [];
  lines.push('', '## Not tested');
  if (notTested.length > 0) {
    lines.push('This scan did NOT cover the following — treat them as unknown, not safe:');
    for (const item of notTested) lines.push(`- ${item}`);
  } else {
    lines.push('No coverage gaps were reported for this scan.');
  }

  lines.push('', '> Lock a finding as a CI regression test with @hover-dev/security — "Save as Security spec".');
  return `${lines.join('\n')}\n`;
}
