import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as path from 'path';

import { atomicWriteAsync } from './atomicWrite';
import { getTeamBootstrapStatePath } from './TeamBootstrapStateReader';
import { getTeamLaunchStatePath, getTeamLaunchSummaryPath } from './TeamLaunchStateStore';

import type {
  MemberSpawnStatusEntry,
  PersistedTeamLaunchSnapshot,
  TeamLaunchDiagnosticItem,
  TeamLaunchFailureDiagnosticsBundle,
  TeamMember,
  TeamProviderBackendId,
  TeamProviderId,
  TeamProvisioningProgress,
} from '@shared/types';

const logger = createLogger('Service:TeamLaunchFailureArtifactPack');

const ARTIFACTS_DIR_NAME = 'launch-failure-artifacts';
const LATEST_ARTIFACT_FILE = 'latest.json';
const MAX_CLI_LOG_CHARS = 256_000;
const MAX_TRACE_CHARS = 128_000;
const MAX_COPIED_FILE_BYTES = 256 * 1024;
const MAX_DIAGNOSTICS_COPY_FILE_BYTES = 128 * 1024;
const RUNTIME_ARTIFACT_FILE_PATTERN = /^[^/\\]+(?:\.runtime\.jsonl|\.stdout\.log|\.stderr\.log)$/;
const RUNTIME_ARTIFACT_LABEL_PATTERN =
  /^runtime\/[^/\\]+(?:\.runtime\.jsonl|\.stdout\.log|\.stderr\.log)$/;

type JsonRecord = Record<string, unknown>;

export interface TeamLaunchFailureArtifactPackInput {
  teamName: string;
  runId: string;
  reason: string;
  startedAt?: string;
  cwd?: string;
  pid?: number | null;
  providerId?: TeamProviderId;
  providerBackendId?: TeamProviderBackendId;
  model?: string;
  expectedMembers?: readonly string[];
  effectiveMembers?: readonly TeamMember[];
  progress?: TeamProvisioningProgress | null;
  launchSnapshot?: PersistedTeamLaunchSnapshot | null;
  launchDiagnostics?: readonly TeamLaunchDiagnosticItem[];
  memberSpawnStatuses?: Record<string, MemberSpawnStatusEntry>;
  cliLogs?: string | null;
  progressTraceLines?: readonly string[];
  runtimeAdapterTraceLines?: readonly string[];
  flags?: JsonRecord;
}

export interface TeamLaunchFailureArtifactPackResult {
  directory: string;
  manifestPath: string;
  files: string[];
}

export type LaunchFailureArtifactClassificationCode =
  | 'workspace_trust_required'
  | 'transport_rejected'
  | 'stdin_missing'
  | 'provider_quota'
  | 'provider_auth'
  | 'process_readiness_timeout'
  | 'model_no_bootstrap'
  | 'process_exited'
  | 'opencode_protocol'
  | 'unknown';

export interface LaunchFailureArtifactClassification {
  code: LaunchFailureArtifactClassificationCode;
  confidence: number;
  evidence: string[];
}

export interface LaunchBootstrapTransportBreadcrumb {
  lastTransportStage: string | null;
  submitRejected: boolean;
  retryable: boolean | null;
  noStdinWarning: boolean;
  bootstrapSubmitted: boolean;
  evidence: string[];
}

interface CopiedArtifactFile {
  sourcePath: string;
  artifactName: string;
  issue?: string;
}

function sanitizeArtifactNamePart(value: string): string {
  const sanitized = value
    .trim()
    .replace(/[^a-zA-Z0-9._-]+/g, '-')
    .replace(/^-+|-+$/g, '');
  return sanitized || 'unknown';
}

function artifactTimestamp(now: Date): string {
  return now.toISOString().replace(/[:.]/g, '-');
}

function assertPathWithin(root: string, target: string): void {
  const relative = path.relative(path.resolve(root), path.resolve(target));
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
    throw new Error(`Launch artifact path escaped teams root: ${target}`);
  }
}

function truncateTail(text: string, maxChars: number): string {
  if (text.length <= maxChars) return text;
  return `[truncated to last ${maxChars} chars]\n${text.slice(text.length - maxChars)}`;
}

