import { readFile } from "node:fs/promises";
import { basename, isAbsolute, relative, resolve } from "node:path";
import { requestCodexAppServer } from "./codex-runtime.js";
import { projectPublicActivityItem } from "./public-thread-item.js";
import { createThreadPreferencesStore } from "./thread-preferences.js";
import { projectPublicDiffData } from "./public-diff.js";
import { sanitizePublicData, sanitizePublicText } from "./public-sanitizer.js";
import { safeChangeFiles } from "./safe-change-paths.js";

export async function listAccountThreads(
  config,
  accountId,
  input = {},
  requester = requestCodexAppServer
) {
  const account = accountFor(config, accountId);
  const result = await requester({
    accountId,
    codexCliPath: config.codexCliPath,
    codexHome: account.codexHome,
    method: "thread/list",
    params: compact({
      cursor: input.cursor,
      limit: input.limit ?? 50,
      sortKey: "updated_at",
      sortDirection: "desc",
      searchTerm: input.search,
      archived: Boolean(input.archived)
    })
  });

  const desktopProjects = await readDesktopProjects(account.codexHome);

  const preferences = createThreadPreferencesStore(config.dataDir);
  const pins = await preferences.pinnedKeys();
  const deleted = await preferences.deletedKeys();
  return {
    threads: (result.data ?? [])
      .filter((thread) => !deleted.has(`${accountId}:${thread.id}`))
      .map((thread) => ({
        ...publicThread(thread, config.workspaces, desktopProjects, config),
        ...(pins.has(`${accountId}:${thread.id}`) ? { pinned: true } : {})
      })),
    nextCursor: result.nextCursor ?? null
  };
}

export async function archiveAccountThread(
  config,
  accountId,
  threadId,
  requester = requestCodexAppServer
) {
  const account = accountFor(config, accountId);
  await requester({
    codexCliPath: config.codexCliPath,
    codexHome: account.codexHome,
    method: "thread/archive",
    params: { threadId }
  });
  return { archived: true, ownerId: accountId, threadId };
}

export async function deleteArchivedAccountThreadLocalData(
  config,
  accountId,
  threadId,
  requester = requestCodexAppServer
) {
  const archived = await listAccountThreads(config, accountId, { archived: true }, requester);
  if (!archived.threads.some((thread) => thread.id === threadId)) {
    const error = new Error("thread_not_archived");
    error.status = 409;
    throw error;
  }
  await createThreadPreferencesStore(config.dataDir).setDeleted(accountId, threadId, true);
  return { deleted: true, ownerId: accountId, threadId };
}

export async function renameAccountThread(config, accountId, threadId, value, requester = requestCodexAppServer) {
  const account = accountFor(config, accountId);
  const name = typeof value === "string" ? value.trim() : "";
  if (!name) {
    const error = new Error("thread_name_required");
    error.status = 400;
    throw error;
  }
  await requester({ codexCliPath: config.codexCliPath, codexHome: account.codexHome,
    method: "thread/name/set", params: { threadId, name } });
  return { renamed: true, ownerId: accountId, threadId, name };
}

export async function setAccountThreadPinned(config, accountId, threadId, pinned) {
  accountFor(config, accountId);
  if (typeof pinned !== "boolean") {
    const error = new Error("thread_pinned_boolean_required");
    error.status = 400;
    throw error;
  }
  await createThreadPreferencesStore(config.dataDir).setPinned(accountId, threadId, pinned);
  return { ownerId: accountId, threadId, pinned };
}

