/**
 * Agent session store — manages per-project agent state and RPC communication.
 *
 * Listens for agent.message, agent.status, agent.cost events from Bun process.
 * Exposes reactive Svelte 5 rune state per project.
 */

import { appRpc } from './rpc.ts';

// ── Types ────────────────────────────────────────────────────────────────────

export type AgentStatus = 'idle' | 'running' | 'done' | 'error';
export type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result' | 'thinking' | 'system';

export interface AgentMessage {
  id: string;
  role: MsgRole;
  content: string;
  toolName?: string;
  toolInput?: string;
  toolPath?: string;
  timestamp: number;
}

export interface AgentSession {
  sessionId: string;
  projectId: string;
  provider: string;
  status: AgentStatus;
  messages: AgentMessage[];
  costUsd: number;
  inputTokens: number;
  outputTokens: number;
  model: string;
  error?: string;
}

interface StartOptions {
  cwd?: string;
  model?: string;
  systemPrompt?: string;
  maxTurns?: number;
  permissionMode?: string;
  claudeConfigDir?: string;
  extraEnv?: Record<string, string>;
}

// ── Env var validation (Fix #14) ─────────────────────────────────────────────

const BLOCKED_ENV_PREFIXES = ['CLAUDE', 'CODEX', 'OLLAMA', 'ANTHROPIC_'];

function validateExtraEnv(env: Record<string, string> | undefined): Record<string, string> | undefined {
  if (!env) return undefined;
  const clean: Record<string, string> = {};
  for (const [key, value] of Object.entries(env)) {
    const blocked = BLOCKED_ENV_PREFIXES.some(p => key.startsWith(p));
    if (blocked) {
      console.warn(`[agent-store] Rejected extraEnv key "${key}" — provider-prefixed keys are not allowed`);
      continue;
    }
    clean[key] = value;
  }
  return Object.keys(clean).length > 0 ? clean : undefined;
}

// ── Internal state ───────────────────────────────────────────────────────────

// Map projectId -> sessionId for lookup
const projectSessionMap = new Map<string, string>();

// Map sessionId -> reactive session state
let sessions = $state<Record<string, AgentSession>>({});

// Grace period timers for cleanup after done/error
const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();

// Debounce timer for message persistence
const msgPersistTimers = new Map<string, ReturnType<typeof setTimeout>>();

// ── Session persistence helpers ─────────────────────────────────────────────

function persistSession(session: AgentSession): void {
  appRpc.request['session.save']({
    projectId: session.projectId,
    sessionId: session.sessionId,
    provider: session.provider,
    status: session.status,
    costUsd: session.costUsd,
    inputTokens: session.inputTokens,
    outputTokens: session.outputTokens,
    model: session.model,
    error: session.error,
    createdAt: session.messages[0]?.timestamp ?? Date.now(),
    updatedAt: Date.now(),
  }).catch((err: unknown) => {
    console.error('[session.save] persist error:', err);
  });
}

function persistMessages(session: AgentSession): void {
  // Debounce: batch message saves every 2 seconds
  const existing = msgPersistTimers.get(session.sessionId);
  if (existing) clearTimeout(existing);

  const timer = setTimeout(() => {
    msgPersistTimers.delete(session.sessionId);
    const msgs = session.messages.map((m) => ({
      sessionId: session.sessionId,
      msgId: m.id,
      role: m.role,
      content: m.content,
      toolName: m.toolName,
      toolInput: m.toolInput,
      timestamp: m.timestamp,
    }));
    if (msgs.length === 0) return;
    appRpc.request['session.messages.save']({ messages: msgs }).catch((err: unknown) => {
      console.error('[session.messages.save] persist error:', err);
    });
  }, 2000);

  msgPersistTimers.set(session.sessionId, timer);
}

// ── RPC event listeners (registered once) ────────────────────────────────────

let listenersRegistered = false;