export function redactLaunchFailureArtifactText(text: string): string {
  return (
    text
      .replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED_ANTHROPIC_API_KEY]')
      .replace(/sk-proj-[A-Za-z0-9_-]{20,}/g, '[REDACTED_OPENAI_API_KEY]')
      .replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
      .replace(
        /\b(ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|OPENAI_API_KEY|CODEX_API_KEY|OPENROUTER_API_KEY|GEMINI_API_KEY)\s*=\s*("[^"]*"|'[^']*'|[^\s"'`]+)/gi,
        '$1=[REDACTED]'
      )
      // eslint-disable-next-line sonarjs/duplicates-in-character-class -- URL-safe token alphabet intentionally includes these literal characters.
      .replace(/\b(authorization:\s*bearer\s+)([A-Za-z0-9._~+/=-]{20,})/gi, '$1[REDACTED]')
      .replace(
        // eslint-disable-next-line sonarjs/regex-complexity, sonarjs/duplicates-in-character-class -- Secret redaction regex intentionally covers common token field spellings.
        /\b(api[_-]?key|token|access[_-]?token|refresh[_-]?token)(["']?\s*[:=]\s*["']?)([A-Za-z0-9._~+/=-]{20,})/gi,
        '$1$2[REDACTED]'
      )
  );
}

function redactJsonLike<T>(value: T): T {
  return redactJsonValue(value) as T;
}

function isSecretJsonKey(key: string): boolean {
  return /^(api[_-]?key|token|access[_-]?token|refresh[_-]?token|authorization)$/i.test(key);
}

function redactJsonValue(value: unknown, key = ''): unknown {
  if (isSecretJsonKey(key)) {
    return '[REDACTED]';
  }
  if (typeof value === 'string') {
    return redactLaunchFailureArtifactText(value);
  }
  if (Array.isArray(value)) {
    return value.map((item) => redactJsonValue(item));
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value as JsonRecord).map(([entryKey, entryValue]) => [
        entryKey,
        redactJsonValue(entryValue, entryKey),
      ])
    );
  }
  return value;
}

function appendIfString(parts: string[], value: unknown): void {
  if (typeof value === 'string' && value.trim()) {
    parts.push(value.trim());
  }
}

function collectLaunchFailureSearchParts(input: TeamLaunchFailureArtifactPackInput): string[] {
  const parts: string[] = [];
  appendIfString(parts, input.reason);
  appendIfString(parts, input.cliLogs);
  for (const line of input.progressTraceLines ?? []) appendIfString(parts, line);
  for (const line of input.runtimeAdapterTraceLines ?? []) appendIfString(parts, line);
  appendIfString(parts, input.progress?.message);
  appendIfString(parts, input.progress?.error);
  appendIfString(parts, input.progress?.cliLogsTail);
  for (const warning of input.progress?.warnings ?? []) appendIfString(parts, warning);
  for (const diagnostic of input.launchDiagnostics ?? input.progress?.launchDiagnostics ?? []) {
    appendIfString(parts, diagnostic.code);
    appendIfString(parts, diagnostic.label);
    appendIfString(parts, diagnostic.detail);
  }
  for (const [memberName, entry] of Object.entries(input.memberSpawnStatuses ?? {})) {
    appendIfString(parts, memberName);
    appendIfString(parts, entry.status);
    appendIfString(parts, entry.launchState);
    appendIfString(parts, entry.error);
    appendIfString(parts, entry.hardFailureReason);
    appendIfString(parts, entry.runtimeDiagnostic);
  }
  if (input.launchSnapshot) {
    appendIfString(parts, input.launchSnapshot.launchPhase);
    appendIfString(parts, input.launchSnapshot.teamLaunchState);
    for (const [memberName, member] of Object.entries(input.launchSnapshot.members)) {
      appendIfString(parts, memberName);
      appendIfString(parts, member.launchState);
      appendIfString(parts, member.hardFailureReason);
      appendIfString(parts, member.runtimeDiagnostic);
      for (const diagnostic of member.diagnostics ?? []) appendIfString(parts, diagnostic);
    }
  }
  return parts;
}

