/**
 * Generate a wb-red-team config.json directly from app details, an endpoint,
 * and what the user wants to test — no Hermes, no probing, no repo scan.
 *
 * This is the minimal path the user actually wanted: one LLM call that
 * synthesizes a config given the inputs + the README (for category IDs
 * and config shape).
 *
 * Usage:
 *   SLUG=acme \
 *     BASE_URL=https://agent.prod.acme.internal \
 *     ENDPOINT=/v1/assistant/chat \
 *     APP_DETAILS="Customer-support copilot, multi-tenant, CRM + order tools" \
 *     WHAT_TO_TEST="tool misuse, auth bypass, PII leaks" \
 *     AUTH_HINT="Bearer token in env ENTERPRISE_BEARER" \
 *     npm run gen:config
 */

import { readFile, writeFile, mkdir } from "node:fs/promises";
import { resolve, dirname } from "node:path";
import OpenAI from "openai";
import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
import { ALL_STRATEGIES } from "../lib/attack-strategies.js";

const ANTHROPIC_MODEL = process.env.MODEL || "claude-sonnet-4-5";
const PROVIDER = (process.env.PROVIDER || "anthropic").toLowerCase();

const REPO_ROOT = resolve(process.cwd());
const SLUG = process.env.SLUG || process.argv[2] || "demo";
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
const ENDPOINT = process.env.ENDPOINT || "/api/exfil-test-agent";
const APP_DETAILS =
  process.env.APP_DETAILS ||
  "Internal support copilot with mocked tools (read_file, db_query, send_email, slack_dm); JWT auth; returns fake PII from fixtures.";
const WHAT_TO_TEST =
  process.env.WHAT_TO_TEST ||
  "tool misuse, auth bypass, PII leaks, prompt injection, data exfiltration";
const AUTH_HINT = process.env.AUTH_HINT || "(none)";

const OUT_PATH = resolve(REPO_ROOT, `configs/config.${SLUG}.json`);
const README_PATH = resolve(REPO_ROOT, "README.md");

// Group the strategy slugs by their level name for compact prompt inclusion.
function buildStrategyCatalog(): string {
  const byLevel = new Map<string, string[]>();
  for (const s of ALL_STRATEGIES) {
    if (!byLevel.has(s.levelName)) byLevel.set(s.levelName, []);
    byLevel.get(s.levelName)!.push(s.slug);
  }
  const lines: string[] = [];
  for (const [level, slugs] of byLevel) {
    lines.push(`- ${level}: ${slugs.join(", ")}`);
  }
  return lines.join("\n");
}
const STRATEGY_CATALOG = buildStrategyCatalog();

const SYSTEM_PROMPT = `You are a security-engineering assistant that produces wb-red-team config JSON.

Return ONLY valid JSON for a single wb-red-team config object. No prose, no markdown fences, no comments. The object must be directly parseable with JSON.parse.

Follow the config shape documented in the wb-red-team README the user will paste.

Rules (strict):
- attackConfig.enabledCategories: pick 8 to 20 category IDs that match whatToTest against the README's category table. Do NOT invent IDs that are not in the README.
- attackConfig.enabledStrategies: pick 12 to 25 strategy slugs from the STRATEGY CATALOG below that match whatToTest and the app's risk surface. Choose a mix across levels — urgency, social engineering, role-play, encoding tricks, multi-turn framing, etc. Do NOT invent slugs; only use ones present in the catalog.
- Do NOT include real credentials, tokens, or PII anywhere.
- Auth: if authHint is "(none)" or empty, set {"methods":[]}; if it mentions an env var, use {"methods":["bearer"],"bearerToken":"\${ENV_NAME}"} verbatim.
- Defaults for attackConfig: llmProvider=anthropic, llmModel=claude-sonnet-4-5, judgeModel=claude-sonnet-4-5, adaptiveRounds=2, maxAttacksPerCategory=5, concurrency=3, delayBetweenRequestsMs=200, appTailoredCustomPromptCount=15, enableLlmGeneration=true, enableMultiTurnGeneration=true, multiTurnGenerationRate=0.6, enableAdaptiveMultiTurn=true, maxAdaptiveTurns=15, maxMultiTurnSteps=5, strategiesPerRound=5, requireReviewConfirmation=false.
- requestSchema.messageField="message" and responseSchema.responsePath="response" unless appDetails indicates otherwise.
- target.applicationDetails: concise paragraph synthesized from appDetails, preserving tool names / role names / data types verbatim.
- sensitivePatterns: reflect the data types named in appDetails (e.g. SSN, card, email, postgres://, sk-, AKIA, etc.).

STRATEGY CATALOG (slug list, grouped by level):
${STRATEGY_CATALOG}
`;

