import { expectDefined } from '@wrongstack/core';
import * as path from 'node:path';
import {
  Context,
  DefaultAttachmentStore,
  QueueStore,
  RecoveryLock,
  // createSessionEventBridge,
  // resolveAuditLevel,
  type AbandonedSession,
  type SessionStore,
  type SessionWriter,
  type WstackPaths,
  attachTodosCheckpoint,
  loadDirectorState,
  loadPlan,
  loadTodosCheckpoint,
  DEFAULT_SESSION_PRUNE_DAYS,
} from '@wrongstack/core';
export interface SessionResult {
  session: SessionWriter;
  sessionRef: { current?: SessionWriter | undefined };
  context: Context;
  restoredMessages: import('@wrongstack/core').Message[];
  attachments: DefaultAttachmentStore;
  recoveryLock: RecoveryLock;
  queueStore: QueueStore;
  planPath: string;
  detachTodosCheckpoint: () => void;
  /** Director state checkpoint from the prior run — null if this is not a resume. */
  priorFleetState?: import('@wrongstack/core').DirectorStateSnapshot | undefined;
  /** Tool execution records from the prior session (tool_call_end JSONL events). */
  restoredToolCalls: Array<{
    name: string;
    id: string;
    durationMs: number;
    ok: boolean;
    outputBytes?: number | undefined;
    outputTokens?: number | undefined;
    outputLines?: number | undefined;
  }>;
}