export async function listUnifiedThreads(config, input = {}, requester = requestCodexAppServer) {
  const cursors = decodeUnifiedCursor(input.cursor);
  const paginating = Boolean(input.cursor);
  const owners = paginating
    ? config.accounts.filter((owner) => typeof cursors[owner.id] === "string")
    : config.accounts;
  const results = await Promise.allSettled(owners.map(async (owner) => ({
    owner,
    result: await listAccountThreads(config, owner.id, {
      cursor: cursors[owner.id],
      search: input.search,
      archived: Boolean(input.archived),
      limit: input.archived && !input.cursor ? 20 : undefined
    }, requester)
  })));
  const threads = [];
  const errors = [];
  const nextCursors = Object.fromEntries(config.accounts
    .filter((owner) => Object.hasOwn(cursors, owner.id))
    .map((owner) => [owner.id, cursors[owner.id]]));
  for (let index = 0; index < results.length; index += 1) {
    const settled = results[index];
    const owner = owners[index];
    if (settled.status === "rejected") {
      errors.push({ ownerId: owner.id, ownerLabel: owner.label, error: "source_unavailable" });
      continue;
    }
    for (const thread of settled.value.result.threads) {
      threads.push({ ...thread, ownerId: owner.id, ownerLabel: owner.label });
    }
    nextCursors[owner.id] = settled.value.result.nextCursor ?? null;
  }
  const unique = [...new Map(threads.map((thread) => [`${thread.ownerId}:${thread.id}`, thread])).values()]
    .sort((left, right) => Date.parse(right.updatedAt ?? 0) - Date.parse(left.updatedAt ?? 0));
  const hasNextPage = Object.values(nextCursors).some((cursor) => typeof cursor === "string" && cursor.length > 0);
  return {
    threads: unique,
    nextCursor: hasNextPage ? encodeUnifiedCursor(nextCursors) : null,
    errors
  };
}

export async function listAllUnifiedThreads(config, input = {}, requester = requestCodexAppServer) {
  const results = await Promise.allSettled(config.accounts.map(async (owner) => {
    const threads = [];
    let cursor;
    const seenCursors = new Set();
    do {
      if (cursor) {
        if (seenCursors.has(cursor)) break;
        seenCursors.add(cursor);
      }
      const result = await listAccountThreads(config, owner.id, {
        cursor,
        search: input.search,
        archived: Boolean(input.archived)
      }, requester);
      for (const thread of result.threads) threads.push({ ...thread, ownerId: owner.id, ownerLabel: owner.label });
      cursor = result.nextCursor;
    } while (cursor);
    return { owner, threads };
  }));
  const threads = [];
  const errors = [];
  for (let index = 0; index < results.length; index += 1) {
    const settled = results[index];
    const owner = config.accounts[index];
    if (settled.status === "rejected") {
      errors.push({ ownerId: owner.id, ownerLabel: owner.label, error: "source_unavailable" });
      continue;
    }
    threads.push(...settled.value.threads);
  }
  const unique = [...new Map(threads.map((thread) => [`${thread.ownerId}:${thread.id}`, thread])).values()]
    .sort((left, right) => Date.parse(right.updatedAt ?? 0) - Date.parse(left.updatedAt ?? 0));
  return { threads: unique, errors };
}

