import { promises as fs } from 'node:fs';
import path from 'node:path';

import { writeTasksFile } from './tasks-file.js';
import type { CreateMissionOptions, Mission, MissionMilestone, MissionSession } from './types.js';
import { isMissionStatus } from './types.js';

const DEFAULT_ORCHESTRATOR_DIR = '.mosaic/orchestrator';
const DEFAULT_MISSION_FILE = 'mission.json';
const DEFAULT_TASKS_FILE = 'docs/TASKS.md';
const DEFAULT_MANIFEST_FILE = 'docs/MISSION-MANIFEST.md';
const DEFAULT_SCRATCHPAD_DIR = 'docs/scratchpads';
const DEFAULT_MILESTONE_VERSION = '0.0.1';

function asRecord(value: unknown): Record<string, unknown> {
  if (typeof value === 'object' && value !== null) {
    return value as Record<string, unknown>;
  }

  return {};
}

function readString(
  source: Record<string, unknown>,
  ...keys: readonly string[]
): string | undefined {
  for (const key of keys) {
    const value = source[key];
    if (typeof value === 'string') {
      const trimmed = value.trim();
      if (trimmed.length > 0) {
        return trimmed;
      }
    }
  }

  return undefined;
}

function readNumber(
  source: Record<string, unknown>,
  ...keys: readonly string[]
): number | undefined {
  for (const key of keys) {
    const value = source[key];
    if (typeof value === 'number' && Number.isFinite(value)) {
      return value;
    }
  }

  return undefined;
}

function normalizeMilestoneStatus(status: string | undefined): MissionMilestone['status'] {
  if (status === 'completed') return 'completed';
  if (status === 'in-progress') return 'in-progress';
  if (status === 'blocked') return 'blocked';
  return 'pending';
}

function normalizeSessionRuntime(runtime: string | undefined): MissionSession['runtime'] {
  if (runtime === 'claude' || runtime === 'codex' || runtime === 'unknown') {
    return runtime;
  }
  return 'unknown';
}

function normalizeEndedReason(reason: string | undefined): MissionSession['endedReason'] {
  if (
    reason === 'completed' ||
    reason === 'paused' ||
    reason === 'crashed' ||
    reason === 'killed' ||
    reason === 'unknown'
  ) {
    return reason;
  }
  return undefined;
}

function normalizeMission(raw: unknown, resolvedProjectPath: string): Mission {
  const source = asRecord(raw);

  const id = readString(source, 'id', 'mission_id') ?? 'mission';
  const name = readString(source, 'name') ?? 'Unnamed Mission';
  const statusCandidate = readString(source, 'status') ?? 'inactive';
  const status = isMissionStatus(statusCandidate) ? statusCandidate : 'inactive';

  const mission: Mission = {
    schemaVersion: 1,
    id,
    name,
    description: readString(source, 'description'),
    projectPath: readString(source, 'projectPath', 'project_path') ?? resolvedProjectPath,
    createdAt: readString(source, 'createdAt', 'created_at') ?? new Date().toISOString(),
    status,
    tasksFile: readString(source, 'tasksFile', 'tasks_file') ?? DEFAULT_TASKS_FILE,
    manifestFile: readString(source, 'manifestFile', 'manifest_file') ?? DEFAULT_MANIFEST_FILE,
    scratchpadFile:
      readString(source, 'scratchpadFile', 'scratchpad_file') ??
      `${DEFAULT_SCRATCHPAD_DIR}/${id}.md`,
    orchestratorDir:
      readString(source, 'orchestratorDir', 'orchestrator_dir') ?? DEFAULT_ORCHESTRATOR_DIR,
    taskPrefix: readString(source, 'taskPrefix', 'task_prefix'),
    qualityGates: readString(source, 'qualityGates', 'quality_gates'),
    milestoneVersion: readString(source, 'milestoneVersion', 'milestone_version'),
    milestones: [],
    sessions: [],
  };

  const milestonesRaw = Array.isArray(source.milestones) ? source.milestones : [];
  mission.milestones = milestonesRaw.map(
    (milestoneValue: unknown, index: number): MissionMilestone => {
      const milestone = asRecord(milestoneValue);
      return {
        id: readString(milestone, 'id') ?? `phase-${index + 1}`,
        name: readString(milestone, 'name') ?? `Phase ${index + 1}`,
        status: normalizeMilestoneStatus(readString(milestone, 'status')),
        branch: readString(milestone, 'branch'),
        issueRef: readString(milestone, 'issueRef', 'issue_ref'),
        startedAt: readString(milestone, 'startedAt', 'started_at'),
        completedAt: readString(milestone, 'completedAt', 'completed_at'),
      };
    },
  );

  const sessionsRaw = Array.isArray(source.sessions) ? source.sessions : [];
  mission.sessions = sessionsRaw.map((sessionValue: unknown, index: number): MissionSession => {
    const session = asRecord(sessionValue);
    const fallbackSessionId = `sess-${String(index + 1).padStart(3, '0')}`;

    return {
      sessionId: readString(session, 'sessionId', 'session_id') ?? fallbackSessionId,
      runtime: normalizeSessionRuntime(readString(session, 'runtime')),
      pid: readNumber(session, 'pid'),
      startedAt: readString(session, 'startedAt', 'started_at') ?? mission.createdAt,
      endedAt: readString(session, 'endedAt', 'ended_at'),
      endedReason: normalizeEndedReason(readString(session, 'endedReason', 'ended_reason')),
      milestoneId: readString(session, 'milestoneId', 'milestone_id'),
      lastTaskId: readString(session, 'lastTaskId', 'last_task_id'),
      durationSeconds: readNumber(session, 'durationSeconds', 'duration_seconds'),
    };
  });

  return mission;
}