function ensureListeners() {
  if (listenersRegistered) return;
  listenersRegistered = true;

  // agent.message — raw messages from sidecar, converted to display format
  appRpc.addMessageListener('agent.message', (payload: {
    sessionId: string;
    messages: Array<{
      id: string;
      type: string;
      parentId?: string;
      content: unknown;
      timestamp: number;
    }>;
  }) => {
    const session = sessions[payload.sessionId];
    if (!session) return;

    const converted: AgentMessage[] = [];
    for (const raw of payload.messages) {
      const msg = convertRawMessage(raw);
      if (msg) converted.push(msg);
    }

    if (converted.length > 0) {
      session.messages = [...session.messages, ...converted];
      persistMessages(session);
    }
  });

  // agent.status — session status changes
  appRpc.addMessageListener('agent.status', (payload: {
    sessionId: string;
    status: string;
    error?: string;
  }) => {
    const session = sessions[payload.sessionId];
    if (!session) return;

    session.status = normalizeStatus(payload.status);
    if (payload.error) session.error = payload.error;

    // Persist on every status change
    persistSession(session);

    // Schedule cleanup after done/error (Fix #2)
    if (session.status === 'done' || session.status === 'error') {
      // Flush any pending message persistence immediately
      const pendingTimer = msgPersistTimers.get(session.sessionId);
      if (pendingTimer) {
        clearTimeout(pendingTimer);
        msgPersistTimers.delete(session.sessionId);
      }
      persistMessages(session);
      scheduleCleanup(session.sessionId, session.projectId);
    }
  });

  // agent.cost — token/cost updates
  appRpc.addMessageListener('agent.cost', (payload: {
    sessionId: string;
    costUsd: number;
    inputTokens: number;
    outputTokens: number;
  }) => {
    const session = sessions[payload.sessionId];
    if (!session) return;

    session.costUsd = payload.costUsd;
    session.inputTokens = payload.inputTokens;
    session.outputTokens = payload.outputTokens;
  });
}

// ── Cleanup scheduling (Fix #2) ──────────────────────────────────────────────

const CLEANUP_GRACE_MS = 60_000; // 60 seconds after done/error

function scheduleCleanup(sessionId: string, projectId: string) {
  // Cancel any existing timer for this session
  const existing = cleanupTimers.get(sessionId);
  if (existing) clearTimeout(existing);

  const timer = setTimeout(() => {
    cleanupTimers.delete(sessionId);
    // Only clean up if session is still in done/error state
    const session = sessions[sessionId];
    if (session && (session.status === 'done' || session.status === 'error')) {
      // Keep session data (messages, cost) but remove from projectSessionMap
      // so starting a new session on this project works cleanly
      const currentMapped = projectSessionMap.get(projectId);
      if (currentMapped === sessionId) {
        projectSessionMap.delete(projectId);
      }
    }
  }, CLEANUP_GRACE_MS);

  cleanupTimers.set(sessionId, timer);
}

// ── Message conversion ───────────────────────────────────────────────────────

function convertRawMessage(raw: {
  id: string;
  type: string;
  parentId?: string;
  content: unknown;
  timestamp: number;
}): AgentMessage | null {
  const c = raw.content as Record<string, unknown> | undefined;

  switch (raw.type) {
    case 'text':
      return {
        id: raw.id,
        role: 'assistant',
        content: String(c?.text ?? ''),
        timestamp: raw.timestamp,
      };

    case 'thinking':
      return {
        id: raw.id,
        role: 'thinking',
        content: String(c?.text ?? ''),
        timestamp: raw.timestamp,
      };

    case 'tool_call': {
      const name = String(c?.name ?? 'Tool');
      const input = c?.input as Record<string, unknown> | undefined;
      const path = extractToolPath(name, input);
      return {
        id: raw.id,
        role: 'tool-call',
        content: formatToolInput(name, input),
        toolName: name,
        toolInput: JSON.stringify(input, null, 2),
        toolPath: path,
        timestamp: raw.timestamp,
      };
    }

    case 'tool_result': {
      const output = c?.output;
      const text = typeof output === 'string'
        ? output
        : JSON.stringify(output, null, 2);
      return {
        id: raw.id,
        role: 'tool-result',
        content: truncateOutput(text, 500),
        timestamp: raw.timestamp,
      };
    }

    case 'init': {
      const model = String(c?.model ?? '');
      const sid = String(c?.sessionId ?? '');
      for (const s of Object.values(sessions)) {
        if (s.sessionId === raw.id || (sid && s.sessionId.includes(sid.slice(0, 8)))) {
          if (model) s.model = model;
        }
      }
      return {
        id: raw.id,
        role: 'system',
        content: `Session initialized${model ? ` (${model})` : ''}`,
        timestamp: raw.timestamp,
      };
    }

    case 'error':
      return {
        id: raw.id,
        role: 'system',
        content: `Error: ${String(c?.message ?? 'Unknown error')}`,
        timestamp: raw.timestamp,
      };

    case 'cost':
    case 'status':
    case 'compaction':
    case 'unknown':
      return null;

    default:
      return null;
  }
}