function firstEvidence(parts: readonly string[], pattern: RegExp): string[] {
  const evidence: string[] = [];
  for (const part of parts) {
    if (pattern.test(part)) {
      evidence.push(truncateTail(part, 600));
      if (evidence.length >= 3) break;
    }
  }
  return evidence;
}

const WORKSPACE_TRUST_FAILURE_PATTERN =
  /workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust|workspace_trust_preflight_not_confirmed|workspace trust was not confirmed|workspace trust preflight blocked launch/i;

const BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN = new RegExp(
  [
    'mailbox_bootstrap_written',
    'startup_checkpoint',
    'last runtime stage',
    'bootstrap_prompt_observed',
    'bootstrap_submit_attempted',
    'bootstrap_submitted',
    'inbox_poller_ready',
    'runtime_events_log',
  ].join('|'),
  'i'
);

const MODEL_NO_BOOTSTRAP_PATTERN = new RegExp(
  [
    'did not bootstrap-confirm',
    'bootstrap unconfirmed',
    'bootstrap-confirm before timeout',
    'bootstrap was not confirmed',
    'bootstrap not confirmed',
    'check-in not yet received',
    'bootstrap_stalled',
    'did not submit bootstrap prompt',
    'bootstrap_submit_accepted_without_uuid',
    'timed out waiting for bootstrap_submitted',
    'last transport stage:\\s*(?:mailbox_bootstrap_written|bootstrap_prompt_observed|bootstrap_submit_attempted|bootstrap_submitted)',
  ].join('|'),
  'i'
);

export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
  return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
}

export function classifyLaunchFailureArtifact(
  input: TeamLaunchFailureArtifactPackInput
): LaunchFailureArtifactClassification {
  const parts = collectLaunchFailureSearchParts(input);
  const text = parts.join('\n').toLowerCase();
  const hasBootstrapTransportEvidence = BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN.test(text);
  const candidates: {
    code: LaunchFailureArtifactClassificationCode;
    confidence: number;
    pattern: RegExp;
  }[] = [
    {
      code: 'workspace_trust_required',
      confidence: 0.96,
      pattern: WORKSPACE_TRUST_FAILURE_PATTERN,
    },
    {
      code: 'transport_rejected',
      confidence: 0.95,
      pattern: /bootstrap_submit_rejected|submit rejected by local prompt handler/i,
    },
    {
      code: 'stdin_missing',
      confidence: 0.9,
      pattern: /no stdin data received|proceeding without it/i,
    },
    {
      code: 'provider_quota',
      confidence: 0.92,
      pattern: /quota exhausted|insufficient credits|key limit exceeded|total limit|rate limit/i,
    },
    {
      code: 'provider_auth',
      confidence: 0.88,
      pattern:
        /401 unauthorized|not_logged_in|login required|auth(?:entication)? failed|api key.*(?:missing|invalid)|token refresh failed/i,
    },
    {
      code: 'process_readiness_timeout',
      confidence: 0.9,
      pattern:
        /did not become (?:runtime_ready|inbox_poller_ready)|timed out waiting for (?:runtime_ready|inbox_poller_ready)/i,
    },
    {
      code: 'opencode_protocol',
      confidence: 0.84,
      pattern:
        /visible_reply_still_required|non_visible_tool_without_task_progress|empty_assistant_turn|runtime_bootstrap_checkin/i,
    },
    {
      code: 'model_no_bootstrap',
      confidence: 0.82,
      pattern: MODEL_NO_BOOTSTRAP_PATTERN,
    },
    {
      code: 'process_exited',
      confidence: 0.78,
      pattern: /process exited|pid is not alive|pid was not found|stale_metadata|exited before/i,
    },
  ];

  for (const candidate of candidates) {
    if (candidate.code === 'stdin_missing' && hasBootstrapTransportEvidence) {
      continue;
    }
    if (candidate.pattern.test(text)) {
      return {
        code: candidate.code,
        confidence: candidate.confidence,
        evidence: firstEvidence(parts, candidate.pattern).map(redactLaunchFailureArtifactText),
      };
    }
  }
  return {
    code: 'unknown',
    confidence: 0.2,
    evidence: firstEvidence(parts, /failed|error|timeout/i).map(redactLaunchFailureArtifactText),
  };
}

