// apps/web/lib/integrate/claude-dispatch.ts
// Dispatch build tasks to Claude Code CLI running inside the sandbox container.
//
// Mirrors codex-dispatch.ts exactly but for Anthropic's Claude Code CLI.
// Supports two auth modes:
//   "oauth" (default) — Claude Max subscription. Flat-rate billing, ~20x more
//     economical than API keys. Uses CLAUDE_CODE_OAUTH_TOKEN env var.
//   "apikey" — Standard Anthropic API key. Per-token billing ($100 burns in hours).
//     Uses ANTHROPIC_API_KEY env var. Only use for testing or when Max is unavailable.
//
// Set CLAUDE_CODE_AUTH_MODE=oauth|apikey to choose. Default: oauth.
// Credential store providerId: "claude-code" (both modes read from the same entry).

import type { AssignedTask } from "./task-dependency-graph";
import type { SpecialistRole } from "./task-dependency-graph";
import { getDecryptedCredential, getProviderBearerToken } from "@/lib/inference/ai-provider-internals";
import { lazyChildProcess, lazyUtil } from "@/lib/shared/lazy-node";

const SANDBOX_CONTAINER = process.env.SANDBOX_CONTAINER_ID ?? "dpf-sandbox-1";

// Timeout per task. Data-architect tasks need more time for schema design.
const CLAUDE_TASK_TIMEOUT_MS = 900_000;        // 15 minutes default
const CLAUDE_SCHEMA_TASK_TIMEOUT_MS = 1_200_000; // 20 minutes for schema tasks

export type ClaudeResult = {
  content: string;       // Claude's response text
  success: boolean;
  executedTools: Array<{ name: string; args: unknown; result: { success: boolean } }>;
  durationMs: number;
  sessionId?: string;    // Claude Code session ID (for session continuity across tasks)
};

/**
 * Auth credentials resolved from the credential store.
 *
 * OAuth mode (Max Plan): flat-rate subscription, ~20x cheaper than API keys
 *   for sustained build workloads. Injects CLAUDE_CODE_OAUTH_TOKEN env var.
 * API key mode: per-token billing via ANTHROPIC_API_KEY. Burns fast —
 *   $100 in a few hours vs. 5+ days on Max Plan.
 */
type ClaudeAuth =
  | { mode: "oauth"; tokenJson: string }  // raw access token string (sk-ant-oat01-...)
  | { mode: "apikey"; apiKey: string };

async function resolveClaudeAuth(providerId: string): Promise<ClaudeAuth> {
  // Auth mode is implicit in the provider ID:
  //   "anthropic-sub" → OAuth (Max Plan subscription, flat-rate)
  //   "anthropic"     → API key (per-token billing)
  const isOAuth = providerId === "anthropic-sub";

  if (!isOAuth) {
    const credential = await getDecryptedCredential(providerId);
    const apiKey = credential?.secretRef ?? credential?.cachedToken;
    if (!apiKey) {
      throw new Error(`No Anthropic API key for provider "${providerId}". Configure via Admin > AI Workforce > External Services.`);
    }
    return { mode: "apikey", apiKey };
  }

  // OAuth: use getProviderBearerToken which checks tokenExpiresAt and refreshes
  // automatically when the access token has expired. Access tokens expire every
  // few hours; using getDecryptedCredential directly would send an expired token
  // causing 401. CLAUDE_CODE_OAUTH_TOKEN takes the raw sk-ant-oat01-... string.
  const result = await getProviderBearerToken(providerId);
  if ("error" in result) {
    throw new Error(`OAuth token refresh failed for "${providerId}": ${result.error}. Re-authenticate via Admin > AI Providers > Anthropic Subscription.`);
  }
  return { mode: "oauth", tokenJson: result.token };
}

/**
 * Build context instructions for Claude Code based on the specialist role.
 * Same role instructions as codex-dispatch.ts.
 */