function missionIdFromName(name: string): string {
  const slug = name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .replace(/-{2,}/g, '-');
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  return `${slug || 'mission'}-${date}`;
}

function toAbsolutePath(basePath: string, targetPath: string): string {
  if (path.isAbsolute(targetPath)) {
    return targetPath;
  }
  return path.join(basePath, targetPath);
}

function isNodeErrorWithCode(error: unknown, code: string): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    (error as { code?: string }).code === code
  );
}

async function fileExists(filePath: string): Promise<boolean> {
  try {
    await fs.access(filePath);
    return true;
  } catch (error) {
    if (isNodeErrorWithCode(error, 'ENOENT')) {
      return false;
    }
    throw error;
  }
}

async function writeFileAtomic(filePath: string, content: string): Promise<void> {
  const directory = path.dirname(filePath);
  await fs.mkdir(directory, { recursive: true });

  const tempPath = path.join(
    directory,
    `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random()
      .toString(16)
      .slice(2)}`,
  );

  await fs.writeFile(tempPath, content, 'utf8');
  await fs.rename(tempPath, filePath);
}

function renderManifest(mission: Mission): string {
  const milestoneRows = mission.milestones
    .map((milestone, index) => {
      const issue = milestone.issueRef ?? '—';
      const branch = milestone.branch ?? '—';
      const started = milestone.startedAt ?? '—';
      const completed = milestone.completedAt ?? '—';
      return `| ${index + 1} | ${milestone.id} | ${milestone.name} | ${milestone.status} | ${branch} | ${issue} | ${started} | ${completed} |`;
    })
    .join('\n');

  const body = [
    `# Mission Manifest — ${mission.name}`,
    '',
    '> Persistent document tracking full mission scope, status, and session history.',
    '',
    '## Mission',
    '',
    `**ID:** ${mission.id}`,
    `**Statement:** ${mission.description ?? ''}`,
    '**Phase:** Intake',
    '**Current Milestone:** —',
    `**Progress:** 0 / ${mission.milestones.length} milestones`,
    `**Status:** ${mission.status}`,
    `**Last Updated:** ${new Date().toISOString().replace('T', ' ').replace(/\..+/, ' UTC')}`,
    '',
    '## Milestones',
    '',
    '| # | ID | Name | Status | Branch | Issue | Started | Completed |',
    '|---|-----|------|--------|--------|-------|---------|-----------|',
    milestoneRows,
    '',
    '## Session History',
    '',
    '| Session | Runtime | Started | Duration | Ended Reason | Last Task |',
    '|---------|---------|---------|----------|--------------|-----------|',
    '',
    `## Scratchpad\n\nPath: \`${mission.scratchpadFile}\``,
    '',
  ];

  return body.join('\n');
}

function renderScratchpad(mission: Mission): string {
  return [
    `# Mission Scratchpad — ${mission.name}`,
    '',
    '> Append-only log. NEVER delete entries. NEVER overwrite sections.',
    '',
    '## Original Mission Prompt',
    '',
    '```',
    '(Paste the mission prompt here on first session)',
    '```',
    '',
    '## Planning Decisions',
    '',
    '## Session Log',
    '',
    '| Session | Date | Milestone | Tasks Done | Outcome |',
    '|---------|------|-----------|------------|---------|',
    '',
    '## Open Questions',
    '',
    '## Corrections',
    '',
  ].join('\n');
}