export async function setupSession(params: {
  config: { model: string; provider: string };
  wpaths: WstackPaths;
  projectRoot: string;
  cwd: string;
  sessionStore: SessionStore;
  systemPrompt: import('@wrongstack/core').TextBlock[];
  provider: import('@wrongstack/core').Provider;
  tokenCounter: import('@wrongstack/core').TokenCounter;
  renderer: { writeInfo(msg: string): void; writeError(msg: string): void };
  flags: Record<string, unknown>;
  onRecovery: (
    abandoned: AbandonedSession,
    autoRecover: boolean,
  ) => Promise<'resume' | 'delete' | 'skip'>;
}): Promise<SessionResult> {
  const {
    config,
    wpaths,
    projectRoot,
    cwd,
    sessionStore,
    systemPrompt,
    provider,
    tokenCounter,
    renderer,
    flags,
    onRecovery,
  } = params;

  // Prune sessions older than the shared retention window on every interactive start.
  // Best-effort: failures here should not block the user.
  sessionStore
    .prune(DEFAULT_SESSION_PRUNE_DAYS)
    .then((count) => {
      if (count > 0) renderer.writeInfo(`Pruned ${count} old session${count === 1 ? '' : 's'}.`);
    })
    .catch((err) => console.debug(`[session] prune failed: ${err}`));

  let resumeId = typeof flags['resume'] === 'string' ? (flags['resume'] as string) : undefined;

  const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
  if (!resumeId && !flags['no-recovery']) {
    const abandoned = await recoveryLock.checkAbandoned();
    if (abandoned && abandoned.messageCount > 0) {
      const choice = await onRecovery(abandoned, !!flags['recover']);
      if (choice === 'resume') resumeId = abandoned.sessionId;
      else if (choice === 'delete') {
        await sessionStore.delete(abandoned.sessionId).catch(() => undefined);
        await recoveryLock.clear();
      } else await recoveryLock.clear();
    } else if (abandoned) {
      await sessionStore.delete(abandoned.sessionId).catch(() => undefined);
      await recoveryLock.clear();
    }
  }

  let session: SessionWriter | undefined;
  let restoredMessages: import('@wrongstack/core').Message[] = [];
  let restoredToolCalls: SessionResult['restoredToolCalls'] = [];
  if (resumeId) {
    try {
      const resumed = await sessionStore.resume(resumeId);
      session = resumed.writer;
      restoredMessages = resumed.data.messages;
      // Sessions written before tool_call_end events existed (or alternate
      // store impls) may not carry toolCallEnds — missing must not turn a
      // perfectly resumable session into RESUME_FAILED.
      restoredToolCalls = resumed.data.toolCallEnds ?? [];
      renderer.writeInfo(
        `Resumed session ${resumed.data.metadata.id} — ${restoredMessages.length} messages, ${restoredToolCalls.length} tool executions, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`,
      );
    } catch (err) {
      renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
      throw Object.assign(new Error('RESUME_FAILED'), { exitCode: 2 });
    }
  } else {
    session = await sessionStore.create({
      id: '',
      title: '',
      model: config.model,
      provider: config.provider,
    });
  }

  const sessionRef: { current?: SessionWriter | undefined } = { current: session };
  await recoveryLock.write(session?.id).catch(() => undefined);

  const attachments = new DefaultAttachmentStore({
    spoolDir: path.join(wpaths.projectSessions, session?.id, 'attachments'),
  });
  const queueStore = new QueueStore({ dir: path.join(wpaths.projectSessions, session?.id) });

  const ctxSignal = new AbortController().signal;
  const context = new Context({
    systemPrompt,
    provider,
    session: expectDefined(session),
    signal: ctxSignal,
    tokenCounter,
    cwd,
    projectRoot,
    model: config.model,
    agentId: 'leader',
    agentName: 'Leader Agent',
  });
  // Inject package-author-tracker options so the install tool can record authorship.
  context.meta['packageTrackerOpts'] = {
    storageDir: wpaths.projectDir,
    projectRoot,
  };
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);

  const todosCheckpointPath = path.join(wpaths.projectSessions, `${session?.id}.todos.json`);
  if (resumeId) {
    try {
      const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
      if (restoredTodos && restoredTodos.length > 0) {
        context.state.replaceTodos(restoredTodos);
        renderer.writeInfo(
          `Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? '' : 's'} from previous run.`,
        );
      }
    } catch {
      /* best-effort */
    }
  }
  const detachTodosCheckpoint = attachTodosCheckpoint(
    context.state,
    todosCheckpointPath,
    session?.id,
  );

  const planPath = path.join(wpaths.projectSessions, `${session?.id}.plan.json`);
  context.state.setMeta('plan.path', planPath);

  const taskPath = path.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
  context.state.setMeta('task.path', taskPath);

  let dirState;
  if (resumeId) {
    try {
      const fleetRoot = path.join(wpaths.projectSessions, session?.id);
      dirState = await loadDirectorState(path.join(fleetRoot, 'director-state.json'));
      if (dirState) {
        const tCounts: Record<string, number> = {};
        for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
        const summary = Object.entries(tCounts)
          .map(([k, v]) => `${v} ${k}`)
          .join(', ');
        renderer.writeInfo(
          `Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? '' : 's'}, tasks ${summary || '(none)'}.`,
        );
      }
    } catch {
      /* ignore */
    }
    try {
      const plan = await loadPlan(planPath);
      if (plan && plan.items.length > 0) {
        const open = plan.items.filter((p) => p.status !== 'done').length;
        const done = plan.items.length - open;
        renderer.writeInfo(
          `Plan: ${plan.items.length} item${plan.items.length === 1 ? '' : 's'} (${open} open, ${done} done). Use /plan to review.`,
        );
      }
    } catch {
      /* ignore */
    }
  }

  return {
    session: expectDefined(session),
    sessionRef,
    context,
    restoredMessages,
    attachments,
    recoveryLock,
    queueStore,
    planPath,
    detachTodosCheckpoint,
    priorFleetState: dirState ?? undefined,
    restoredToolCalls,
  };
}

// Future (Phase 1+): when emitting richer audit events, resolve via:
// const auditLevel = resolveAuditLevel(fullConfig);
// const bridge = createSessionEventBridge(sessionWriter, auditLevel);
// Prefer passing the bridge instead of raw writer for new audit writes.