function buildClaudeInstructions(
  role: SpecialistRole,
  buildContext: string,
  priorResults?: string,
): string {
  const roleInstructions: Record<SpecialistRole, string> = {
    "data-architect": `You are a data architect working on a Prisma schema.
Key files:
- Schema: packages/db/prisma/schema.prisma
- Validate with: pnpm --filter @dpf/db exec prisma validate
- After changes: pnpm --filter @dpf/db exec prisma migrate dev --name <descriptive_name>
- Then: pnpm --filter @dpf/db exec prisma generate
- Enums use LOWERCASE values. Multi-word statuses use hyphens: "in-progress" not "in_progress".
- Every foreign key field (xxxId) needs @@index.
- Relations need inverse on BOTH sides.`,

    "software-engineer": `You are a software engineer building Next.js server actions and API routes.
Key patterns:
- Server actions: apps/web/lib/actions/<feature>.ts — "use server" directive, prisma queries
- API routes: apps/web/app/api/<feature>/route.ts — GET/POST/PATCH/DELETE handlers
- Always read an existing similar file first to match patterns.
- Typecheck with: pnpm exec tsc --noEmit`,

    "frontend-engineer": `You are a frontend engineer building Next.js pages and React components.
Key patterns:
- Pages: apps/web/app/(shell)/<feature>/page.tsx — server components with prisma queries
- Components: apps/web/components/<feature>/ — client components with "use client"
- Use Tailwind CSS. Match existing design patterns.
- Read an existing page first to understand the layout structure.
- Typecheck with: pnpm exec tsc --noEmit`,

    "qa-engineer": `You are a QA engineer verifying the build.
- Run tests: pnpm exec vitest run --reporter=verbose
- Run typecheck: pnpm exec tsc --noEmit
- Check for runtime errors in the build output
- Report specific failures with file paths and line numbers`,
  };

  const parts = [
    roleInstructions[role],
    "",
    "PROJECT CONTEXT:",
    buildContext.slice(0, 3000),
  ];

  if (priorResults) {
    parts.push("", "RESULTS FROM PRIOR TASKS:", priorResults.slice(0, 2000));
  }

  return parts.join("\n");
}

/**
 * Dispatch a single build task to Claude Code CLI inside the sandbox container.
 *
 * Auth modes (set CLAUDE_CODE_AUTH_MODE env var):
 *   "oauth" (default) — Max Plan subscription. Flat-rate, ~20x cheaper for builds.
 *   "apikey" — Per-token API billing. Fast to set up, expensive at scale.
 *
 * Flow:
 * 1. Resolve auth from portal credential store (OAuth token or API key)
 * 2. Write prompt to a temp file in the sandbox (avoids shell escaping issues)
 * 3. Inject auth env var + run: claude --bare -p - --dangerously-skip-permissions --output-format json
 * 4. Parse JSON output, extract .result field for content
 */