function buildMissionFromOptions(
  options: CreateMissionOptions,
  resolvedProjectPath: string,
): Mission {
  const id = missionIdFromName(options.name);
  const milestones = (options.milestones ?? []).map((name, index): MissionMilestone => {
    const cleanName = name.trim();
    const milestoneName = cleanName.length > 0 ? cleanName : `Phase ${index + 1}`;
    return {
      id: `phase-${index + 1}`,
      name: milestoneName,
      status: 'pending' as const,
      branch: milestoneName
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, ''),
    };
  });

  return {
    schemaVersion: 1,
    id,
    name: options.name,
    description: options.description,
    projectPath: resolvedProjectPath,
    createdAt: new Date().toISOString(),
    status: 'active',
    tasksFile: DEFAULT_TASKS_FILE,
    manifestFile: DEFAULT_MANIFEST_FILE,
    scratchpadFile: `${DEFAULT_SCRATCHPAD_DIR}/${id}.md`,
    orchestratorDir: DEFAULT_ORCHESTRATOR_DIR,
    taskPrefix: options.prefix,
    qualityGates: options.qualityGates,
    milestoneVersion: options.version ?? DEFAULT_MILESTONE_VERSION,
    milestones,
    sessions: [],
  };
}

export function missionFilePath(projectPath: string, mission?: Mission): string {
  const orchestratorDir = mission?.orchestratorDir ?? DEFAULT_ORCHESTRATOR_DIR;
  const baseDir = path.isAbsolute(orchestratorDir)
    ? orchestratorDir
    : path.join(projectPath, orchestratorDir);
  return path.join(baseDir, DEFAULT_MISSION_FILE);
}

export async function saveMission(mission: Mission): Promise<void> {
  const filePath = missionFilePath(mission.projectPath, mission);
  const payload = `${JSON.stringify(mission, null, 2)}\n`;
  await writeFileAtomic(filePath, payload);
}

export async function createMission(options: CreateMissionOptions): Promise<Mission> {
  const name = options.name.trim();
  if (name.length === 0) {
    throw new Error('Mission name is required');
  }

  const resolvedProjectPath = path.resolve(options.projectPath ?? process.cwd());
  const mission = buildMissionFromOptions({ ...options, name }, resolvedProjectPath);

  const missionPath = missionFilePath(resolvedProjectPath, mission);
  const hasExistingMission = await fileExists(missionPath);

  if (hasExistingMission) {
    const existingRaw = await fs.readFile(missionPath, 'utf8');
    const existingMission = normalizeMission(JSON.parse(existingRaw), resolvedProjectPath);
    const active = existingMission.status === 'active' || existingMission.status === 'paused';
    if (active && options.force !== true) {
      throw new Error(
        `Active mission exists: ${existingMission.name} (${existingMission.status}). Use force to overwrite.`,
      );
    }
  }

  await saveMission(mission);

  const manifestPath = toAbsolutePath(resolvedProjectPath, mission.manifestFile);
  const scratchpadPath = toAbsolutePath(resolvedProjectPath, mission.scratchpadFile);
  const tasksPath = toAbsolutePath(resolvedProjectPath, mission.tasksFile);

  if (options.force === true || !(await fileExists(manifestPath))) {
    await writeFileAtomic(manifestPath, renderManifest(mission));
  }

  if (!(await fileExists(scratchpadPath))) {
    await writeFileAtomic(scratchpadPath, renderScratchpad(mission));
  }

  if (!(await fileExists(tasksPath))) {
    await writeFileAtomic(tasksPath, writeTasksFile([]));
  }

  return mission;
}

export async function loadMission(projectPath: string): Promise<Mission> {
  const resolvedProjectPath = path.resolve(projectPath);
  const filePath = missionFilePath(resolvedProjectPath);

  let raw: string;
  try {
    raw = await fs.readFile(filePath, 'utf8');
  } catch (error) {
    if (isNodeErrorWithCode(error, 'ENOENT')) {
      throw new Error(`No mission found at ${filePath}`);
    }
    throw error;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    throw new Error(`Invalid JSON in mission file: ${filePath}`);
  }

  const mission = normalizeMission(parsed, resolvedProjectPath);
  if (mission.status === 'inactive') {
    throw new Error('Mission exists but is inactive. Re-initialize with mosaic coord init.');
  }

  return mission;
}
