import { join } from 'node:path';
import { mkdirSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';

const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');

export type PersistedState = {
  proxySecret: string;
  claudeSessions: Record<string, string>; // sessionKey → Claude Code session_id
};

const DEFAULT_STATE: PersistedState = {
  proxySecret: '',
  claudeSessions: {},
};

let stateDir: string;
let stateFile: string;
let lockFile: string;
let currentState: PersistedState = { ...DEFAULT_STATE };
let saveTimer: Timer | null = null;

/** Call once at startup to configure paths. For proxy: no email. For per-user: pass email. */
export function initPaths(email?: string): void {
  if (email) {
    stateDir = join(DATA_PATH, email, 'sidecar');
    stateFile = join(stateDir, 'claude-state.json');
    lockFile = join(stateDir, 'claude.lock');
  } else {
    stateDir = join(DATA_PATH, 'sidecar');
    stateFile = join(stateDir, 'claude-state.json');
    lockFile = join(stateDir, 'claude.lock');
  }
}

// Default to proxy paths
initPaths();

function ensureDir() {
  if (!existsSync(stateDir)) {
    mkdirSync(stateDir, { recursive: true });
  }
}

export function loadState(): PersistedState {
  ensureDir();
  try {
    if (!existsSync(stateFile)) {
      currentState = { ...DEFAULT_STATE };
      return currentState;
    }
    const text = readFileSync(stateFile, 'utf-8');
    currentState = { ...DEFAULT_STATE, ...JSON.parse(text) };
    return currentState;
  } catch {
    currentState = { ...DEFAULT_STATE };
    return currentState;
  }
}

export async function saveState(): Promise<void> {
  ensureDir();
  await Bun.write(stateFile, JSON.stringify(currentState, null, 2));
}

export function getState(): PersistedState {
  return currentState;
}

export function updateState(patch: Partial<PersistedState>): void {
  Object.assign(currentState, patch);
  scheduleSave();
}

/**
 * Both of these write through instead of debouncing.
 *
 * This map is the only thing that lets a conversation be reattached with `--resume` after the agent
 * sidecar dies, so it is precisely the state that must not be in a 30s window when the process is
 * killed rather than asked to stop — `flushAndSave` on SIGTERM covers a `pm2 restart`, but not a
 * crash or a SIGKILL, which is the case the resume path exists for. Losing it silently starts the
 * next turn as a fresh Claude session and orphans the transcript on disk.
 *
 * There is nothing to debounce: `onSessionId` fires on every message but with the same id, so the
 * equality guard collapses it to one write per session, and clearing happens once.
 */
export function setClaudeSession(sessionKey: string, sessionId: string): void {
  if (currentState.claudeSessions[sessionKey] === sessionId) return;
  currentState.claudeSessions[sessionKey] = sessionId;
  writeThrough();
}

export function clearClaudeSession(sessionKey: string): void {
  if (!(sessionKey in currentState.claudeSessions)) return;
  delete currentState.claudeSessions[sessionKey];
  writeThrough();
}

function writeThrough(): void {
  void flushAndSave().catch((err) => {
    console.error('[claude-state] save failed:', err instanceof Error ? err.message : err);
  });
}

export function getClaudeSession(sessionKey: string): string | undefined {
  return currentState.claudeSessions[sessionKey];
}

/**
 * The same map read backwards: Claude's transcript uuid → the key officer made up for the session.
 *
 * A browser that has refreshed holds only the uuid, because that is what is in the URL; officer's own
 * key lived in page state and is gone. This is the only record anywhere that relates the two, which is
 * why re-binding a socket to a running turn has to come through the sidecar rather than being answerable
 * on the platform side.
 *
 * A linear scan over a handful of live sessions. If that ever stops being true, add the inverse map —
 * but a second copy of a mapping is a second thing to keep honest, and this one is written on every turn.
 * Newest wins: a transcript resumed under a fresh key leaves the old entry in place, and the caller wants
 * the session generating now, not the one that produced the same file yesterday.
 */
export function findSessionKeyByClaudeSession(claudeSessionId: string): string | undefined {
  const keys = Object.keys(currentState.claudeSessions);
  for (let i = keys.length - 1; i >= 0; i--) {
    const key = keys[i]!;
    if (currentState.claudeSessions[key] === claudeSessionId) return key;
  }
  return undefined;
}

function scheduleSave() {
  if (saveTimer) return;
  saveTimer = setTimeout(async () => {
    saveTimer = null;
    await saveState();
  }, 30_000);
}

export async function flushAndSave(): Promise<void> {
  if (saveTimer) {
    clearTimeout(saveTimer);
    saveTimer = null;
  }
  await saveState();
}

/**
 * Read the Anthropic proxy secret out of the *proxy* sidecar's state file.
 *
 * The proxy (`officer-anthropic-proxy`) and the agent (`officer-agent`) keep separate state — see
 * `initPaths`: `DATA_PATH/sidecar/` versus `DATA_PATH/<email>/sidecar/` — so the agent cannot reach
 * the secret through `getState()`. It used to be handed the secret in env by the main server, and
 * needing that handoff is precisely why the agent had to be spawned by `officer` (and therefore died
 * with it). Reading it off disk keeps the two processes independent, with the proxy still the only
 * writer.
 *
 * Returns '' when the secret is not on disk yet: `ensureProxySecret` persists through a 30s debounce,
 * so a brand-new install has a window where the file exists without it. Callers should treat '' as
 * "retry later" rather than fatal.
 */
export function readProxySecretFromDisk(): string {
  try {
    const proxyStateFile = join(DATA_PATH, 'sidecar', 'claude-state.json');
    if (!existsSync(proxyStateFile)) return '';
    const parsed = JSON.parse(readFileSync(proxyStateFile, 'utf-8')) as Partial<PersistedState>;
    return parsed.proxySecret ?? '';
  } catch {
    return '';
  }
}

// ── Lockfile ──

export function acquireLock(): boolean {
  ensureDir();
  try {
    if (existsSync(lockFile)) {
      const pidStr = readFileSync(lockFile, 'utf-8').trim();
      const pid = Number(pidStr);
      if (pid && isProcessAlive(pid)) {
        return false;
      }
    }
    writeFileSync(lockFile, String(process.pid));
    return true;
  } catch {
    return false;
  }
}

export function releaseLock(): void {
  try {
    if (existsSync(lockFile)) {
      unlinkSync(lockFile);
    }
  } catch {
    // best effort
  }
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}