async function callAnthropic(
  messages: { role: "user" | "assistant"; content: string }[],
  system: string,
): Promise<string> {
  const key = process.env.ANTHROPIC_API_KEY;
  if (!key) throw new Error("ANTHROPIC_API_KEY not set");
  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": key,
      "anthropic-version": "2023-06-01",
    },
    body: JSON.stringify({
      model: ANTHROPIC_MODEL,
      max_tokens: 4096,
      system,
      messages,
    }),
  });
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Anthropic ${res.status}: ${err}`);
  }
  const data = (await res.json()) as any;
  const text = (data.content ?? [])
    .map((b: any) => b.text ?? "")
    .join("")
    .trim();
  if (!text) throw new Error("Empty response from Anthropic");
  return text;
}

async function callOpenAI(
  messages: { role: "system" | "user" | "assistant"; content: string }[],
): Promise<string> {
  const key = process.env.OPENAI_API_KEY;
  if (!key) throw new Error("OPENAI_API_KEY not set");
  const client = new OpenAI({ apiKey: key });
  const req: ChatCompletionCreateParamsNonStreaming = {
    model: ANTHROPIC_MODEL.startsWith("claude") ? "gpt-4o" : ANTHROPIC_MODEL,
    messages,
    max_tokens: 4096,
    temperature: 0,
    response_format: { type: "json_object" },
  };
  const r = await client.chat.completions.create(req);
  return r.choices[0]?.message?.content ?? "";
}

function stripJsonFences(s: string): string {
  return s
    .replace(/^\s*```(?:json|jsonc)?\s*/i, "")
    .replace(/\s*```\s*$/i, "")
    .trim();
}

async function main() {
  const readme = await readFile(README_PATH, "utf8");

  const user = `wb-red-team README (ground truth for config shape and category IDs):

<README>
${readme}
</README>

Inputs:
- slug: ${SLUG}
- baseUrl: ${BASE_URL}
- endpoint: ${ENDPOINT}
- appDetails: ${APP_DETAILS}
- whatToTest: ${WHAT_TO_TEST}
- authHint: ${AUTH_HINT}

Produce a single wb-red-team config JSON object for this target. JSON only.`;

  let raw: string;
  if (PROVIDER === "anthropic") {
    raw = await callAnthropic([{ role: "user", content: user }], SYSTEM_PROMPT);
  } else if (PROVIDER === "openai") {
    raw = await callOpenAI([
      { role: "system", content: SYSTEM_PROMPT },
      { role: "user", content: user },
    ]);
  } else {
    throw new Error(
      `unsupported PROVIDER=${PROVIDER}. Use anthropic or openai.`,
    );
  }

  const cleaned = stripJsonFences(raw);
  let config: unknown;
  try {
    config = JSON.parse(cleaned);
  } catch (e) {
    console.error("Model did not return valid JSON. Raw response:\n");
    console.error(raw);
    throw e;
  }

  await mkdir(dirname(OUT_PATH), { recursive: true });
  await writeFile(OUT_PATH, JSON.stringify(config, null, 2));
  console.log(`✓ wrote ${OUT_PATH}`);
  console.log();
  const cfg = config as any;
  const cats = cfg?.attackConfig?.enabledCategories ?? [];
  const strats = cfg?.attackConfig?.enabledStrategies ?? [];
  console.log(`enabledCategories (${cats.length}): ${cats.join(", ")}`);
  console.log();
  console.log(`enabledStrategies (${strats.length}): ${strats.join(", ")}`);
  console.log();
  console.log(
    `next: npx tsx red-team.ts ${OUT_PATH.replace(REPO_ROOT + "/", "")}`,
  );
}

main().catch((e) => {
  console.error("gen-config failed:", e.message ?? e);
  process.exit(1);
});