function extractToolPath(name: string, input: Record<string, unknown> | undefined): string | undefined {
  if (!input) return undefined;
  if (typeof input.file_path === 'string') return input.file_path;
  if (typeof input.path === 'string') return input.path;
  if (name === 'Bash' && typeof input.command === 'string') {
    return input.command.length > 80 ? input.command.slice(0, 80) + '...' : input.command;
  }
  return undefined;
}

function formatToolInput(name: string, input: Record<string, unknown> | undefined): string {
  if (!input) return '';
  if (name === 'Bash' && typeof input.command === 'string') return input.command;
  if (typeof input.file_path === 'string') return input.file_path;
  return JSON.stringify(input, null, 2);
}

function truncateOutput(text: string, maxLines: number): string {
  const lines = text.split('\n');
  if (lines.length <= maxLines) return text;
  return lines.slice(0, maxLines).join('\n') + `\n... (${lines.length - maxLines} more lines)`;
}

function normalizeStatus(status: string): AgentStatus {
  if (status === 'running' || status === 'idle' || status === 'done' || status === 'error') {
    return status;
  }
  return 'idle';
}

// ── Public API ───────────────────────────────────────────────────────────────

/** Start an agent session for a project (Fix #5: reads permission_mode + system_prompt from settings). */
export async function startAgent(
  projectId: string,
  provider: string,
  prompt: string,
  options: StartOptions = {},
): Promise<{ ok: boolean; error?: string }> {
  ensureListeners();

  // If there's an existing done/error session for this project, clear it first
  clearSession(projectId);

  const sessionId = `${projectId}-${Date.now()}`;

  // Read settings defaults if not explicitly provided (Fix #5)
  let permissionMode = options.permissionMode;
  let systemPrompt = options.systemPrompt;
  try {
    const { settings } = await appRpc.request['settings.getAll']({});
    if (!permissionMode && settings['permission_mode']) {
      permissionMode = settings['permission_mode'];
    }
    if (!systemPrompt && settings['system_prompt_template']) {
      systemPrompt = settings['system_prompt_template'];
    }
  } catch { /* use provided or defaults */ }

  // Create reactive session state
  sessions[sessionId] = {
    sessionId,
    projectId,
    provider,
    status: 'running',
    messages: [{
      id: `${sessionId}-user-0`,
      role: 'user',
      content: prompt,
      timestamp: Date.now(),
    }],
    costUsd: 0,
    inputTokens: 0,
    outputTokens: 0,
    model: options.model ?? 'claude-opus-4-5',
  };

  projectSessionMap.set(projectId, sessionId);

  const result = await appRpc.request['agent.start']({
    sessionId,
    provider: provider as 'claude' | 'codex' | 'ollama',
    prompt,
    cwd: options.cwd,
    model: options.model,
    systemPrompt: systemPrompt,
    maxTurns: options.maxTurns,
    permissionMode: permissionMode,
    claudeConfigDir: options.claudeConfigDir,
    extraEnv: validateExtraEnv(options.extraEnv),
  });

  if (!result.ok) {
    sessions[sessionId].status = 'error';
    sessions[sessionId].error = result.error;
  }

  return result;
}