function decodeUnifiedCursor(cursor) {
  if (!cursor) return {};
  try { return JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")); } catch { return {}; }
}

function encodeUnifiedCursor(cursors) {
  return Buffer.from(JSON.stringify(cursors), "utf8").toString("base64url");
}

export async function readAccountThread(
  config,
  accountId,
  threadId,
  requester = requestCodexAppServer
) {
  const account = accountFor(config, accountId);
  return requester({
    accountId,
    codexCliPath: config.codexCliPath,
    codexHome: account.codexHome,
    method: "thread/read",
    params: { threadId, includeTurns: true }
  });
}

export function publicThread(thread, workspaces = [], desktopProjects = {}, sanitizerConfig = {}) {
  const workspace = workspaceForThread(workspaces, thread.cwd);
  const workspaceRoot = workspace?.path;
  const projectRoot = threadProjectRoot(thread, desktopProjects, workspaceRoot, sanitizerConfig.projectsRoot);
  const projectLabel = projectRoot ? cwdProjectLabel(projectRoot) : undefined;
  return compact({
    id: thread.id,
    name: sanitizePublicText(thread.name, sanitizerConfig) || undefined,
    preview: sanitizePublicText(thread.preview ?? "", sanitizerConfig),
    status: thread.status?.type ?? thread.status ?? "notLoaded",
    workspaceId: workspace?.id,
    workspaceLabel: projectLabel,
    createdAt: timestamp(thread.createdAt),
    updatedAt: timestamp(thread.updatedAt)
  });
}

export function workspaceForThread(workspaces, cwd) {
  return workspaces.find((workspace) => pathContains(workspace.path, cwd));
}

export function threadEvents(thread, config = {}) {
  const events = [];
  const workspace = workspaceForThread(config.workspaces ?? [], thread.cwd);
  const itemConfig = { ...config, changeCwd: thread.cwd, changeWorkspacePath: workspace?.path };
  for (const turn of thread.turns ?? []) {
    const startedAt = milliseconds(turn.startedAt);
    const completedAt = milliseconds(turn.completedAt);
    const items = turn.items ?? [];

    for (const item of items.filter((item) => item.type === "userMessage")) {
      projectItem(events, turn.id, item, startedAt, itemConfig);
    }

    events.push(historyEvent(turn.id, "turn.started", {
      turnId: turn.id,
      status: "running",
      startedAt
    }, startedAt));

    for (const item of items.filter((item) => item.type !== "userMessage")) {
      projectItem(events, turn.id, item, startedAt, itemConfig);
    }

    if (isCompletedTurn(turn)) {
      events.push(historyEvent(`${turn.id}-completed`, "turn.completed", compact({
        turnId: turn.id,
        status: turn.status?.type ?? turn.status ?? "completed",
        startedAt,
        completedAt,
        durationMs: turn.durationMs,
        error: redactPublicText(turn.error?.message ?? "", config) || undefined
      }), completedAt ?? startedAt));
    }
  }
  return sanitizePublicData(projectPublicDiffData(events), config);
}

function projectItem(events, turnId, item, time, config) {
  if (item.type === "userMessage") {
    const content = item.content ?? [];
    const text = content
      .filter((content) => content.type === "text")
      .map((content) => content.text)
      .join("\n");
    const attachmentCount = content.filter((entry) => ["image", "localImage"].includes(entry.type)).length;
    events.push(historyEvent(item.id, "message.accepted", { turnId, text, attachmentCount }, time));
    return;
  }

  if (item.type === "agentMessage") {
    events.push(historyEvent(`${item.id}-started`, "activity.started", {
      turnId,
      itemId: item.id,
      kind: "agentMessage",
      phase: item.phase,
      summary: item.phase === "commentary" ? "Progress update" : "Writing response",
      status: "completed"
    }, time));
    events.push(historyEvent(item.id, "session.output", {
      turnId,
      itemId: item.id,
      kind: "assistant_delta",
      text: redactPublicText(projectAssistantText(item.text ?? "", config), config)
    }, time));
    return;
  }

  if (item.type === "reasoning") {
    events.push(historyEvent(item.id, "reasoning.delta", {
      turnId,
      itemId: item.id,
      text: redactPublicText((item.summary ?? []).join("\n"), config)
    }, time));
    return;
  }

  if (item.type === "plan") {
    events.push(historyEvent(item.id, "plan.updated", {
      turnId,
      plan: [{ step: redactPublicText(item.text ?? "", config), status: "completed" }]
    }, time));
    return;
  }

  if (item.type === "commandExecution") {
    const activity = projectPublicActivityItem(item, config);
    events.push(historyEvent(item.id, "activity.completed", {
      turnId,
      itemId: item.id,
      ...activity
    }, time));
    return;
  }

  if (item.type === "fileChange") {
    const files = safeChangeFiles(item.changes, config.changeCwd, config.changeWorkspacePath);
    events.push(historyEvent(item.id, "activity.completed", {
      turnId,
      itemId: item.id,
      kind: item.type,
      summary: `Updated ${(item.changes ?? []).length} file(s)`,
      status: item.status ?? "completed",
      paths: files.map((file) => file.path),
      diffs: files.filter((file) => typeof file.diff === "string" || file.unavailableReason)
    }, time));
    return;
  }

  if (["mcpToolCall", "dynamicToolCall", "webSearch", "imageGeneration", "imageView", "collabAgentToolCall", "subAgentActivity", "sleep", "enteredReviewMode", "exitedReviewMode", "contextCompaction"].includes(item.type)) {
    const activity = projectPublicActivityItem(item, config);
    events.push(historyEvent(item.id, "activity.completed", {
      turnId,
      itemId: item.id,
      ...activity,
      images: projectGeneratedItem(item, config)
    }, time));
  }
}

function historyEvent(id, event, data, time) {
  return {
    id: `history-${id}`,
    event,
    data: compact(data),
    createdAt: new Date(time ?? 0).toISOString()
  };
}

function accountFor(config, accountId) {
  const account = config.accounts.find((item) => item.id === accountId);
  if (!account) {
    const error = new Error("account_not_found");
    error.status = 404;
    throw error;
  }
  return account;
}

function pathContains(root, candidate) {
  if (!root || !candidate) return false;
  const child = relative(resolve(root), resolve(candidate));
  return child === "" || (!child.startsWith("..") && !isAbsolute(child));
}

function cwdProjectLabel(cwd) {
  if (!cwd) return undefined;
  const label = basename(resolve(cwd)).trim();
  return label || undefined;
}

function threadProjectRoot(thread, desktopProjects, workspaceRoot, projectsRoot) {
  if (!thread?.cwd) return undefined;
  if (thread.id && desktopProjects.projectlessThreadIds?.has?.(thread.id)) return undefined;
  const projectRoots = (desktopProjects.projectRoots ?? []).filter((root) => isAbsolute(root));
  const roots = uniqueStrings([workspaceRoot, ...projectRoots, projectRootChild(projectsRoot, thread.cwd)]);
  return roots.find((root) => pathContains(root, thread.cwd));
}

function projectGeneratedItem(item, config) {
  return typeof config.projectGeneratedImageItem === "function"
    ? config.projectGeneratedImageItem(item)
    : undefined;
}

function projectAssistantText(text, config) {
  return typeof config.projectAssistantText === "function" ? config.projectAssistantText(text) : text;
}

function projectRootChild(root, cwd) {
  if (!root || !cwd || !isAbsolute(root) || !isAbsolute(cwd)) return undefined;
  const child = relative(resolve(root), resolve(cwd));
  if (!child || child.startsWith("..") || isAbsolute(child)) return undefined;
  const [projectName] = child.split(/[\\/]+/);
  return projectName ? resolve(root, projectName) : undefined;
}

async function readDesktopProjects(codexHome) {
  if (!codexHome) return { projectRoots: [], projectlessThreadIds: new Set() };
  try {
    const raw = await readFile(resolve(codexHome, ".codex-global-state.json"), "utf8");
    const state = JSON.parse(raw);
    return {
      projectRoots: uniqueStrings([
        ...(Array.isArray(state["project-order"]) ? state["project-order"] : []),
        ...(Array.isArray(state["electron-saved-workspace-roots"]) ? state["electron-saved-workspace-roots"] : []),
        ...(Array.isArray(state["active-workspace-roots"]) ? state["active-workspace-roots"] : [])
      ]).filter((root) => isAbsolute(root)),
      projectlessThreadIds: new Set(Array.isArray(state["projectless-thread-ids"])
        ? state["projectless-thread-ids"]
        : [])
    };
  } catch {
    return { projectRoots: [], projectlessThreadIds: new Set() };
  }
}

function uniqueStrings(values) {
  return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
}

function timestamp(seconds) {
  return Number.isFinite(seconds) ? new Date(seconds * 1000).toISOString() : undefined;
}

function milliseconds(seconds) {
  return Number.isFinite(seconds) ? seconds * 1000 : undefined;
}

function redactPublicText(value, config) {
  return sanitizePublicText(value, config);
}

function compact(value) {
  return Object.fromEntries(
    Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== "")
  );
}

function isCompletedTurn(turn) {
  const status = turn.status?.type ?? turn.status;
  return !["active", "inProgress", "running"].includes(status);
}
