import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync } from "node:fs";
import { basename, dirname, join, relative } from "node:path";

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

type SessionMetric = {
  type: string;
  timestamp: string;
  cwd: string;
  sessionFile?: string;
  data: Record<string, unknown>;
};

type MessageLike = {
  role?: string;
  content?: unknown;
};

type EntryLike = {
  type?: string;
  message?: MessageLike;
};

const MEMORY_ROOT = "docs/agent-memory";
const RAW_ROOT = ".agent-memory/raw";
const REPORT_ROOT = ".agent-memory/reports";
const STATE_ROOT = ".agent-memory/state";
const REVIEW_ROOT = ".agent-memory/review";
const PENDING_REVIEW_ROOT = `${REVIEW_ROOT}/pending`;
const APPROVED_REVIEW_ROOT = `${REVIEW_ROOT}/approved`;
const DISCARDED_REVIEW_ROOT = `${REVIEW_ROOT}/discarded`;
const MAX_INJECT_CHARS = 9000;
const MAX_RAW_TEXT_CHARS = 1600;
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;

const isoDate = (date = new Date()) => date.toISOString().slice(0, 10);
const isoTime = (date = new Date()) => date.toISOString();
const compactStamp = (date = new Date()) => date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");

const ensureDir = (path: string) => mkdirSync(path, { recursive: true });

const appendJsonl = (path: string, row: unknown) => {
  ensureDir(dirname(path));
  appendFileSync(path, `${JSON.stringify(row)}\n`, "utf8");
};

const formatMs = (ms: number): string => {
  const safeMs = Math.max(0, Math.round(ms));
  const seconds = Math.floor(safeMs / 1000);
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  if (h > 0) return `${h}h ${m}m ${s}s`;
  if (m > 0) return `${m}m ${s}s`;
  return `${s}s`;
};

const readIfExists = (path: string, maxChars = MAX_INJECT_CHARS): string => {
  if (!existsSync(path) || !statSync(path).isFile()) return "";
  const text = readFileSync(path, "utf8").trim();
  if (text.length <= maxChars) return text;
  return `${text.slice(0, maxChars)}\n\n[truncated by agent-memory extension]`;
};

const truncate = (text: string, maxChars = MAX_RAW_TEXT_CHARS): string => {
  const clean = text.replace(/\s+/g, " ").trim();
  if (clean.length <= maxChars) return clean;
  return `${clean.slice(0, maxChars)}... [truncated]`;
};

const extractTextParts = (content: unknown): string[] => {
  if (typeof content === "string") return [content];
  if (!Array.isArray(content)) return [];

  const parts: string[] = [];
  for (const part of content) {
    if (!part || typeof part !== "object") continue;
    const block = part as { type?: string; text?: string };
    if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
  }
  return parts;
};

const branchConversation = (entries: EntryLike[], maxMessages = 24) => {
  const messages: Array<{ role: string; text: string }> = [];

  for (const entry of entries) {
    if (entry.type !== "message" || !entry.message?.role) continue;
    const role = entry.message.role;
    if (role !== "user" && role !== "assistant") continue;

    const text = extractTextParts(entry.message.content).join("\n").trim();
    if (!text) continue;
    messages.push({ role, text: truncate(text) });
  }

  return messages.slice(-maxMessages);
};

const latestAssistantText = (entries: EntryLike[]) => {
  for (const entry of [...entries].reverse()) {
    if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
    const text = extractTextParts(entry.message.content).join("\n").trim();
    if (text) return text;
  }
  return "";
};

const looksBlockedOnUser = (text: string) => {
  const clean = text.replace(/\s+/g, " ").trim();
  if (!clean) return false;

  const directQuestion = /(^|[\s])[^.!?]{8,240}\?\s*($|[\])"'`])/m.test(clean);
  const requestForDecision = /\b(please confirm|please provide|which option|what would you prefer|do you want|would you like|should i|can you confirm|could you confirm|waiting for your|i need your)\b/i.test(clean);
  return directQuestion || requestForDecision;
};

const slugify = (value: string, fallback = "memory") => {
  const slug = value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 48);
  return slug || fallback;
};

const writeMetric = (cwd: string, metric: SessionMetric) => {
  const day = isoDate();
  appendJsonl(join(cwd, RAW_ROOT, `${day}.jsonl`), metric);
};

