/**
 * `@hover-dev/pentest/plugin` — the RED pentest mode for the Hover widget.
 *
 * Sibling to `@hover-dev/security`'s orange mode: same MITM machinery, opposite
 * posture. Where security is surgical business/authz testing crystallized into a
 * regression spec, pentest is offensive vuln hunting (destructive ON, in-band
 * exploit-to-confirm) that produces a findings report.
 *
 * Runtime: it reaches the shared resident MITM proxy + control plane through
 * `@hover-dev/security`'s `startSecurityRuntime` (refcounted — coexists with the
 * security plugin when both are loaded; the two modes are mutually exclusive via
 * `conflictsWith`). The agent's objective is the offensive prompt from this
 * package's pure half; the Save dropdown writes the report.
 *
 * This entry is SEPARATE from the package's main `index.js` so the pure library
 * (objective + report, consumed by the CLI and by security) stays dependency-
 * free — only this file pulls in the security runtime + core plugin-api.
 *
 * Usage:
 *   import { hover } from 'vite-plugin-hover';
 *   import pentestMode from '@hover-dev/pentest/plugin';
 *   export default defineConfig({ plugins: [hover({}, pentestMode())] });
 */
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineHoverPlugin, type HoverPluginManifest } from '@hover-dev/core/plugin-api';
import {
  startSecurityRuntime,
  securityMcpScriptPath,
  type SecurityRuntimeHandle,
} from '@hover-dev/security';
import { buildScanObjective } from './objective.js';
import { writeFindingsReport } from './index.js';

/** Absolute path to the widget contribution module (registers the "Findings
 *  report" Save entry). Resolved relative to this module so it works whether
 *  loaded from dist/ (the build copies src/widget.js → dist/widget.js) or src/. */
function resolveWidgetScriptPath(): string {
  const here = dirname(fileURLToPath(import.meta.url));
  return resolve(here, 'widget.js');
}

export interface PentestModeOptions {
  /** Second identities for cross-identity (IDOR/BOLA) replay during a scan —
   *  label → Playwright storageState path relative to the project root. */
  identities?: Record<string, string>;
}

const MODE_ID = 'pentest';
const MCP_SERVER_ID = '@hover-dev/pentest:flows';

/**
 * The offensive objective, as a static system-prompt addition. The widget
 * supplies the *scope* through the chat box (e.g. "test the checkout flow");
 * the origin is locked generically to whatever the debug Chrome is currently
 * on, since the agent drives that very tab and must not wander off it.
 */
const PENTEST_SYSTEM_PROMPT = buildScanObjective({
  scope: null,
  origin:
    'the dev-server origin currently open in this browser (the page the debug ' +
    'Chrome is showing) — do NOT navigate to or send requests to any other origin',
});

export default defineHoverPlugin<PentestModeOptions | void>((opts) => {
  let rt: SecurityRuntimeHandle | null = null;

  const manifest: HoverPluginManifest = {
    apiVersion: 1,
    name: '@hover-dev/pentest',

    mode: {
      id: MODE_ID,
      label: 'Pentest',
      description:
        'RED offensive scan — attacks your own dev app for vulnerabilities (injection / SSRF / IDOR / …) and writes a findings report. Destructive, origin-locked.',
      engagedHint: 'RED — offensive scan',
      accent: '#dc2626',
      // The two modes share one resident proxy; only one may record at a time.
      conflictsWith: ['security'],
    },

    mcpServers: [
      {
        id: MCP_SERVER_ID,
        command: process.execPath,
        // Reuse the security MCP server script under our own id (env wired in
        // the activate hook via ctx.setMcpServerEnv).
        args: [securityMcpScriptPath()],
        activeInModes: [MODE_ID],
      },
    ],

    systemPromptAdditions: [{ text: PENTEST_SYSTEM_PROMPT, activeInModes: [MODE_ID] }],

    widgetEntry: resolveWidgetScriptPath(),
    widgetEventTypes: ['pentest:check:recorded'],

    saveHandlers: [
      {
        type: 'save:pentest:report',
        label: 'Findings report',
        description:
          'Markdown report of the probes the agent ran — findings, severity, PoC, and an explicit "what was NOT tested" section.',
        handle: async ({ devRoot, payload }) => {
          if (!rt) throw new Error('Pentest mode is not active — no runtime to read findings from.');
          const checks = rt.listChecks();
          const findings = rt.listFindings();
          if (checks.length === 0 && findings.length === 0) {
            throw new Error(
              'Nothing recorded in this session. Have the agent confirm an attack first — replay_flow (intent + expectStatus) for HTTP-level probes, or record_finding for a browser-confirmed bug (e.g. reflected XSS).',
            );
          }
          // A findings report is a regenerated artifact — re-saving the same
          // name intentionally overwrites (re-run scan → fresh report). No
          // `overwrite` flag: unlike a spec (which the user may hand-edit), a
          // report carries no edits worth protecting.
          const p = (payload ?? {}) as { name?: string };
          if (typeof p.name !== 'string' || !p.name.trim()) {
            throw new Error('save:pentest:report: name is required');
          }
          const { path, slug } = await writeFindingsReport({
            devRoot,
            name: p.name,
            checks,
            findings,
            notTested: rt.listGaps(),
          });
          return { path, slug };
        },
      },
    ],

    hooks: {
      // Acquire the shared resident proxy (passthrough) + our own control plane
      // BEFORE Chrome launches, so Chrome is born proxied. We start quiet and
      // only record once the user enters pentest mode.
      async 'hover:service:start'(ctx) {
        if (rt) return; // idempotent
        rt = await startSecurityRuntime({
          devRoot: ctx.devRoot,
          identities: opts?.identities,
          intercept: false,
          mcpServerId: MCP_SERVER_ID,
        });
        ctx.setChromeProxy({ port: rt.proxyPort, spki: rt.spki });
        ctx.setMcpServerEnv(MCP_SERVER_ID, rt.mcpEnv);
        rt.onCheck((check) => ctx.broadcast({ type: 'pentest:check:recorded', payload: check }));
      },

      // Enter pentest mode: start recording the (already-proxied) Chrome.
      async 'hover:mode:activate'() {
        rt?.setIntercept(true);
      },
      // Leave pentest mode: stop recording (proxy stays resident).
      async 'hover:mode:deactivate'() {
        rt?.setIntercept(false);
      },

      async 'hover:service:shutdown'() {
        await rt?.stop();
        rt = null;
      },
    },
  };

  return manifest;
});