export function extractLaunchBootstrapTransportBreadcrumb(
  input: TeamLaunchFailureArtifactPackInput
): LaunchBootstrapTransportBreadcrumb {
  const parts = collectLaunchFailureSearchParts(input);
  const combined = parts.join('\n');
  const lastStageMatches = [
    ...combined.matchAll(/last (?:transport|runtime) stage:\s*([^;\n]+)/gi),
  ];
  const retryableMatches = [
    ...combined.matchAll(/bootstrap_submit_rejected[^\n]*(?:retryable[=:]\s*(true|false))/gi),
  ];
  const evidence = firstEvidence(
    parts,
    /bootstrap_submit_|mailbox_bootstrap_written|startup_checkpoint|bootstrap_prompt_observed|bootstrap_submitted|last (?:transport|runtime) stage|no stdin data received|local prompt handler/i
  ).map(redactLaunchFailureArtifactText);
  const retryableRaw = retryableMatches.at(-1)?.[1]?.toLowerCase();
  return {
    lastTransportStage: normalizeLastTransportStage(lastStageMatches.at(-1)?.[1]),
    submitRejected: /bootstrap_submit_rejected|submit rejected by local prompt handler/i.test(
      combined
    ),
    retryable: retryableRaw === 'true' ? true : retryableRaw === 'false' ? false : null,
    noStdinWarning: /no stdin data received|proceeding without it/i.test(combined),
    bootstrapSubmitted:
      /(?:(?:event|type)["']?\s*[:=]\s*["']bootstrap_submitted["']|bootstrap_submit_accepted|bootstrap submitted)/i.test(
        combined
      ),
    evidence,
  };
}

function normalizeLastTransportStage(stage: string | undefined): string | null {
  const normalized = stage?.replace(/\s+Last\s+(?:stderr|stdout):.*$/i, '').trim();
  return normalized || null;
}

async function readBoundedTextFile(sourcePath: string): Promise<{ text?: string; issue?: string }> {
  try {
    const stat = await fs.promises.stat(sourcePath);
    if (!stat.isFile()) {
      return { issue: 'not_regular_file' };
    }
    const handle = await fs.promises.open(sourcePath, 'r');
    try {
      const start = Math.max(0, stat.size - MAX_COPIED_FILE_BYTES);
      const buffer = Buffer.alloc(stat.size - start);
      if (buffer.length > 0) {
        await handle.read(buffer, 0, buffer.length, start);
      }
      const prefix = start > 0 ? `[truncated to last ${MAX_COPIED_FILE_BYTES} bytes]\n` : '';
      return { text: `${prefix}${buffer.toString('utf8')}` };
    } finally {
      await handle.close().catch(() => undefined);
    }
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    return { issue: code === 'ENOENT' ? 'missing' : 'unreadable' };
  }
}

async function readDiagnosticsCopyFile(
  label: string,
  sourcePath: string
): Promise<TeamLaunchFailureDiagnosticsBundle['files'][number]> {
  const read = await readBoundedTextFile(sourcePath);
  if (read.text === undefined) {
    return {
      label,
      path: sourcePath,
      issue: read.issue ?? 'unreadable',
    };
  }

  const text =
    read.text.length > MAX_DIAGNOSTICS_COPY_FILE_BYTES
      ? `[truncated to last ${MAX_DIAGNOSTICS_COPY_FILE_BYTES} chars]\n${read.text.slice(
          read.text.length - MAX_DIAGNOSTICS_COPY_FILE_BYTES
        )}`
      : read.text;

  return {
    label,
    path: sourcePath,
    content: redactLaunchFailureArtifactText(text).trimEnd(),
  };
}

function parseJsonObject(text: string | undefined): JsonRecord | null {
  if (!text) return null;
  try {
    const parsed = JSON.parse(text);
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
      ? (parsed as JsonRecord)
      : null;
  } catch {
    return null;
  }
}

