// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 - see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * engagement.js: Engagement/Campaign/Task state machine for NERF.
 *
 * Manages the .nerf/ state directory and the engagement lifecycle:
 *   Engagement (E001) -> Campaign (C01) -> Task (T01)
 *
 * Reads and writes:
 *   .nerf/STATE.md              - Quick-glance dashboard
 *   .nerf/ENGAGEMENT.md         - Active engagement scope/targets/constraints
 *   .nerf/DECISIONS.md          - Append-only decisions register
 *   .nerf/KNOWLEDGE.md          - Cross-session patterns and lessons
 *   .nerf/engagements/E001/     - Per-engagement artifacts
 *     E001-ROADMAP.md             - Engagement plan with campaign checkboxes
 *     E001-CONTEXT.md             - Operator decisions from discuss phase
 *     E001-RESEARCH.md            - Target and ecosystem research
 *     campaigns/C01/              - Per-campaign artifacts
 *       C01-PLAN.md               - Campaign task decomposition
 *       C01-FINDINGS.md           - Security findings for this campaign
 *       tasks/T01-PLAN.md         - Individual task plan
 *       tasks/T01-SUMMARY.md      - What happened (YAML frontmatter + narrative)
 *
 * @module execution-engine/engagement
 */

import { readFileSync, writeFileSync, appendFileSync, renameSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
import { join } from 'node:path';

// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------

const NERF_DIR = '.nerf';

/**
 * Resolve the .nerf directory, creating it if needed.
 * @param {string} [root=process.cwd()]
 * @returns {string}
 */
export function nerfDir(root = process.cwd()) {
  const dir = join(root, NERF_DIR);
  mkdirSync(dir, { recursive: true });
  return dir;
}

/**
 * Resolve the engagement directory.
 * @param {string} eid - Engagement ID (e.g. 'E001')
 * @param {string} [root=process.cwd()]
 * @returns {string}
 */
export function engagementDir(eid, root = process.cwd()) {
  const dir = join(nerfDir(root), 'engagements', eid);
  mkdirSync(dir, { recursive: true });
  return dir;
}

/**
 * Resolve the campaign directory within an engagement.
 * @param {string} eid
 * @param {string} cid - Campaign ID (e.g. 'C01')
 * @param {string} [root=process.cwd()]
 * @returns {string}
 */
export function campaignDir(eid, cid, root = process.cwd()) {
  const dir = join(engagementDir(eid, root), 'campaigns', cid);
  mkdirSync(dir, { recursive: true });
  return dir;
}

/**
 * Resolve the tasks directory within a campaign.
 * @param {string} eid
 * @param {string} cid
 * @param {string} [root=process.cwd()]
 * @returns {string}
 */
export function tasksDir(eid, cid, root = process.cwd()) {
  const dir = join(campaignDir(eid, cid, root), 'tasks');
  mkdirSync(dir, { recursive: true });
  return dir;
}

// ---------------------------------------------------------------------------
// State management
// ---------------------------------------------------------------------------

/**
 * @typedef {Object} EngagementState
 * @property {string|null} activeEngagement - Active engagement ID
 * @property {string} phase - Current phase (ready|research|planning|executing|reporting)
 * @property {string|null} activeCampaign - Active campaign ID
 * @property {string|null} activeTask - Active task ID
 */

/**
 * Read .nerf/STATE.md and parse current state.
 * @param {string} [root=process.cwd()]
 * @returns {EngagementState}
 */
export function readState(root = process.cwd()) {
  const statePath = join(nerfDir(root), 'STATE.md');
  const defaults = { activeEngagement: null, phase: 'ready', activeCampaign: null, activeTask: null };

  if (!existsSync(statePath)) return defaults;

  const content = readFileSync(statePath, 'utf-8');

  const engMatch = content.match(/\*\*Active Engagement:\*\*\s*(\S+)/);
  const phaseMatch = content.match(/\*\*Phase:\*\*\s*(\S+)/);

  return {
    activeEngagement: engMatch && engMatch[1] !== 'None' ? engMatch[1] : null,
    phase: phaseMatch ? phaseMatch[1] : 'ready',
    activeCampaign: null,
    activeTask: null,
  };
}

/**
 * Write .nerf/STATE.md.
 * @param {EngagementState} state
 * @param {Object} [opts]
 * @param {string[]} [opts.registry] - Engagement registry entries
 * @param {string[]} [opts.decisions] - Recent decisions
 * @param {string[]} [opts.blockers] - Current blockers
 * @param {string} [opts.nextAction] - Next action
 * @param {string} [root=process.cwd()]
 */
export function writeState(state, opts = {}, root = process.cwd()) {
  const registry = opts.registry?.length
    ? opts.registry.map(r => `- ${r}`).join('\n')
    : '_(empty)_';
  const decisions = opts.decisions?.length
    ? opts.decisions.map(d => `- ${d}`).join('\n')
    : '_(none)_';
  const blockers = opts.blockers?.length
    ? opts.blockers.map(b => `- ${b}`).join('\n')
    : '- None';
  const nextAction = opts.nextAction || 'Start an engagement.';

  const content = `# NERF State

**Active Engagement:** ${state.activeEngagement || 'None'}
**Phase:** ${state.phase}

## Engagement Registry

${registry}

## Recent Decisions

${decisions}

## Blockers

${blockers}

## Next Action

${nextAction}
`;

  // Write atomically: temp file → rename. Prevents readers from seeing
  // a partially-written STATE.md if the process is interrupted mid-write.
  // CWE-362: Race Condition / non-atomic file write prevention.
  const statePath = join(nerfDir(root), 'STATE.md');
  const tmpPath = statePath + '.tmp';
  writeFileSync(tmpPath, content, 'utf-8');
  renameSync(tmpPath, statePath);
}

// ---------------------------------------------------------------------------
// Engagement lifecycle
// ---------------------------------------------------------------------------

/**
 * Generate the next engagement ID by scanning existing engagement directories.
 * @param {string} [root=process.cwd()]
 * @returns {string} - e.g. 'E001'
 */
export function nextEngagementId(root = process.cwd()) {
  const engDir = join(nerfDir(root), 'engagements');
  mkdirSync(engDir, { recursive: true });

  const existing = readdirSync(engDir)
    .filter(d => /^E\d{3}$/.test(d))
    .map(d => parseInt(d.slice(1), 10))
    .sort((a, b) => a - b);

  const next = existing.length ? existing[existing.length - 1] + 1 : 1;
  return `E${String(next).padStart(3, '0')}`;
}

/**
 * Create a new engagement.
 * @param {Object} params
 * @param {string} params.mission - What we are trying to accomplish
 * @param {string} [params.type] - pentest|ir|audit|build|review
 * @param {string[]} [params.targets] - In-scope targets
 * @param {string[]} [params.constraints] - Scope limits, RoE
 * @param {string} [root=process.cwd()]
 * @returns {{ eid: string, dir: string }}
 */
export function createEngagement({ mission, type = 'pentest', targets = [], constraints = [] }, root = process.cwd()) {
  const eid = nextEngagementId(root);
  const dir = engagementDir(eid, root);

  // Create ENGAGEMENT.md
  const engagementMd = `# Engagement ${eid}

## Mission

${mission}

## Type

${type}

## Targets

${targets.length ? targets.map(t => `- ${t}`).join('\n') : '_(none specified)_'}

## Constraints

${constraints.length ? constraints.map(c => `- ${c}`).join('\n') : '_(none specified)_'}

## Phase

research

## Active Hypotheses

_(none yet)_

## Resolved Findings

_(none yet)_

## Detection Opportunities

_(none yet)_

## Open Questions

_(none yet)_

## Next Actions

1. Research targets and threat landscape
2. Plan campaigns
3. Execute
`;

  writeFileSync(join(nerfDir(root), 'ENGAGEMENT.md'), engagementMd, 'utf-8');

  // Create empty roadmap
  writeFileSync(join(dir, `${eid}-ROADMAP.md`), `# ${eid} Roadmap\n\n## Campaigns\n\n_(none planned)_\n`, 'utf-8');

  // Create context doc (operator decisions from discuss phase)
  writeFileSync(join(dir, `${eid}-CONTEXT.md`), `# ${eid} Context\n\n## Operator Decisions\n\n_(none yet)_\n`, 'utf-8');

  // Create research doc
  writeFileSync(join(dir, `${eid}-RESEARCH.md`), `# ${eid} Research\n\n## Target Analysis\n\n_(pending)_\n\n## Threat Landscape\n\n_(pending)_\n`, 'utf-8');

  // Update state
  writeState(
    { activeEngagement: eid, phase: 'research', activeCampaign: null, activeTask: null },
    {
      registry: [`⬜ **${eid}:** ${mission.slice(0, 80)}`],
      nextAction: `Research phase for ${eid}`,
    },
    root,
  );

  return { eid, dir };
}

// ---------------------------------------------------------------------------
// Campaign lifecycle
// ---------------------------------------------------------------------------

/**
 * Generate the next campaign ID within an engagement.
 * @param {string} eid
 * @param {string} [root=process.cwd()]
 * @returns {string} - e.g. 'C01'
 */
export function nextCampaignId(eid, root = process.cwd()) {
  const cDir = join(engagementDir(eid, root), 'campaigns');
  mkdirSync(cDir, { recursive: true });

  const existing = readdirSync(cDir)
    .filter(d => /^C\d{2}$/.test(d))
    .map(d => parseInt(d.slice(1), 10))
    .sort((a, b) => a - b);

  const next = existing.length ? existing[existing.length - 1] + 1 : 1;
  return `C${String(next).padStart(2, '0')}`;
}

/**
 * Create a campaign within an engagement.
 * @param {Object} params
 * @param {string} params.eid
 * @param {string} params.title
 * @param {string} params.goal
 * @param {string} [params.risk] - low|medium|high
 * @param {string[]} [params.depends] - Campaign dependencies
 * @param {string} [root=process.cwd()]
 * @returns {{ cid: string, dir: string }}
 */
export function createCampaign({ eid, title, goal, risk = 'medium', depends = [] }, root = process.cwd()) {
  const cid = nextCampaignId(eid, root);
  const dir = campaignDir(eid, cid, root);

  const planMd = `# ${cid}: ${title}

## Goal

${goal}

## Risk

${risk}

## Dependencies

${depends.length ? depends.map(d => `- ${d}`).join('\n') : '_(none)_'}

## Tasks

_(none planned)_

## Findings

_(none yet)_
`;

  writeFileSync(join(dir, `${cid}-PLAN.md`), planMd, 'utf-8');
  writeFileSync(join(dir, `${cid}-FINDINGS.md`), `# ${cid} Findings\n\n_(none yet)_\n`, 'utf-8');

  // Create tasks dir
  mkdirSync(join(dir, 'tasks'), { recursive: true });

  return { cid, dir };
}

// ---------------------------------------------------------------------------
// Task lifecycle
// ---------------------------------------------------------------------------

/**
 * Generate the next task ID within a campaign.
 * @param {string} eid
 * @param {string} cid
 * @param {string} [root=process.cwd()]
 * @returns {string} - e.g. 'T01'
 */
export function nextTaskId(eid, cid, root = process.cwd()) {
  const tDir = tasksDir(eid, cid, root);

  const existing = readdirSync(tDir)
    .filter(f => /^T\d{2}-/.test(f))
    .map(f => parseInt(f.slice(1, 3), 10))
    .sort((a, b) => a - b);

  // Deduplicate
  const unique = [...new Set(existing)];
  const next = unique.length ? unique[unique.length - 1] + 1 : 1;
  return `T${String(next).padStart(2, '0')}`;
}

/**
 * Create a task within a campaign.
 * @param {Object} params
 * @param {string} params.eid
 * @param {string} params.cid
 * @param {string} params.title
 * @param {string} params.description
 * @param {string[]} [params.mustHaves] - Verification criteria
 * @param {string[]} [params.files] - Files likely touched
 * @param {string} [params.verify] - Verification command
 * @param {string} [root=process.cwd()]
 * @returns {{ tid: string, path: string }}
 */
export function createTask({ eid, cid, title, description, mustHaves = [], files = [], verify = '' }, root = process.cwd()) {
  const tid = nextTaskId(eid, cid, root);
  const dir = tasksDir(eid, cid, root);

  const planMd = `# ${tid}: ${title}

## Description

${description}

## Must-Haves

${mustHaves.length ? mustHaves.map(m => `- [ ] ${m}`).join('\n') : '_(none specified)_'}

## Files

${files.length ? files.map(f => `- ${f}`).join('\n') : '_(to be determined)_'}

## Verify

${verify || '_(manual verification)_'}
`;

  const planPath = join(dir, `${tid}-PLAN.md`);
  writeFileSync(planPath, planMd, 'utf-8');
  return { tid, path: planPath };
}

/**
 * Write a task summary after completion.
 * @param {Object} params
 * @param {string} params.eid
 * @param {string} params.cid
 * @param {string} params.tid
 * @param {string} params.oneLiner
 * @param {string} params.narrative
 * @param {string} params.verification
 * @param {string} [params.deviations]
 * @param {string[]} [params.keyFiles]
 * @param {string[]} [params.findings]
 * @param {string} [root=process.cwd()]
 * @returns {string} - Path to summary file
 */
export function completeTask({ eid, cid, tid, oneLiner, narrative, verification, deviations = 'None.', keyFiles = [], findings = [] }, root = process.cwd()) {
  const dir = tasksDir(eid, cid, root);

  // Verify the task plan exists
  const planPath = join(dir, `${tid}-PLAN.md`);
  if (!existsSync(planPath)) {
    throw new Error(`Task plan not found: ${planPath}. Create the task before completing it.`);
  }

  const ts = new Date().toISOString();

  const summaryMd = `---
task: ${tid}
campaign: ${cid}
engagement: ${eid}
completed: ${ts}
oneLiner: "${oneLiner.replace(/"/g, '\\"')}"
---

# ${tid} Summary

${narrative}

## Verification

${verification}

## Deviations

${deviations}

## Key Files

${keyFiles.length ? keyFiles.map(f => `- ${f}`).join('\n') : '_(none)_'}

## Findings

${findings.length ? findings.map(f => `- ${f}`).join('\n') : '_(none)_'}
`;

  const summaryPath = join(dir, `${tid}-SUMMARY.md`);
  writeFileSync(summaryPath, summaryMd, 'utf-8');
  return summaryPath;
}

// ---------------------------------------------------------------------------
// Cost tracking
// ---------------------------------------------------------------------------

/**
 * @typedef {Object} CostEntry
 * @property {string} unit - E001/C01/T01
 * @property {string} model - Model used
 * @property {number} inputTokens
 * @property {number} outputTokens
 * @property {number} cost - USD
 * @property {string} timestamp
 */

/**
 * Append a cost entry to .nerf/metrics.json
 * @param {CostEntry} entry
 * @param {string} [root=process.cwd()]
 */
export function trackCost(entry, root = process.cwd()) {
  // Append-only JSONL format eliminates the read-modify-write race condition.
  // Each line is one JSON entry; engagementCost() sums them at read time.
  // CWE-362: Race Condition / non-atomic file write prevention.
  const metricsPath = join(nerfDir(root), 'metrics.jsonl');
  const line = JSON.stringify({ ...entry, timestamp: entry.timestamp || new Date().toISOString() });
  appendFileSync(metricsPath, line + '\n', 'utf-8');
}

/**
 * Get total cost for an engagement.
 * @param {string} eid
 * @param {string} [root=process.cwd()]
 * @returns {{ totalCost: number, totalInputTokens: number, totalOutputTokens: number, entries: number }}
 */
export function engagementCost(eid, root = process.cwd()) {
  // Read from JSONL format (one JSON entry per line — append-only).
  // Fall back to legacy metrics.json if present during migration.
  const jsonlPath = join(nerfDir(root), 'metrics.jsonl');
  const jsonPath  = join(nerfDir(root), 'metrics.json');

  let metrics = [];

  if (existsSync(jsonlPath)) {
    try {
      metrics = readFileSync(jsonlPath, 'utf-8')
        .split('\n')
        .filter(Boolean)
        .map(line => { try { return JSON.parse(line); } catch { return null; } })
        .filter(Boolean);
    } catch { /* return empty */ }
  } else if (existsSync(jsonPath)) {
    // Legacy JSON array fallback
    try { metrics = JSON.parse(readFileSync(jsonPath, 'utf-8')); } catch { /* return empty */ }
  }

  const filtered = metrics.filter(m => m.unit?.startsWith(eid));
  return {
    totalCost: filtered.reduce((sum, m) => sum + (m.cost || 0), 0),
    totalInputTokens: filtered.reduce((sum, m) => sum + (m.inputTokens || 0), 0),
    totalOutputTokens: filtered.reduce((sum, m) => sum + (m.outputTokens || 0), 0),
    entries: filtered.length,
  };
}
