import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { canonicalToolName } from "#cli/core/tool-metadata";

export const RUNTIME_SNAPSHOT_DIR = "runtime-snapshots";

export function sanitizeStructuredCloneValue<T>(value: T): T {
  return JSON.parse(JSON.stringify(value ?? null)) as T;
}

type MessageLike = Readonly<{
  role?: unknown;
  content?: unknown;
}>;

type CanonicalizedSystemMessages<T extends MessageLike> = Readonly<{
  systemPrompt: string;
  messages: T[];
  systemMessages: T[];
}>;

function appendContentText(content: unknown, parts: string[]): void {
  if (typeof content === "string") {
    parts.push(content);
    return;
  }

  if (Array.isArray(content)) {
    for (const item of content) {
      appendContentText(item, parts);
    }
    return;
  }

  if (!content || typeof content !== "object") {
    return;
  }

  const record = content as Record<string, unknown>;
  const type = typeof record.type === "string" ? record.type : "";
  if (type === "text" || type === "input_text" || type === "output_text") {
    if (typeof record.text === "string") {
      parts.push(record.text);
    }
    return;
  }

  if (typeof record.text === "string") {
    parts.push(record.text);
    return;
  }

  if ("content" in record) {
    appendContentText(record.content, parts);
  }
}

export function extractSystemMessageText(content: unknown): string {
  const parts: string[] = [];
  appendContentText(content, parts);
  return parts.join("");
}

function normalizeSegment(content: unknown): string {
  if (typeof content !== "string") {
    return "";
  }
  return content.trim();
}

function stripLeadingSystemPrompt(content: string, systemPrompt: unknown): string {
  const normalizedContent = normalizeSegment(content);
  const normalizedSystemPrompt = normalizeSegment(systemPrompt);
  if (!normalizedContent || !normalizedSystemPrompt) {
    return normalizedContent;
  }
  if (normalizedContent === normalizedSystemPrompt) {
    return "";
  }
  if (!normalizedContent.startsWith(normalizedSystemPrompt)) {
    return normalizedContent;
  }

  const rest = normalizedContent.slice(normalizedSystemPrompt.length);
  if (!rest) {
    return "";
  }
  if (!/^\s*\n/.test(rest)) {
    return normalizedContent;
  }

  return rest.replace(/^\s*\n\s*/, "").trim();
}

function pushUniqueSegment(
  segments: string[],
  seen: Set<string>,
  content: unknown,
  systemPrompt?: unknown,
): void {
  const normalized = stripLeadingSystemPrompt(
    normalizeSegment(content),
    systemPrompt,
  );
  if (!normalized || seen.has(normalized)) {
    return;
  }
  seen.add(normalized);
  segments.push(normalized);
}

export function canonicalizeSystemMessages<T extends MessageLike>(input: Readonly<{
  systemPrompt?: unknown;
  messages: readonly T[];
}>): CanonicalizedSystemMessages<T> {
  const segments: string[] = [];
  const seen = new Set<string>();
  const systemMessages: T[] = [];
  const messages: T[] = [];

  pushUniqueSegment(segments, seen, input.systemPrompt);

  for (const message of input.messages) {
    if (String(message?.role ?? "") === "system") {
      systemMessages.push(message);
      pushUniqueSegment(
        segments,
        seen,
        extractSystemMessageText(message?.content),
        input.systemPrompt,
      );
      continue;
    }
    messages.push(message);
  }

  return {
    systemPrompt: segments.join("\n\n"),
    messages,
    systemMessages,
  };
}

export function applyCcMockSystemPrompt(input: unknown): unknown {
  return input;
}

export function isDelegationRecordedFinalMessage(): boolean {
  return false;
}

export function normalizeRuntimeToolName(value: unknown): string {
  return canonicalToolName(value);
}

export function normalizeAskUserQuestions<T>(questions: readonly T[]): T[] {
  return [...questions];
}

export function normalizePersistedSubagentChildSummary(
  value: unknown,
): {
  sessionId: string;
  summary?: string;
  taskId?: string;
  taskName?: string;
} | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return null;
  }
  const record = value as Record<string, unknown>;
  const sessionId = String(record.sessionId ?? record.childSessionId ?? "").trim();
  if (!sessionId) {
    return null;
  }
  const summary = String(record.summary ?? "").trim() || undefined;
  const taskId = String(record.taskId ?? record.id ?? "").trim() || undefined;
  const taskName = String(record.taskName ?? record.name ?? "").trim() || undefined;
  return {
    sessionId,
    summary,
    taskId,
    taskName,
  };
}

export function readPersistedSubagentChildSummaries(metadata: unknown): Array<{
  sessionId: string;
  summary?: string;
  taskId?: string;
  taskName?: string;
}> {
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
    return [];
  }
  const record = metadata as Record<string, unknown>;
  const candidates = [
    record.subagentChildren,
    record.subagents,
    record.childSessions,
  ];
  for (const candidate of candidates) {
    if (!Array.isArray(candidate)) {
      continue;
    }
    return candidate
      .map((entry) => normalizePersistedSubagentChildSummary(entry))
      .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));
  }
  return [];
}

export function runtimeSnapshotPath(stateDir: string, sessionId: string): string {
  return join(resolve(stateDir), RUNTIME_SNAPSHOT_DIR, `${sessionId}.json`);
}

export async function readJsonFile<T>(filePath: string): Promise<T> {
  return JSON.parse(await readFile(filePath, "utf8")) as T;
}

export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
  await mkdir(dirname(filePath), { recursive: true });
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

export async function safeLoadRuntimeSnapshot<T>(input: {
  directory: string;
  sessionId: string;
}): Promise<T | null> {
  const filePath = join(input.directory, `${input.sessionId}.json`);
  try {
    return await readJsonFile<T>(filePath);
  } catch {
    return null;
  }
}

export function getSkillsCatalogRoots(cwd: string): string[] {
  const home = String(process.env.HOME ?? "").trim();
  const codeHome = String(process.env.CODEX_HOME ?? "").trim();
  const roots = [
    join(cwd, ".codex", "skills"),
    join(cwd, ".agents", "skills"),
    codeHome ? join(codeHome, "skills") : "",
    home ? join(home, ".codex", "skills") : "",
    home ? join(home, ".agents", "skills") : "",
  ].filter(Boolean);
  return Array.from(new Set(roots));
}

export async function scanMultipleSkillsDirectories(
  roots: readonly string[],
): Promise<Array<{ name: string; description: string }>> {
  const { readdir, stat } = await import("node:fs/promises");
  const skills = new Map<string, { name: string; description: string }>();

  async function scan(root: string): Promise<void> {
    let entries;
    try {
      entries = await readdir(root, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (!entry.isDirectory()) {
        continue;
      }
      const skillDir = join(root, entry.name);
      const skillFile = join(skillDir, "SKILL.md");
      try {
        const info = await stat(skillFile);
        if (!info.isFile()) {
          continue;
        }
        const content = await readFile(skillFile, "utf8");
        const description = content
          .split("\n")
          .map((line) => line.trim())
          .find((line) => line.length > 0 && !line.startsWith("#"))
          ?? entry.name;
        skills.set(entry.name, { description, name: entry.name });
      } catch {
        continue;
      }
    }
  }

  for (const root of roots) {
    await scan(root);
  }

  return [...skills.values()].sort((left, right) =>
    left.name.localeCompare(right.name),
  );
}