function getString(value: unknown): string | undefined {
  return typeof value === 'string' && value.trim() ? value : undefined;
}

function getRecord(value: unknown): JsonRecord | null {
  return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonRecord) : null;
}

function getRuntimeArtifactLabels(manifestJson: JsonRecord | null): string[] {
  const artifactFiles = manifestJson?.artifactFiles;
  if (!Array.isArray(artifactFiles)) return [];
  return artifactFiles
    .filter((item): item is string => typeof item === 'string')
    .filter((item) => RUNTIME_ARTIFACT_LABEL_PATTERN.test(item))
    .sort();
}

function resolveArtifactManifestPath(
  teamDir: string,
  latestJson: JsonRecord | null,
  runId?: string
): { path?: string; issue?: string } {
  if (!latestJson) {
    return { issue: 'latest_json_unavailable' };
  }
  const latestRunId = getString(latestJson.runId);
  if (runId && latestRunId && latestRunId !== runId) {
    return { issue: `latest_run_mismatch:${latestRunId}` };
  }

  const manifestPath = getString(latestJson.manifestPath);
  if (!manifestPath) {
    return { issue: 'manifest_path_missing' };
  }

  try {
    assertPathWithin(path.join(teamDir, ARTIFACTS_DIR_NAME), manifestPath);
  } catch {
    return { issue: 'manifest_path_outside_artifacts_dir' };
  }
  return { path: manifestPath };
}

export async function readTeamLaunchFailureDiagnosticsBundle(
  teamName: string,
  runId?: string
): Promise<TeamLaunchFailureDiagnosticsBundle> {
  const teamDir = path.join(getTeamsBasePath(), teamName);
  const latestPath = path.join(teamDir, ARTIFACTS_DIR_NAME, LATEST_ARTIFACT_FILE);
  const latestFile = await readDiagnosticsCopyFile(
    'launch-failure-artifacts/latest.json',
    latestPath
  );
  const latestJson = parseJsonObject(latestFile.content);
  const resolvedManifest = resolveArtifactManifestPath(teamDir, latestJson, runId);
  const files: TeamLaunchFailureDiagnosticsBundle['files'] = [latestFile];

  let manifestJson: JsonRecord | null = null;
  if (resolvedManifest.path) {
    const manifestFile = await readDiagnosticsCopyFile(
      'launch-failure-artifacts/manifest.json',
      resolvedManifest.path
    );
    files.push(manifestFile);
    manifestJson = parseJsonObject(manifestFile.content);
  } else {
    files.push({
      label: 'launch-failure-artifacts/manifest.json',
      path:
        getString(latestJson?.manifestPath) ??
        path.join(teamDir, ARTIFACTS_DIR_NAME, 'manifest.json'),
      issue: resolvedManifest.issue ?? 'manifest_unavailable',
    });
  }

  if (resolvedManifest.path) {
    const artifactDirectory = path.dirname(resolvedManifest.path);
    for (const artifactName of getRuntimeArtifactLabels(manifestJson)) {
      files.push(
        await readDiagnosticsCopyFile(
          `launch-failure-artifacts/${artifactName}`,
          path.join(artifactDirectory, artifactName)
        )
      );
    }
  }

  files.push(
    await readDiagnosticsCopyFile(
      'bootstrap-journal.jsonl',
      path.join(teamDir, 'bootstrap-journal.jsonl')
    ),
    await readDiagnosticsCopyFile('launch-state.json', getTeamLaunchStatePath(teamName))
  );

  const classification = getRecord(manifestJson?.classification);
  const bootstrapTransportBreadcrumb = getRecord(manifestJson?.bootstrapTransportBreadcrumb);

  return {
    teamName,
    ...(runId
      ? { runId }
      : getString(latestJson?.runId)
        ? { runId: getString(latestJson?.runId) }
        : {}),
    latestPath,
    ...(getString(latestJson?.directory)
      ? { artifactDirectory: getString(latestJson?.directory) }
      : {}),
    ...(resolvedManifest.path ? { manifestPath: resolvedManifest.path } : {}),
    classification: classification
      ? {
          code: getString(classification.code),
          confidence:
            typeof classification.confidence === 'number' ? classification.confidence : undefined,
          evidence: Array.isArray(classification.evidence)
            ? classification.evidence.filter((item): item is string => typeof item === 'string')
            : undefined,
        }
      : null,
    bootstrapTransportBreadcrumb: bootstrapTransportBreadcrumb
      ? {
          lastTransportStage:
            typeof bootstrapTransportBreadcrumb.lastTransportStage === 'string'
              ? bootstrapTransportBreadcrumb.lastTransportStage
              : bootstrapTransportBreadcrumb.lastTransportStage === null
                ? null
                : undefined,
          submitRejected:
            typeof bootstrapTransportBreadcrumb.submitRejected === 'boolean'
              ? bootstrapTransportBreadcrumb.submitRejected
              : undefined,
          retryable:
            typeof bootstrapTransportBreadcrumb.retryable === 'boolean'
              ? bootstrapTransportBreadcrumb.retryable
              : bootstrapTransportBreadcrumb.retryable === null
                ? null
                : undefined,
          noStdinWarning:
            typeof bootstrapTransportBreadcrumb.noStdinWarning === 'boolean'
              ? bootstrapTransportBreadcrumb.noStdinWarning
              : undefined,
          bootstrapSubmitted:
            typeof bootstrapTransportBreadcrumb.bootstrapSubmitted === 'boolean'
              ? bootstrapTransportBreadcrumb.bootstrapSubmitted
              : undefined,
          evidence: Array.isArray(bootstrapTransportBreadcrumb.evidence)
            ? bootstrapTransportBreadcrumb.evidence.filter(
                (item): item is string => typeof item === 'string'
              )
            : undefined,
        }
      : null,
    files,
  };
}

