#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

const MARKER_TYPE = "live-transcript-marker";
const JOURNAL_VERSION = 1;

function expandHome(filePath) {
  if (!filePath || typeof filePath !== "string") return null;
  if (filePath === "~") return os.homedir();
  if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2));
  return filePath;
}

function ensureDir(dir) {
  fs.mkdirSync(dir, { recursive: true });
}

function appendJsonl(filePath, records) {
  if (!records.length) return;
  ensureDir(path.dirname(filePath));
  const fd = fs.openSync(filePath, "a");
  try {
    const text = records.map((record) => `${JSON.stringify(record)}\n`).join("");
    fs.writeSync(fd, text);
  } finally {
    fs.closeSync(fd);
  }
}

function firstRecordType(filePath) {
  try {
    const fd = fs.openSync(filePath, "r");
    try {
      const buffer = Buffer.alloc(8192);
      const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
      const firstLine = buffer.subarray(0, bytes).toString("utf8").split("\n")[0];
      if (!firstLine.trim()) return null;
      return JSON.parse(firstLine).type ?? null;
    } finally {
      fs.closeSync(fd);
    }
  } catch {
    return null;
  }
}

function selectTarget(transcriptPath) {
  if (!fs.existsSync(transcriptPath)) {
    return { filePath: transcriptPath, hookOwned: true, seedMarker: true };
  }
  if (firstRecordType(transcriptPath) === MARKER_TYPE) {
    return { filePath: transcriptPath, hookOwned: true, seedMarker: false };
  }
  return { filePath: `${transcriptPath}.live.jsonl`, hookOwned: false, seedMarker: false };
}

function loadState(statePath) {
  try {
    return JSON.parse(fs.readFileSync(statePath, "utf8"));
  } catch {
    return { lastUuid: null };
  }
}

function saveState(statePath, state) {
  ensureDir(path.dirname(statePath));
  const tmp = `${statePath}.${process.pid}.tmp`;
  fs.writeFileSync(tmp, `${JSON.stringify(state)}\n`, "utf8");
  fs.renameSync(tmp, statePath);
}

function baseMessage(input, state, deps, type) {
  const uuid = deps.randomUUID();
  return {
    parentUuid: state.lastUuid ?? null,
    isSidechain: false,
    type,
    uuid,
    timestamp: deps.now(),
    session_id: input.session_id,
    userType: "external",
    entrypoint: "cli",
    cwd: input.cwd ?? process.cwd(),
    sessionId: input.session_id,
    version: `live-transcript-journal/${JOURNAL_VERSION}`
  };
}

function stringifyToolResponse(value) {
  if (typeof value === "string") return value;
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}

function recordsForInput(input, state, deps) {
  switch (input.hook_event_name) {
    case "SessionStart":
      return [{
        type: "mode",
        mode: input.permission_mode ?? "unknown",
        sessionId: input.session_id
      }];

    case "UserPromptSubmit": {
      const record = baseMessage(input, state, deps, "user");
      record.promptId = input.turn_id ?? record.uuid;
      record.message = { role: "user", content: input.prompt ?? "" };
      return [record];
    }

    case "MessageDisplay": {
      if (!input.delta) return [];
      const record = baseMessage(input, state, deps, "assistant");
      record.liveMessageId = input.message_id;
      record.liveTurnId = input.turn_id;
      record.message = {
        id: input.message_id ?? record.uuid,
        type: "message",
        role: "assistant",
        content: [{ type: "text", text: input.delta }],
        stop_reason: input.final ? "end_turn" : null,
        stop_sequence: null
      };
      return [record];
    }

    case "PostToolUse":
    case "PostToolUseFailure": {
      const assistant = baseMessage(input, state, deps, "assistant");
      assistant.message = {
        id: assistant.uuid,
        type: "message",
        role: "assistant",
        content: [{
          type: "tool_use",
          id: input.tool_use_id ?? assistant.uuid,
          name: input.tool_name,
          input: input.tool_input ?? {}
        }],
        stop_reason: "tool_use",
        stop_sequence: null
      };

      const nextState = { ...state, lastUuid: assistant.uuid };
      const user = baseMessage(input, nextState, deps, "user");
      user.message = {
        role: "user",
        content: [{
          type: "tool_result",
          tool_use_id: input.tool_use_id ?? assistant.uuid,
          content: stringifyToolResponse(input.tool_response ?? input.error ?? input)
        }]
      };
      user.toolUseResult = input.tool_response ?? input.error ?? null;
      return [assistant, user];
    }

    case "Stop":
    case "StopFailure": {
      if (!input.last_assistant_message) return [];
      const record = baseMessage(input, state, deps, "assistant");
      record.message = {
        id: record.uuid,
        type: "message",
        role: "assistant",
        content: [{ type: "text", text: input.last_assistant_message }],
        stop_reason: input.hook_event_name === "Stop" ? "end_turn" : "error",
        stop_sequence: null
      };
      return [record];
    }

    default:
      return [];
  }
}

export async function journalHook(input, deps = {}) {
  const transcriptPath = expandHome(input?.transcript_path);
  if (!transcriptPath || path.extname(transcriptPath) !== ".jsonl") return;

  const runtimeDeps = {
    now: deps.now ?? (() => new Date().toISOString()),
    randomUUID: deps.randomUUID ?? (() => crypto.randomUUID())
  };

  const target = selectTarget(transcriptPath);
  const statePath = `${target.filePath}.state.json`;
  const state = loadState(statePath);
  const records = recordsForInput(input, state, runtimeDeps);
  if (!records.length && !target.seedMarker) return;

  const output = [];
  if (target.seedMarker) {
    output.push({
      type: MARKER_TYPE,
      version: JOURNAL_VERSION,
      sessionId: input.session_id,
      createdAt: runtimeDeps.now(),
      note: "Synthetic crash journal created because Claude had not created transcript_path yet."
    });
  }
  output.push(...records);
  appendJsonl(target.filePath, output);

  const lastMessage = [...records].reverse().find((record) => record.uuid);
  if (lastMessage) {
    saveState(statePath, { lastUuid: lastMessage.uuid });
  }
}

async function readStdin() {
  let input = "";
  for await (const chunk of process.stdin) input += chunk;
  return input ? JSON.parse(input) : {};
}

function invokedAsCli() {
  const entry = process.argv[1];
  if (!entry) return false;
  for (const candidate of [entry, (() => { try { return fs.realpathSync(entry); } catch { return null; } })()]) {
    if (candidate && pathToFileURL(candidate).href === import.meta.url) return true;
  }
  return false;
}

if (invokedAsCli()) {
  try {
    await journalHook(await readStdin());
  } catch (error) {
    try {
      fs.appendFileSync(
        path.join(os.tmpdir(), "claude-live-transcript-journal.err.log"),
        `${new Date().toISOString()} ${error instanceof Error ? error.stack : String(error)}\n`
      );
    } catch {
      // Hooks must not fail the Claude turn because crash journaling failed.
    }
  }
}