const memoryInjection = (cwd: string): string => {
  const indexPath = join(cwd, MEMORY_ROOT, "index.md");
  const changeLogPath = join(cwd, MEMORY_ROOT, "prompt-change-log.md");
  const index = readIfExists(indexPath, 6000);
  const changeLog = readIfExists(changeLogPath, 3000);

  if (!index && !changeLog) return "";

  return [
    "## Project Agent Memory",
    "",
    "Use this as a compact index of reviewed project memory. Do not treat it as exhaustive; read cited files when relevant.",
    "Never store secrets or raw private transcript content in reviewed memory.",
    "",
    index ? `### Memory Index\n\n${index}` : "",
    changeLog ? `### Prompt Change Log\n\n${changeLog}` : "",
    "",
    "When the user gives a durable correction, says a prompt pattern worked, or reports an error/fix, suggest `/pi-remember` or `/pi-evolve` instead of relying on chat history.",
  ]
    .filter(Boolean)
    .join("\n");
};

export default function agentMemoryExtension(pi: ExtensionAPI) {
  let sessionStartedAt = Date.now();
  let currentPrompt = "";
  let promptStartedAt = 0;
  let lastAgentEndedAt = 0;
  let turnStartedAt = 0;
  let providerRequestStartedAt = 0;
  let providerRequests = 0;
  let providerResponses = 0;
  let toolsStarted = 0;
  let toolsErrored = 0;
  let totalPauseInclusiveGapMs = 0;
  let totalIdleExcludedMs = 0;
  let totalAgentDurationMs = 0;
  let totalTurnDurationMs = 0;
  let totalProviderHeaderLatencyMs = 0;
  let activeWorkStartedAt = 0;
  let activeWorkPausedAt = 0;
  let activeWorkAccumulatedMs = 0;
  let activeWorkLabel = "";
  let blockedOnUserStartedAt = 0;
  let blockedOnUserPrompt = "";
  let totalBlockedOnUserMs = 0;
  let activeAnswerStartedAt = 0;
  let activeAnswerAccumulatedMs = 0;
  let activeAnswerLabel = "";
  let answerAutoStartedActiveWork = false;
  const submittedPrompts: Array<{ at: string; text: string; pauseInclusiveGapMs?: number }> = [];

  const baseMetric = (ctx: { cwd: string; sessionManager?: { getSessionFile?: () => string | undefined } }, type: string, data: Record<string, unknown>): SessionMetric => ({
    type,
    timestamp: isoTime(),
    cwd: ctx.cwd,
    sessionFile: ctx.sessionManager?.getSessionFile?.(),
    data,
  });

  const captureSnapshot = (ctx: { cwd: string; sessionManager: { getBranch: () => EntryLike[]; getSessionFile?: () => string | undefined } }, note = "") => {
    ensureDir(join(ctx.cwd, RAW_ROOT));
    const row = baseMetric(ctx, "memory_snapshot", {
      note,
      sessionStartedAt: new Date(sessionStartedAt).toISOString(),
      capturedAt: isoTime(),
      prompts: submittedPrompts.slice(-20),
      conversation: branchConversation(ctx.sessionManager.getBranch()),
    });
    writeMetric(ctx.cwd, row);
    return row;
  };

  const latestConversation = (ctx: { sessionManager: { getBranch: () => EntryLike[] } }) => branchConversation(ctx.sessionManager.getBranch(), 8);

  const writeReviewCandidate = (
    ctx: { cwd: string; sessionManager: { getBranch: () => EntryLike[]; getSessionFile?: () => string | undefined } },
    source: "automatic" | "manual",
    note = "",
  ) => {
    const conversation = latestConversation(ctx);
    const latestUser = [...conversation].reverse().find((message) => message.role === "user")?.text || note || "memory candidate";
    const filename = `${compactStamp()}-${source}-${slugify(latestUser)}.md`;
    const candidatePath = join(ctx.cwd, PENDING_REVIEW_ROOT, filename);
    const rawPath = join(ctx.cwd, RAW_ROOT, `${isoDate()}.jsonl`);
    const metrics = activeWorkSummary();
    const lines = [
      "---",
      `status: pending`,
      `source: ${source}`,
      `created: ${isoTime()}`,
      `raw_log: ${relative(ctx.cwd, rawPath)}`,
      `session: ${ctx.sessionManager.getSessionFile?.() || ""}`,
      "---",
      "",
      `# Memory Review Candidate: ${source}`,
      "",
      "## Review Decision",
      "",
      "- [ ] Approve for memory compilation",
      "- [ ] Discard",
      "- [ ] Needs manual editing before compile",
      "",
      "## Why This Was Captured",
      "",
      note || "Automatic capture after agent completion.",
      "",
      "## Suggested Durable Lessons",
      "",
      "- ",
      "",
      "## Errors And Fixes",
      "",
      "- Symptom:",
      "- Cause:",
      "- Fix:",
      "- Evidence:",
      "",
      "## Prompt/Agent Evolution Candidates",
      "",
      "- Target:",
      "- Proposed change:",
      "- Evidence:",
      "- Risk:",
      "",
      "## Recent Conversation Excerpt",
      "",
      ...conversation.map((message) => [`### ${message.role}`, "", message.text, ""].join("\n")),
      "## Metrics Snapshot",
      "",
      `- Active user work time: ${formatMs(metrics.activeWorkMs as number)}`,
      `- Active answer time: ${formatMs(metrics.activeAnswerMs as number)}`,
      `- Blocked waiting for user: ${formatMs(metrics.blockedOnUserMs as number)}`,
      `- Pause-inclusive prompt gaps: ${formatMs(metrics.totalPauseInclusiveGapMs as number)}`,
      `- Idle-excluded gap time: ${formatMs(metrics.totalIdleExcludedMs as number)}`,
      `- Agent duration: ${formatMs(metrics.totalAgentDurationMs as number)}`,
      `- Turn duration: ${formatMs(metrics.totalTurnDurationMs as number)}`,
      `- Provider response header latency: ${formatMs(metrics.totalProviderHeaderLatencyMs as number)}`,
      `- Tools started: ${metrics.toolsStarted}`,
      `- Tool errors: ${metrics.toolsErrored}`,
      "",
      "## Next Commands",
      "",
      "```text",
      `/memory-approve ${filename}`,
      `/memory-discard ${filename}`,
      `/memory-compile Review approved candidate ${filename}`,
      "```",
      "",
    ];

    ensureDir(dirname(candidatePath));
    writeFileSync(candidatePath, `${lines.join("\n")}\n`, "utf8");
    writeMetric(ctx.cwd, baseMetric(ctx, "memory_review_candidate", {
      source,
      candidatePath: relative(ctx.cwd, candidatePath),
      note,
    }));
    return candidatePath;
  };

  const pendingCandidates = (cwd: string) => {
    const dir = join(cwd, PENDING_REVIEW_ROOT);
    if (!existsSync(dir)) return [];
    return readdirSync(dir)
      .filter((entry) => entry.endsWith(".md"))
      .sort()
      .map((entry) => join(dir, entry));
  };

  const resolvePendingCandidate = (cwd: string, value: string) => {
    const query = value.trim();
    const candidates = pendingCandidates(cwd);
    if (!query) return candidates[candidates.length - 1];
    return candidates.find((candidate) => basename(candidate) === query || basename(candidate).includes(query));
  };

  const activeWorkMs = () => {
    if (!activeWorkStartedAt) return activeWorkAccumulatedMs;
    if (activeWorkPausedAt) return activeWorkAccumulatedMs;
    return activeWorkAccumulatedMs + Date.now() - activeWorkStartedAt;
  };

  const blockedOnUserMs = () => totalBlockedOnUserMs + (blockedOnUserStartedAt ? Date.now() - blockedOnUserStartedAt : 0);

  const activeAnswerMs = () => {
    if (!activeAnswerStartedAt) return activeAnswerAccumulatedMs;
    return activeAnswerAccumulatedMs + Date.now() - activeAnswerStartedAt;
  };

  const activeWorkSummary = () => ({
    activeWorkMs: activeWorkMs(),
    activeWorkLabel,
    activeWorkRunning: activeWorkStartedAt > 0 && activeWorkPausedAt === 0,
    activeWorkPaused: activeWorkPausedAt > 0,
    activeAnswerMs: activeAnswerMs(),
    activeAnswerLabel,
    activeAnswerRunning: activeAnswerStartedAt > 0,
    blockedOnUserMs: blockedOnUserMs(),
    blockedOnUserActive: blockedOnUserStartedAt > 0,
    blockedOnUserPrompt,
    promptsSeen: submittedPrompts.length,
    totalPauseInclusiveGapMs,
    totalIdleExcludedMs,
    totalAgentDurationMs,
    totalTurnDurationMs,
    totalProviderHeaderLatencyMs,
    providerRequests,
    providerResponses,
    toolsStarted,
    toolsErrored,
  });

  const writeTimeReport = (cwd: string, sessionFile?: string) => {
    const summary = activeWorkSummary();
    const reportPath = join(cwd, REPORT_ROOT, `active-time-${isoDate()}.md`);
    const lines = [
      `# Active Time Report: ${isoDate()}`,
      "",
      `Generated: ${isoTime()}`,
      sessionFile ? `Session: ${sessionFile}` : "",
      "",
      "## Summary",
      "",
      `- Active user work time: ${formatMs(summary.activeWorkMs as number)}`,
      `- Active work label: ${summary.activeWorkLabel || "n/a"}`,
      `- Active answer time: ${formatMs(summary.activeAnswerMs as number)}`,
      `- Active answer label: ${summary.activeAnswerLabel || "n/a"}`,
      `- Blocked waiting for user: ${formatMs(summary.blockedOnUserMs as number)}`,
      `- Currently waiting for user: ${summary.blockedOnUserActive ? "yes" : "no"}`,
      `- Prompts submitted: ${summary.promptsSeen}`,
      `- Pause-inclusive prompt gaps: ${formatMs(summary.totalPauseInclusiveGapMs as number)}`,
      `- Idle-excluded gap time: ${formatMs(summary.totalIdleExcludedMs as number)}`,
      `- Agent duration: ${formatMs(summary.totalAgentDurationMs as number)}`,
      `- Turn duration: ${formatMs(summary.totalTurnDurationMs as number)}`,
      `- Provider response header latency: ${formatMs(summary.totalProviderHeaderLatencyMs as number)}`,
      `- Provider requests: ${summary.providerRequests}`,
      `- Provider responses: ${summary.providerResponses}`,
      `- Tools started: ${summary.toolsStarted}`,
      `- Tool errors: ${summary.toolsErrored}`,
      "",
      "## Notes",
      "",
      "- Active user work time is measured by explicit `/prompt-start`, `/prompt-pause`, `/prompt-resume`, and `/prompt-stop` commands.",
      "- Active answer time is measured by `/answer-start` and `/answer-stop` when you are composing an answer to an agent question.",
      "- Blocked waiting for user starts automatically when the last assistant message looks like a direct question and stops on the next interactive user input.",
      "- Pi extension APIs do not expose per-keystroke editor activity here, so this is explicit block timing plus automatic idle-capped gap metrics.",
      "- Use LiteLLM and `npx @ccusage/pi@latest session` for provider-side tokens/cost/inference reports.",
      "",
    ].filter(Boolean);

    ensureDir(dirname(reportPath));
    writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf8");
    return reportPath;
  };

  const showOrPrint = (ctx: { hasUI: boolean; ui: { notify: (message: string, level?: string) => void } }, message: string, level: string = "info") => {
    if (ctx.hasUI) ctx.ui.notify(message, level);
    else console.log(message);
  };

  pi.on("session_start", async (_event, ctx) => {
    sessionStartedAt = Date.now();
    ensureDir(join(ctx.cwd, RAW_ROOT));
    ensureDir(join(ctx.cwd, REPORT_ROOT));
    ensureDir(join(ctx.cwd, STATE_ROOT));
    ensureDir(join(ctx.cwd, PENDING_REVIEW_ROOT));
    ensureDir(join(ctx.cwd, APPROVED_REVIEW_ROOT));
    ensureDir(join(ctx.cwd, DISCARDED_REVIEW_ROOT));
    writeMetric(ctx.cwd, baseMetric(ctx, "session_start", { pid: process.pid }));
  });

  pi.on("input", async (event, ctx) => {
    if (event.source === "extension") return { action: "continue" };

    const now = Date.now();
    const pauseInclusiveGapMs = lastAgentEndedAt > 0 ? now - lastAgentEndedAt : undefined;
    let answeredBlockedPrompt: string | undefined;
    let blockedOnUserDurationMs: number | undefined;
    if (blockedOnUserStartedAt) {
      blockedOnUserDurationMs = now - blockedOnUserStartedAt;
      totalBlockedOnUserMs += blockedOnUserDurationMs;
      answeredBlockedPrompt = blockedOnUserPrompt;
      writeMetric(ctx.cwd, baseMetric(ctx, "blocked_on_user_end", {
        blockedOnUserDurationMs,
        blockedOnUserPrompt: blockedOnUserPrompt ? truncate(blockedOnUserPrompt, 700) : "",
        answerChars: event.text.length,
      }));
      blockedOnUserStartedAt = 0;
      blockedOnUserPrompt = "";
    }

    promptStartedAt = now;
    currentPrompt = event.text;
    submittedPrompts.push({
      at: isoTime(new Date(now)),
      text: truncate(event.text, 500),
      pauseInclusiveGapMs,
    });

    const idleExcludedMs = pauseInclusiveGapMs && pauseInclusiveGapMs > IDLE_TIMEOUT_MS ? pauseInclusiveGapMs - IDLE_TIMEOUT_MS : 0;
    totalPauseInclusiveGapMs += pauseInclusiveGapMs ?? 0;
    totalIdleExcludedMs += idleExcludedMs;

    writeMetric(ctx.cwd, baseMetric(ctx, "prompt_submitted", {
      promptChars: event.text.length,
      pauseInclusiveGapMs,
      idleExcludedMs,
      blockedOnUserDurationMs,
      answeredBlockedPrompt: answeredBlockedPrompt ? truncate(answeredBlockedPrompt, 500) : undefined,
    }));

    return { action: "continue" };
  });

  pi.on("before_agent_start", async (event, ctx) => {
    const injection = memoryInjection(ctx.cwd);
    if (!injection) return;

    writeMetric(ctx.cwd, baseMetric(ctx, "memory_injected", {
      promptChars: event.prompt.length,
      injectionChars: injection.length,
      indexPath: relative(ctx.cwd, join(ctx.cwd, MEMORY_ROOT, "index.md")),
    }));

    return {
      systemPrompt: `${event.systemPrompt}\n\n${injection}`,
    };
  });

  pi.on("agent_start", async (_event, ctx) => {
    toolsStarted = 0;
    toolsErrored = 0;
    providerRequests = 0;
    providerResponses = 0;
    writeMetric(ctx.cwd, baseMetric(ctx, "agent_start", {
      promptChars: currentPrompt.length,
      promptSubmitToStartMs: promptStartedAt > 0 ? Date.now() - promptStartedAt : undefined,
    }));
  });

  pi.on("turn_start", async (_event, ctx) => {
    turnStartedAt = Date.now();
    writeMetric(ctx.cwd, baseMetric(ctx, "turn_start", {}));
  });

  pi.on("before_provider_request", (event, ctx) => {
    providerRequests += 1;
    providerRequestStartedAt = Date.now();
    writeMetric(ctx.cwd, baseMetric(ctx, "provider_request_start", {
      providerRequestIndex: providerRequests,
      payloadKeys: event.payload && typeof event.payload === "object" ? Object.keys(event.payload as Record<string, unknown>).sort() : [],
    }));
  });

  pi.on("after_provider_response", (event, ctx) => {
    providerResponses += 1;
    const headerLatencyMs = providerRequestStartedAt > 0 ? Date.now() - providerRequestStartedAt : undefined;
    totalProviderHeaderLatencyMs += headerLatencyMs ?? 0;
    writeMetric(ctx.cwd, baseMetric(ctx, "provider_response_headers", {
      providerResponseIndex: providerResponses,
      status: event.status,
      headerLatencyMs,
    }));
  });

  pi.on("tool_execution_start", async (event, ctx) => {
    toolsStarted += 1;
    writeMetric(ctx.cwd, baseMetric(ctx, "tool_start", {
      toolName: event.toolName,
    }));
  });

  pi.on("tool_execution_end", async (event, ctx) => {
    if (event.isError) toolsErrored += 1;
    writeMetric(ctx.cwd, baseMetric(ctx, "tool_end", {
      toolName: event.toolName,
      isError: event.isError,
    }));
  });

  pi.on("turn_end", async (_event, ctx) => {
    const turnDurationMs = turnStartedAt > 0 ? Date.now() - turnStartedAt : undefined;
    totalTurnDurationMs += turnDurationMs ?? 0;
    writeMetric(ctx.cwd, baseMetric(ctx, "turn_end", {
      turnDurationMs,
    }));
  });

  pi.on("agent_end", async (_event, ctx) => {
    const now = Date.now();
    lastAgentEndedAt = now;
    const agentDurationMs = promptStartedAt > 0 ? now - promptStartedAt : undefined;
    totalAgentDurationMs += agentDurationMs ?? 0;
    writeMetric(ctx.cwd, baseMetric(ctx, "agent_end", {
      agentDurationMs,
      providerRequests,
      providerResponses,
      toolsStarted,
      toolsErrored,
    }));

    captureSnapshot(ctx, "automatic agent_end snapshot");
    const candidatePath = writeReviewCandidate(ctx, "automatic", "Automatic capture after agent completion. Review before compiling into durable memory.");
    if (ctx.hasUI) ctx.ui.notify(`Memory candidate ready for review: ${relative(ctx.cwd, candidatePath)}`, "info");

    const finalAssistantText = latestAssistantText(ctx.sessionManager.getBranch());
    if (looksBlockedOnUser(finalAssistantText)) {
      blockedOnUserStartedAt = now;
      blockedOnUserPrompt = truncate(finalAssistantText, 900);
      writeMetric(ctx.cwd, baseMetric(ctx, "blocked_on_user_start", {
        blockedOnUserPrompt,
      }));
      if (ctx.hasUI) ctx.ui.notify("Agent appears to be waiting for your answer. Waiting time will stop on your next prompt.", "info");
    }
  });

  pi.on("session_before_compact", async (event, ctx) => {
    writeMetric(ctx.cwd, baseMetric(ctx, "session_before_compact", {
      tokensBefore: event.preparation?.tokensBefore,
      firstKeptEntryId: event.preparation?.firstKeptEntryId,
    }));
  });

  pi.on("session_shutdown", async (event, ctx) => {
    writeMetric(ctx.cwd, baseMetric(ctx, "session_shutdown", {
      reason: event.reason,
      sessionDurationMs: Date.now() - sessionStartedAt,
      promptsSeen: submittedPrompts.length,
      ...activeWorkSummary(),
    }));
  });

  pi.registerCommand("memory-status", {
    description: "Show agent memory automation status",
    handler: async (_args, ctx) => {
      const day = isoDate();
      const rawPath = join(ctx.cwd, RAW_ROOT, `${day}.jsonl`);
      const indexPath = join(ctx.cwd, MEMORY_ROOT, "index.md");
      const message = [
        `Memory index: ${existsSync(indexPath) ? relative(ctx.cwd, indexPath) : "missing"}`,
        `Raw metrics today: ${existsSync(rawPath) ? relative(ctx.cwd, rawPath) : "none yet"}`,
        `Pending memory candidates: ${pendingCandidates(ctx.cwd).length}`,
        `Prompts observed this session: ${submittedPrompts.length}`,
        `Provider requests this turn: ${providerRequests}`,
        `Active user work time: ${formatMs(activeWorkMs())}`,
        `Active answer time: ${formatMs(activeAnswerMs())}`,
        `Blocked waiting for user: ${formatMs(blockedOnUserMs())}${blockedOnUserStartedAt ? " (running)" : ""}`,
      ].join("\n");

      showOrPrint(ctx, message, "info");
    },
  });

  pi.registerCommand("memory-capture", {
    description: "Capture a private raw snapshot for later memory compilation",
    handler: async (args, ctx) => {
      const row = captureSnapshot(ctx, args.trim() || "manual capture");
      const candidatePath = writeReviewCandidate(ctx, "manual", args.trim() || "manual capture");
      const rawPath = join(ctx.cwd, RAW_ROOT, `${isoDate()}.jsonl`);
      pi.appendEntry("agent-memory-capture", row);
      const message = [
        `Captured private memory snapshot: ${relative(ctx.cwd, rawPath)}`,
        `Review candidate: ${relative(ctx.cwd, candidatePath)}`,
      ].join("\n");
      showOrPrint(ctx, message, "success");
    },
  });

  pi.registerCommand("memory-compile", {
    description: "Capture current session and ask Pi to compile durable memory/evolution changes",
    handler: async (args, ctx) => {
      if (!ctx.isIdle()) {
        if (ctx.hasUI) ctx.ui.notify("Agent is busy. Run /memory-compile after the current turn finishes.", "warning");
        return;
      }

      captureSnapshot(ctx, args.trim() || "compile request");
      const goal = args.trim() || "Compile durable lessons from the latest private memory snapshot and propose prompt evolution only if evidence is strong.";
      pi.sendUserMessage(`/pi-evolve ${goal}`);
    },
  });

  pi.registerCommand("memory-review", {
    description: "List pending memory candidates for review",
    handler: async (_args, ctx) => {
      const candidates = pendingCandidates(ctx.cwd);
      const message = candidates.length
        ? [`Pending memory candidates:`, ...candidates.slice(-20).map((candidate) => `- ${relative(ctx.cwd, candidate)}`)].join("\n")
        : "No pending memory candidates.";
      showOrPrint(ctx, message, "info");
    },
  });

  pi.registerCommand("memory-show", {
    description: "Show a pending memory candidate by filename fragment",
    handler: async (args, ctx) => {
      const candidate = resolvePendingCandidate(ctx.cwd, args);
      if (!candidate) {
        showOrPrint(ctx, "No matching pending memory candidate.", "warning");
        return;
      }
      const text = readIfExists(candidate, 12000);
      showOrPrint(ctx, `${relative(ctx.cwd, candidate)}\n\n${text}`, "info");
    },
  });

  pi.registerCommand("memory-approve", {
    description: "Approve a pending memory candidate and launch memory compilation",
    handler: async (args, ctx) => {
      if (!ctx.isIdle()) {
        showOrPrint(ctx, "Agent is busy. Approve after the current turn finishes.", "warning");
        return;
      }

      const candidate = resolvePendingCandidate(ctx.cwd, args);
      if (!candidate) {
        showOrPrint(ctx, "No matching pending memory candidate.", "warning");
        return;
      }

      const approvedPath = join(ctx.cwd, APPROVED_REVIEW_ROOT, basename(candidate));
      ensureDir(dirname(approvedPath));
      renameSync(candidate, approvedPath);
      writeMetric(ctx.cwd, baseMetric(ctx, "memory_candidate_approved", { candidatePath: relative(ctx.cwd, approvedPath) }));
      showOrPrint(ctx, `Approved memory candidate: ${relative(ctx.cwd, approvedPath)}`, "success");
      pi.sendUserMessage(`/pi-evolve Compile approved memory candidate ${relative(ctx.cwd, approvedPath)}. Update reviewed memory and propose prompt changes only if evidence is strong.`);
    },
  });

  pi.registerCommand("memory-discard", {
    description: "Discard a pending memory candidate by filename fragment",
    handler: async (args, ctx) => {
      const candidate = resolvePendingCandidate(ctx.cwd, args);
      if (!candidate) {
        showOrPrint(ctx, "No matching pending memory candidate.", "warning");
        return;
      }

      const discardedPath = join(ctx.cwd, DISCARDED_REVIEW_ROOT, basename(candidate));
      ensureDir(dirname(discardedPath));
      renameSync(candidate, discardedPath);
      writeMetric(ctx.cwd, baseMetric(ctx, "memory_candidate_discarded", { candidatePath: relative(ctx.cwd, discardedPath) }));
      showOrPrint(ctx, `Discarded memory candidate: ${relative(ctx.cwd, discardedPath)}`, "info");
    },
  });

  pi.registerCommand("memory-clear", {
    description: "Delete all pending memory candidates",
    handler: async (_args, ctx) => {
      const candidates = pendingCandidates(ctx.cwd);
      for (const candidate of candidates) unlinkSync(candidate);
      writeMetric(ctx.cwd, baseMetric(ctx, "memory_candidates_cleared", { count: candidates.length }));
      showOrPrint(ctx, `Deleted ${candidates.length} pending memory candidates.`, "info");
    },
  });

  pi.registerCommand("prompt-start", {
    description: "Start explicit active user prompting/work timer",
    handler: async (args, ctx) => {
      const now = Date.now();
      if (activeWorkStartedAt && !activeWorkPausedAt) {
        showOrPrint(ctx, `Active work timer already running: ${formatMs(activeWorkMs())}`, "warning");
        return;
      }

      activeWorkStartedAt = now;
      activeWorkPausedAt = 0;
      activeWorkLabel = args.trim() || activeWorkLabel || "manual prompt work";
      writeMetric(ctx.cwd, baseMetric(ctx, "active_work_start", { label: activeWorkLabel, activeWorkMs: activeWorkMs() }));
      showOrPrint(ctx, `Active work timer started: ${activeWorkLabel}`, "success");
    },
  });

  pi.registerCommand("prompt-pause", {
    description: "Pause explicit active user prompting/work timer",
    handler: async (_args, ctx) => {
      if (!activeWorkStartedAt || activeWorkPausedAt) {
        showOrPrint(ctx, "Active work timer is not running.", "warning");
        return;
      }

      activeWorkAccumulatedMs += Date.now() - activeWorkStartedAt;
      activeWorkStartedAt = 0;
      activeWorkPausedAt = Date.now();
      writeMetric(ctx.cwd, baseMetric(ctx, "active_work_pause", { activeWorkMs: activeWorkMs(), label: activeWorkLabel }));
      showOrPrint(ctx, `Active work timer paused at ${formatMs(activeWorkMs())}`, "info");
    },
  });

  pi.registerCommand("prompt-resume", {
    description: "Resume explicit active user prompting/work timer",
    handler: async (_args, ctx) => {
      if (activeWorkStartedAt && !activeWorkPausedAt) {
        showOrPrint(ctx, "Active work timer is already running.", "warning");
        return;
      }

      activeWorkStartedAt = Date.now();
      activeWorkPausedAt = 0;
      writeMetric(ctx.cwd, baseMetric(ctx, "active_work_resume", { activeWorkMs: activeWorkMs(), label: activeWorkLabel }));
      showOrPrint(ctx, `Active work timer resumed at ${formatMs(activeWorkMs())}`, "success");
    },
  });

  pi.registerCommand("prompt-stop", {
    description: "Stop explicit active user prompting/work timer",
    handler: async (_args, ctx) => {
      if (activeWorkStartedAt && !activeWorkPausedAt) {
        activeWorkAccumulatedMs += Date.now() - activeWorkStartedAt;
      }

      activeWorkStartedAt = 0;
      activeWorkPausedAt = 0;
      const reportPath = writeTimeReport(ctx.cwd, ctx.sessionManager.getSessionFile?.());
      writeMetric(ctx.cwd, baseMetric(ctx, "active_work_stop", { ...activeWorkSummary(), reportPath: relative(ctx.cwd, reportPath) }));
      showOrPrint(ctx, `Active work timer stopped at ${formatMs(activeWorkMs())}\nReport: ${relative(ctx.cwd, reportPath)}`, "success");
    },
  });

  pi.registerCommand("answer-start", {
    description: "Start explicit active answer timer for responding to an agent question",
    handler: async (args, ctx) => {
      if (activeAnswerStartedAt) {
        showOrPrint(ctx, `Active answer timer already running: ${formatMs(activeAnswerMs())}`, "warning");
        return;
      }

      activeAnswerStartedAt = Date.now();
      activeAnswerLabel = args.trim() || activeAnswerLabel || "answering agent question";
      if (!activeWorkStartedAt && !activeWorkPausedAt) {
        activeWorkStartedAt = activeAnswerStartedAt;
        activeWorkLabel = activeWorkLabel || activeAnswerLabel;
        answerAutoStartedActiveWork = true;
      } else {
        answerAutoStartedActiveWork = false;
      }

      writeMetric(ctx.cwd, baseMetric(ctx, "active_answer_start", {
        label: activeAnswerLabel,
        activeAnswerMs: activeAnswerMs(),
        answerAutoStartedActiveWork,
      }));
      showOrPrint(ctx, `Active answer timer started: ${activeAnswerLabel}`, "success");
    },
  });

  pi.registerCommand("answer-stop", {
    description: "Stop explicit active answer timer",
    handler: async (_args, ctx) => {
      if (!activeAnswerStartedAt) {
        showOrPrint(ctx, "Active answer timer is not running.", "warning");
        return;
      }

      activeAnswerAccumulatedMs += Date.now() - activeAnswerStartedAt;
      activeAnswerStartedAt = 0;
      if (answerAutoStartedActiveWork && activeWorkStartedAt && !activeWorkPausedAt) {
        activeWorkAccumulatedMs += Date.now() - activeWorkStartedAt;
        activeWorkStartedAt = 0;
        activeWorkPausedAt = 0;
      }
      answerAutoStartedActiveWork = false;
      writeMetric(ctx.cwd, baseMetric(ctx, "active_answer_stop", {
        ...activeWorkSummary(),
      }));
      showOrPrint(ctx, `Active answer timer stopped at ${formatMs(activeAnswerMs())}`, "success");
    },
  });

  pi.registerCommand("blocked-status", {
    description: "Show automatic blocked-on-user timing status",
    handler: async (_args, ctx) => {
      const message = [
        `Blocked waiting for user: ${formatMs(blockedOnUserMs())}`,
        `Currently waiting for user: ${blockedOnUserStartedAt ? "yes" : "no"}`,
        blockedOnUserPrompt ? `Detected prompt: ${blockedOnUserPrompt}` : "",
      ].filter(Boolean).join("\n");
      showOrPrint(ctx, message, "info");
    },
  });

  pi.registerCommand("time-report", {
    description: "Write active user work and agent timing report",
    handler: async (_args, ctx) => {
      const reportPath = writeTimeReport(ctx.cwd, ctx.sessionManager.getSessionFile?.());
      writeMetric(ctx.cwd, baseMetric(ctx, "time_report", { ...activeWorkSummary(), reportPath: relative(ctx.cwd, reportPath) }));
      showOrPrint(ctx, `Time report written: ${relative(ctx.cwd, reportPath)}`, "info");
    },
  });
}