/** Stop a running agent session for a project. */
export async function stopAgent(projectId: string): Promise<{ ok: boolean; error?: string }> {
  const sessionId = projectSessionMap.get(projectId);
  if (!sessionId) return { ok: false, error: 'No session for project' };

  const result = await appRpc.request['agent.stop']({ sessionId });

  if (result.ok) {
    const session = sessions[sessionId];
    if (session) session.status = 'done';
  }

  return result;
}

/** Send a follow-up prompt to a running session. */
export async function sendPrompt(projectId: string, prompt: string): Promise<{ ok: boolean; error?: string }> {
  const sessionId = projectSessionMap.get(projectId);
  if (!sessionId) return { ok: false, error: 'No session for project' };

  const session = sessions[sessionId];
  if (!session) return { ok: false, error: 'Session not found' };

  // Add user message immediately
  session.messages = [...session.messages, {
    id: `${sessionId}-user-${Date.now()}`,
    role: 'user',
    content: prompt,
    timestamp: Date.now(),
  }];

  session.status = 'running';

  return appRpc.request['agent.prompt']({ sessionId, prompt });
}

/** Get the current session for a project (reactive). */
export function getSession(projectId: string): AgentSession | undefined {
  const sessionId = projectSessionMap.get(projectId);
  if (!sessionId) return undefined;
  return sessions[sessionId];
}

/** Check if a project has an active session. */
export function hasSession(projectId: string): boolean {
  return projectSessionMap.has(projectId);
}

/**
 * Clear a done/error session for a project (Fix #2).
 * Removes from projectSessionMap so a new session can start.
 * Keeps session data in sessions map for history access.
 */
export function clearSession(projectId: string): void {
  const sessionId = projectSessionMap.get(projectId);
  if (!sessionId) return;

  const session = sessions[sessionId];
  if (session && (session.status === 'done' || session.status === 'error')) {
    projectSessionMap.delete(projectId);
    // Cancel any pending cleanup timer
    const timer = cleanupTimers.get(sessionId);
    if (timer) {
      clearTimeout(timer);
      cleanupTimers.delete(sessionId);
    }
  }
}

/**
 * Load the last session for a project from SQLite (for restart recovery).
 * Restores session state + messages into the reactive store.
 * Only restores done/error sessions (running sessions are gone after restart).
 */
export async function loadLastSession(projectId: string): Promise<boolean> {
  ensureListeners();
  try {
    const { session } = await appRpc.request['session.load']({ projectId });
    if (!session) return false;

    // Only restore completed sessions (running sessions can't be resumed)
    if (session.status !== 'done' && session.status !== 'error') return false;

    // Load messages for this session
    const { messages: storedMsgs } = await appRpc.request['session.messages.load']({
      sessionId: session.sessionId,
    });

    const restoredMessages: AgentMessage[] = storedMsgs.map((m: {
      msgId: string; role: string; content: string;
      toolName?: string; toolInput?: string; timestamp: number;
    }) => ({
      id: m.msgId,
      role: m.role as MsgRole,
      content: m.content,
      toolName: m.toolName,
      toolInput: m.toolInput,
      timestamp: m.timestamp,
    }));

    sessions[session.sessionId] = {
      sessionId: session.sessionId,
      projectId: session.projectId,
      provider: session.provider,
      status: normalizeStatus(session.status),
      messages: restoredMessages,
      costUsd: session.costUsd,
      inputTokens: session.inputTokens,
      outputTokens: session.outputTokens,
      model: session.model,
      error: session.error,
    };

    projectSessionMap.set(projectId, session.sessionId);
    return true;
  } catch (err) {
    console.error('[loadLastSession] error:', err);
    return false;
  }
}

/** Initialize listeners on module load. */
ensureListeners();