async function getRuntimeLaunchArtifactSourceFiles(teamDir: string): Promise<CopiedArtifactFile[]> {
  const runtimeDir = path.join(teamDir, 'runtime');
  try {
    const entries = await fs.promises.readdir(runtimeDir, { withFileTypes: true });
    return entries
      .filter((entry) => entry.isFile() && RUNTIME_ARTIFACT_FILE_PATTERN.test(entry.name))
      .map((entry) => ({
        sourcePath: path.join(runtimeDir, entry.name),
        artifactName: `runtime/${entry.name}`,
      }))
      .sort((left, right) => left.artifactName.localeCompare(right.artifactName));
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    if (code === 'ENOENT') return [];
    logger.warn('[ArtifactPack] Failed to enumerate runtime artifacts', {
      teamDir,
      error: String(error),
    });
    return [];
  }
}

async function getKnownLaunchArtifactSourceFiles(teamName: string): Promise<CopiedArtifactFile[]> {
  const bootstrapStatePath = getTeamBootstrapStatePath(teamName);
  const teamDir = path.dirname(bootstrapStatePath);
  const launchFiles: CopiedArtifactFile[] = [
    {
      sourcePath: getTeamLaunchStatePath(teamName),
      artifactName: 'launch-state.json',
    },
    {
      sourcePath: getTeamLaunchSummaryPath(teamName),
      artifactName: 'launch-summary.json',
    },
    {
      sourcePath: bootstrapStatePath,
      artifactName: 'bootstrap-state.json',
    },
    {
      sourcePath: path.join(teamDir, 'bootstrap-journal.jsonl'),
      artifactName: 'bootstrap-journal.tail.jsonl',
    },
    {
      sourcePath: path.join(teamDir, '.bootstrap.lock', 'metadata.json'),
      artifactName: 'bootstrap-lock-metadata.json',
    },
  ];
  return [...launchFiles, ...(await getRuntimeLaunchArtifactSourceFiles(teamDir))];
}

async function writeArtifactTextFile(
  directory: string,
  artifactName: string,
  rawText: string,
  files: string[]
): Promise<void> {
  const targetPath = path.join(directory, artifactName);
  assertPathWithin(directory, targetPath);
  await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
  await atomicWriteAsync(targetPath, `${redactLaunchFailureArtifactText(rawText).trimEnd()}\n`);
  files.push(artifactName);
}