export async function dispatchClaudeTask(params: {
  task: AssignedTask;
  buildId: string;
  buildContext: string;
  priorResults?: string;
  providerId?: string;
  model?: string;
  sessionId?: string;   // Reuse a Claude Code session for cross-task context continuity
  onProgress?: (message: string) => void;
}): Promise<ClaudeResult> {
  const { task, buildContext, priorResults } = params;
  const providerId = params.providerId ?? "anthropic-sub";
  const model = params.model ?? "sonnet";
  const sessionId = params.sessionId;
  const role = task.specialist;

  // Resolve auth credentials (OAuth for Max Plan, or API key for per-token billing)
  let auth: ClaudeAuth;
  try {
    auth = await resolveClaudeAuth(providerId);
  } catch (err) {
    return {
      content: `Auth error: ${(err as Error).message}`,
      success: false,
      executedTools: [],
      durationMs: 0,
    };
  }

  const instructions = buildClaudeInstructions(role, buildContext, priorResults);

  const taskFiles = task.files
    .map(f => `- ${f.path} (${f.action}): ${f.purpose}`)
    .join("\n");

  const taskPrompt = [
    instructions,
    "",
    "CRITICAL: This is a pnpm monorepo. The Next.js app is at apps/web/. All file paths MUST use the full monorepo-relative path (e.g. apps/web/lib/... not lib/..., apps/web/app/... not app/...). The FILES section below has the authoritative paths — use those exactly. Working directory is /workspace (the monorepo root).",
    "",
    `TASK: ${task.title}`,
    "",
    task.task.implement || "",
    "",
    taskFiles ? `FILES (use these exact paths):\n${taskFiles}` : "",
    "",
    task.task.testFirst ? `TEST FIRST: ${task.task.testFirst}` : "",
    task.task.verify ? `VERIFY: ${task.task.verify}` : "",
  ].filter(Boolean).join("\n");

  const startMs = Date.now();
  // Use task-specific temp files to avoid collisions during parallel execution.
  const taskSlug = task.title.replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40).toLowerCase();
  const promptFile = `/tmp/claude-prompt-${taskSlug}.txt`;
  const tokenFile = `/tmp/claude-token-${taskSlug}.txt`;

  try {
    const execAsync = lazyUtil().promisify(lazyChildProcess().exec);

    // Ensure /workspace is writable by the node user (uid 1000).
    // Files may be root-owned from bootstrap or prior Codex runs.
    // Also set git config for node user (Claude Code may run git commands).
    await execAsync(
      `docker exec ${SANDBOX_CONTAINER} sh -c "chown -R node:node /workspace && su -s /bin/sh node -c 'git config --global user.email sandbox@dpf.local && git config --global user.name DPF-Sandbox' 2>/dev/null"`,
      { timeout: 15_000 },
    );

    // Write prompt to task-specific temp file in sandbox (avoids shell escaping
    // AND prevents parallel tasks from overwriting each other's prompts).
    // chmod 644 so the non-root node user can read it.
    const promptB64 = Buffer.from(taskPrompt).toString("base64");
    await execAsync(
      `docker exec ${SANDBOX_CONTAINER} sh -c "echo '${promptB64}' | base64 -d > ${promptFile} && chmod 644 ${promptFile}"`,
      { timeout: 5_000 },
    );

    // Inject auth credentials into the sandbox container.
    // Claude Code CLI must run as non-root (--dangerously-skip-permissions refuses root).
    // Write a runner script to the sandbox to avoid shell quoting issues
    // with $(cat ...) in Alpine's busybox ash.
    let authExportLine: string;
    let useBareflag: boolean;
    if (auth.mode === "oauth") {
      // OAuth (Max Plan): write raw token to task-specific temp file
      const tokenB64 = Buffer.from(auth.tokenJson).toString("base64");
      await execAsync(
        `docker exec ${SANDBOX_CONTAINER} sh -c "echo '${tokenB64}' | base64 -d > ${tokenFile} && chmod 644 ${tokenFile}"`,
        { timeout: 5_000 },
      );
      authExportLine = `export CLAUDE_CODE_OAUTH_TOKEN=$(cat ${tokenFile})`;
      useBareflag = false;  // --bare disables OAuth
    } else {
      // API key mode
      authExportLine = `export ANTHROPIC_API_KEY=${auth.apiKey}`;
      useBareflag = true;   // --bare is safe with API key
    }

    const modeLabel = auth.mode === "oauth" ? "Max Plan (OAuth)" : "API key (per-token)";
    const bareFlag = useBareflag ? "--bare " : "";
    const sessionFlag = sessionId ? `--session-id ${sessionId} ` : "";
    const timeoutMs = role === "data-architect" ? CLAUDE_SCHEMA_TASK_TIMEOUT_MS : CLAUDE_TASK_TIMEOUT_MS;

    // Write a runner script to avoid all shell quoting issues
    const runnerScript = `/tmp/claude-run-${taskSlug}.sh`;
    const script = [
      "#!/bin/sh",
      "cd /workspace",
      authExportLine,
      `exec claude ${bareFlag}${sessionFlag}-p - --dangerously-skip-permissions --output-format json --model ${model} < ${promptFile}`,
    ].join("\n");
    const scriptB64 = Buffer.from(script).toString("base64");
    await execAsync(
      `docker exec ${SANDBOX_CONTAINER} sh -c "echo '${scriptB64}' | base64 -d > ${runnerScript} && chmod 755 ${runnerScript}"`,
      { timeout: 5_000 },
    );

    console.log(`[claude-dispatch] Starting task "${task.title}" with ${model} [${modeLabel}]${sessionId ? ` [session: ${sessionId}]` : ""} in ${SANDBOX_CONTAINER} (timeout: ${timeoutMs / 1000}s)`);

    // Use spawn to stream stderr for progress updates.
    const { spawn: spawnCb } = lazyChildProcess();

    const { stdout, durationMs: elapsed } = await new Promise<{ stdout: string; durationMs: number }>((resolve, reject) => {
      const proc = spawnCb("docker", [
        "exec", "--user", "node", SANDBOX_CONTAINER, runnerScript,
      ]);

      let stdout = "";
      let timedOut = false;
      const timer = setTimeout(() => {
        timedOut = true;
        proc.kill("SIGTERM");
      }, timeoutMs);

      proc.stdout.on("data", (data: Buffer) => { stdout += data.toString(); });

      let stderrBuf = "";
      proc.stderr.on("data", (data: Buffer) => {
        const chunk = data.toString();
        stderrBuf += chunk;
        const lines = chunk.split("\n").filter(Boolean);
        for (const line of lines) {
          const trimmed = line.trim();
          if (trimmed.startsWith("Reading file:")) {
            params.onProgress?.(`Reading ${trimmed.replace("Reading file: ", "")}`);
          } else if (trimmed.startsWith("Writing file:") || trimmed.startsWith("Creating file:")) {
            params.onProgress?.(`Creating ${trimmed.replace(/^(Writing|Creating) file: /, "")}`);
          } else if (trimmed.startsWith("Editing file:")) {
            params.onProgress?.(`Editing ${trimmed.replace("Editing file: ", "")}`);
          } else if (trimmed.startsWith("Running bash command:") || trimmed.startsWith("Running command:")) {
            params.onProgress?.(`Running: ${trimmed.replace(/^Running( bash)? command: /, "").slice(0, 80)}`);
          } else if (trimmed === "Thinking...") {
            params.onProgress?.("Thinking...");
          }
        }
      });

      proc.on("close", (code: number | null) => {
        clearTimeout(timer);
        const d = Date.now() - startMs;
        if (timedOut) {
          reject(Object.assign(new Error(`Timed out after ${timeoutMs / 1000}s`), { stdout, killed: true }));
        } else if (code === 0 || stdout.trim()) {
          resolve({ stdout, durationMs: d });
        } else {
          console.error(`[claude-dispatch] Task "${task.title}" stderr: ${stderrBuf.slice(0, 500)}`);
          reject(Object.assign(new Error(`Exit code ${code}`), { stdout, code, stderr: stderrBuf }));
        }
      });

      proc.on("error", (err: Error) => {
        clearTimeout(timer);
        reject(err);
      });
    });

    // Parse JSON output — Claude Code --output-format json returns { result, session_id, ... }
    let content: string;
    let returnedSessionId: string | undefined;
    try {
      const parsed = JSON.parse(stdout.trim());
      content = typeof parsed.result === "string" ? parsed.result : JSON.stringify(parsed.result);
      returnedSessionId = parsed.session_id ?? undefined;
    } catch {
      content = stdout.trim();
    }

    console.log(`[claude-dispatch] Task "${task.title}" completed in ${(elapsed / 1000).toFixed(1)}s (${content.length} chars)${returnedSessionId ? ` [session: ${returnedSessionId}]` : ""}`);

    return {
      content: content || "Task completed with no output.",
      success: true,
      executedTools: [],
      durationMs: elapsed,
      sessionId: returnedSessionId,
    };
  } catch (err) {
    const durationMs = Date.now() - startMs;
    const execErr = err as { stdout?: string; stderr?: string; message?: string; code?: number; killed?: boolean };
    const output = (execErr.stdout ?? "") + "\n" + (execErr.stderr ?? "");

    if (execErr.killed) {
      console.warn(`[claude-dispatch] Task "${task.title}" killed after ${CLAUDE_TASK_TIMEOUT_MS / 1000}s timeout`);
      return {
        content: `Task timed out after ${CLAUDE_TASK_TIMEOUT_MS / 1000}s. Partial output:\n${output.slice(-2000)}`,
        success: false,
        executedTools: [],
        durationMs,
      };
    }

    if (output.trim()) {
      console.log(`[claude-dispatch] Task "${task.title}" exited with code ${execErr.code}. Output: ${output.slice(0, 200)}`);
      return {
        content: output.trim().slice(-5000),
        success: execErr.code === 0,
        executedTools: [],
        durationMs,
      };
    }

    console.error(`[claude-dispatch] Task "${task.title}" failed: ${execErr.message?.slice(0, 200)}`);
    return {
      content: `Claude Code CLI error: ${execErr.message?.slice(0, 1000) ?? "Unknown error"}`,
      success: false,
      executedTools: [],
      durationMs,
    };
  }
}