export async function writeTeamLaunchFailureArtifactPack(
  input: TeamLaunchFailureArtifactPackInput
): Promise<TeamLaunchFailureArtifactPackResult> {
  const teamsRoot = getTeamsBasePath();
  const teamDir = path.join(teamsRoot, input.teamName);
  const artifactsRoot = path.join(teamDir, ARTIFACTS_DIR_NAME);
  const createdAt = new Date();
  const directory = path.join(
    artifactsRoot,
    `${artifactTimestamp(createdAt)}-${sanitizeArtifactNamePart(input.runId)}`
  );
  assertPathWithin(teamsRoot, directory);
  await fs.promises.mkdir(directory, { recursive: true });

  const files: string[] = [];
  const copiedFiles: CopiedArtifactFile[] = [];

  if (input.cliLogs?.trim()) {
    await writeArtifactTextFile(
      directory,
      'cli-logs-tail.txt',
      truncateTail(input.cliLogs, MAX_CLI_LOG_CHARS),
      files
    );
  }
  if (input.progressTraceLines?.length) {
    await writeArtifactTextFile(
      directory,
      'progress-trace.txt',
      truncateTail(input.progressTraceLines.join('\n'), MAX_TRACE_CHARS),
      files
    );
  }
  if (input.runtimeAdapterTraceLines?.length) {
    await writeArtifactTextFile(
      directory,
      'runtime-adapter-trace.txt',
      truncateTail(input.runtimeAdapterTraceLines.join('\n'), MAX_TRACE_CHARS),
      files
    );
  }

  for (const source of await getKnownLaunchArtifactSourceFiles(input.teamName)) {
    const read = await readBoundedTextFile(source.sourcePath);
    if (read.text !== undefined) {
      await writeArtifactTextFile(directory, source.artifactName, read.text, files);
      copiedFiles.push(source);
    } else {
      copiedFiles.push({ ...source, issue: read.issue ?? 'unreadable' });
    }
  }

  const classification = classifyLaunchFailureArtifact(input);
  const bootstrapTransportBreadcrumb = extractLaunchBootstrapTransportBreadcrumb(input);
  const manifest = redactJsonLike({
    version: 1,
    createdAt: createdAt.toISOString(),
    reason: input.reason,
    classification,
    bootstrapTransportBreadcrumb,
    teamName: input.teamName,
    runId: input.runId,
    startedAt: input.startedAt,
    cwd: input.cwd,
    pid: input.pid ?? null,
    providerId: input.providerId,
    providerBackendId: input.providerBackendId,
    model: input.model,
    expectedMembers: input.expectedMembers ?? [],
    effectiveMembers: (input.effectiveMembers ?? []).map((member) => ({
      name: member.name,
      role: member.role,
      providerId: member.providerId,
      providerBackendId: member.providerBackendId,
      model: member.model,
      agentType: member.agentType,
      removedAt: member.removedAt,
    })),
    progress: input.progress ?? null,
    launchDiagnostics: input.launchDiagnostics ?? input.progress?.launchDiagnostics ?? [],
    memberSpawnStatuses: input.memberSpawnStatuses ?? {},
    launchSnapshot: input.launchSnapshot ?? null,
    flags: input.flags ?? {},
    artifactFiles: files,
    copiedFiles,
  });

  const manifestPath = path.join(directory, 'manifest.json');
  await atomicWriteAsync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
  files.unshift('manifest.json');

  await fs.promises.mkdir(artifactsRoot, { recursive: true });
  await atomicWriteAsync(
    path.join(artifactsRoot, LATEST_ARTIFACT_FILE),
    `${JSON.stringify(
      redactJsonLike({
        version: 1,
        createdAt: createdAt.toISOString(),
        teamName: input.teamName,
        runId: input.runId,
        reason: input.reason,
        directory,
        manifestPath,
      }),
      null,
      2
    )}\n`
  );

  logger.info(`[${input.teamName}] Wrote launch failure artifact pack`, {
    runId: input.runId,
    reason: input.reason,
    directory,
  });

  return { directory, manifestPath, files };
}
