/**
 * mosaic-extension.ts — Pi Extension for Mosaic Framework
 *
 * Integrates the Mosaic agent framework into Pi sessions launched via `mosaic pi`.
 * Handles:
 *   1. Session start — run repo hooks, detect active mission, display status
 *   2. Session end — run repo hooks, clean up session lock
 *   3. Mission context — inject active mission state into conversation
 *   4. Memory routing — remind agent to use ~/.config/mosaic/memory/
 */

import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';
import {
  existsSync,
  readFileSync,
  writeFileSync,
  unlinkSync,
  mkdirSync,
  renameSync,
} from 'node:fs';
import { join, basename } from 'node:path';
import { homedir } from 'node:os';
import { execSync, spawnSync } from 'node:child_process';

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Native heartbeat (fleet R14/R15)
// ---------------------------------------------------------------------------
// When this agent runs under the Mosaic fleet (MOSAIC_AGENT_NAME set), the
// extension writes its OWN heartbeat in the same .hb contract `fleet ps` reads
// (ts/pid/status[/model]) and touches a `.hb.native` precedence marker so the
// shell sidecar defers. Native HB knows the real turn state (busy/ok), so it is
// more accurate than the pane-PID-only sidecar fallback.
const HB_AGENT_NAME = process.env['MOSAIC_AGENT_NAME'] ?? '';
const HB_RUN_DIR = process.env['MOSAIC_HEARTBEAT_RUN_DIR'] ?? join(MOSAIC_HOME, 'fleet', 'run');
const HB_INTERVAL_MS = (() => {
  const s = Number.parseInt(process.env['MOSAIC_HEARTBEAT_INTERVAL'] ?? '', 10);
  return Number.isFinite(s) && s > 0 ? s * 1000 : 15_000;
})();

function nativeHbEnabled(): boolean {
  return HB_AGENT_NAME.length > 0;
}

function readModelId(ctx: ExtensionContext): string | null {
  const m = ctx.model as unknown as { id?: string; name?: string } | undefined;
  return m?.id ?? m?.name ?? null;
}

function writeNativeHeartbeat(status: 'ok' | 'busy', model: string | null): void {
  if (!nativeHbEnabled()) return;
  try {
    mkdirSync(HB_RUN_DIR, { recursive: true });
    const hb = join(HB_RUN_DIR, `${HB_AGENT_NAME}.hb`);
    const lines = [`ts=${nowIso()}`, `pid=${process.pid}`, `status=${status}`];
    if (model) lines.push(`model=${model}`);
    const tmp = `${hb}.tmp.${process.pid}`;
    writeFileSync(tmp, lines.join('\n') + '\n');
    renameSync(tmp, hb); // atomic replace — fleet ps never reads a partial file
    // Precedence marker: tells the shell sidecar that native HB is authoritative.
    writeFileSync(join(HB_RUN_DIR, `${HB_AGENT_NAME}.hb.native`), nowIso() + '\n');
  } catch {
    // Best-effort: never let heartbeat I/O disrupt the Pi session.
  }
}

function clearNativeMarker(): void {
  if (!nativeHbEnabled()) return;
  try {
    const m = join(HB_RUN_DIR, `${HB_AGENT_NAME}.hb.native`);
    if (existsSync(m)) unlinkSync(m); // native stopping — let the sidecar take over
  } catch {
    /* ignore */
  }
}

function safeRead(filePath: string): string | null {
  try {
    return readFileSync(filePath, 'utf-8');
  } catch {
    return null;
  }
}

function safeJsonRead(filePath: string): Record<string, unknown> | null {
  const raw = safeRead(filePath);
  if (!raw) return null;
  try {
    return JSON.parse(raw) as Record<string, unknown>;
  } catch {
    return null;
  }
}

function nowIso(): string {
  return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
}

// ---------------------------------------------------------------------------
// Mission detection
// ---------------------------------------------------------------------------

interface ActiveMission {
  name: string;
  id: string;
  status: string;
  milestonesTotal: number;
  milestonesCompleted: number;
}

function detectMission(cwd: string): ActiveMission | null {
  const missionFile = join(cwd, '.mosaic', 'orchestrator', 'mission.json');
  const data = safeJsonRead(missionFile);
  if (!data) return null;

  const status = String(data.status ?? 'inactive');
  if (status !== 'active' && status !== 'paused') return null;

  const milestones = Array.isArray(data.milestones) ? data.milestones : [];
  const completed = milestones.filter(
    (m: unknown) =>
      typeof m === 'object' && m !== null && (m as Record<string, unknown>).status === 'completed',
  ).length;

  return {
    name: String(data.name ?? 'unnamed'),
    id: String(data.mission_id ?? ''),
    status,
    milestonesTotal: milestones.length,
    milestonesCompleted: completed,
  };
}

// ---------------------------------------------------------------------------
// Session lock management
// ---------------------------------------------------------------------------

function sessionLockPath(cwd: string): string {
  return join(cwd, '.mosaic', 'orchestrator', 'session.lock');
}

function writeSessionLock(cwd: string): void {
  const lockDir = join(cwd, '.mosaic', 'orchestrator');
  if (!existsSync(lockDir)) return; // Only write lock if orchestrator dir exists

  const lock = {
    session_id: `pi-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`,
    runtime: 'pi',
    pid: process.pid,
    started_at: nowIso(),
    project_path: cwd,
    milestone_id: '',
  };

  try {
    writeFileSync(sessionLockPath(cwd), JSON.stringify(lock, null, 2) + '\n', 'utf-8');
  } catch {
    // Non-fatal — orchestrator dir may not be writable
  }
}

function cleanSessionLock(cwd: string): void {
  try {
    const lockFile = sessionLockPath(cwd);
    if (existsSync(lockFile)) {
      unlinkSync(lockFile);
    }
  } catch {
    // Non-fatal
  }
}

// ---------------------------------------------------------------------------
// Repo hooks
// ---------------------------------------------------------------------------

function runRepoHook(cwd: string, hookName: string): void {
  const script = join(cwd, 'scripts', 'agent', `${hookName}.sh`);
  if (!existsSync(script)) return;

  try {
    spawnSync('bash', [script], {
      cwd,
      stdio: 'pipe',
      timeout: 30_000,
      env: { ...process.env, MOSAIC_RUNTIME: 'pi' },
    });
  } catch {
    // Non-fatal
  }
}

// ---------------------------------------------------------------------------
// Build mission summary for notifications
// ---------------------------------------------------------------------------

function buildMissionSummary(cwd: string, mission: ActiveMission): string {
  const lines: string[] = [
    `Mission: ${mission.name}`,
    `Status: ${mission.status} | Milestones: ${mission.milestonesCompleted}/${mission.milestonesTotal}`,
  ];

  // Task counts
  const tasksFile = join(cwd, 'docs', 'TASKS.md');
  const tasksContent = safeRead(tasksFile);
  if (tasksContent) {
    const tableRows = tasksContent
      .split('\n')
      .filter((l) => l.startsWith('|') && !l.includes('---'));
    const total = Math.max(0, tableRows.length - 1); // minus header
    const done = (tasksContent.match(/\|\s*done\s*\|/gi) ?? []).length;
    lines.push(`Tasks: ${done} done / ${total} total`);
  }

  // Latest scratchpad
  try {
    const spDir = join(cwd, 'docs', 'scratchpads');
    if (existsSync(spDir)) {
      const files = execSync(`ls -t "${spDir}"/*.md 2>/dev/null | head -1`, {
        encoding: 'utf-8',
        timeout: 5000,
      }).trim();
      if (files) lines.push(`Scratchpad: ${basename(files)}`);
    }
  } catch {
    // Non-fatal
  }

  lines.push('', 'Read ORCHESTRATOR-PROTOCOL.md + TASKS.md before proceeding.');
  return lines.join('\n');
}

// ---------------------------------------------------------------------------
// Extension registration
// ---------------------------------------------------------------------------

export default function register(pi: ExtensionAPI) {
  let sessionCwd = process.cwd();
  let hbStatus: 'ok' | 'busy' = 'ok';
  let hbModel: string | null = null;
  let hbTimer: ReturnType<typeof setInterval> | null = null;

  // ── Session Start ─────────────────────────────────────────────────────
  pi.on('session_start', async (_event, ctx) => {
    sessionCwd = process.cwd();

    // Run repo session-start hook
    runRepoHook(sessionCwd, 'session-start');

    // Detect active mission
    const mission = detectMission(sessionCwd);

    if (mission) {
      // Write session lock for orchestrator awareness
      writeSessionLock(sessionCwd);

      const summary = buildMissionSummary(sessionCwd, mission);
      ctx.ui.notify(`🎯 Active Mosaic Mission\n${summary}`, 'info');
    } else {
      ctx.ui.notify('Mosaic framework loaded', 'info');
    }

    // Native heartbeat: write immediately, then on an interval. Idle = 'ok';
    // turn_start/turn_end flip the status so `fleet ps` reflects real activity.
    if (nativeHbEnabled()) {
      hbModel = readModelId(ctx);
      writeNativeHeartbeat('ok', hbModel);
      hbTimer = setInterval(() => writeNativeHeartbeat(hbStatus, hbModel), HB_INTERVAL_MS);
      if (typeof hbTimer.unref === 'function') hbTimer.unref();
    }
  });

  // ── Turn lifecycle → accurate busy/ok heartbeat ───────────────────────
  pi.on('turn_start', async (_event, ctx) => {
    hbStatus = 'busy';
    hbModel = readModelId(ctx) ?? hbModel;
    writeNativeHeartbeat('busy', hbModel);
  });
  pi.on('turn_end', async (_event, ctx) => {
    hbStatus = 'ok';
    hbModel = readModelId(ctx) ?? hbModel;
    writeNativeHeartbeat('ok', hbModel);
  });

  // ── Session Shutdown ──────────────────────────────────────────────────
  // (The pi API event is 'session_shutdown'; the prior 'session_end' handler
  // never fired — fixed here so repo hooks + lock cleanup actually run.)
  pi.on('session_shutdown', async (_event, _ctx) => {
    if (hbTimer) {
      clearInterval(hbTimer);
      hbTimer = null;
    }
    clearNativeMarker();

    // Run repo session-end hook
    runRepoHook(sessionCwd, 'session-end');

    // Clean up session lock
    cleanSessionLock(sessionCwd);
  });

  // ── Register /mosaic-status command ───────────────────────────────────
  pi.registerCommand('mosaic-status', {
    description: 'Show Mosaic mission status for the current project',
    handler: async (_args, ctx) => {
      const mission = detectMission(sessionCwd);
      if (!mission) {
        ctx.ui.notify('No active Mosaic mission in this project.', 'info');
        return;
      }
      const summary = buildMissionSummary(sessionCwd, mission);
      ctx.ui.notify(`🎯 Mission Status\n${summary}`, 'info');
    },
  });

  // ── Register /mosaic-memory command ───────────────────────────────────
  pi.registerCommand('mosaic-memory', {
    description: 'Show Mosaic memory directory path and contents',
    handler: async (_args, ctx) => {
      const memDir = join(MOSAIC_HOME, 'memory');
      if (!existsSync(memDir)) {
        ctx.ui.notify(`Memory directory: ${memDir} (empty)`, 'info');
        return;
      }
      try {
        const files = execSync(`ls -la "${memDir}" 2>/dev/null`, {
          encoding: 'utf-8',
          timeout: 5000,
        }).trim();
        ctx.ui.notify(`Memory directory: ${memDir}\n${files}`, 'info');
      } catch {
        ctx.ui.notify(`Memory directory: ${memDir}`, 'info');
      }
    },
  });

  // ── Register mosaic_mission_status tool (model-callable) ──────────────
  // R14 "proper tool usage": give the agent a first-class tool to load its
  // active Mosaic mission, milestone progress, task counts, and latest
  // scratchpad — so it self-orients on in-flight work before planning,
  // instead of shelling out or guessing. Mirrors the /mosaic-status command
  // but returns the summary as tool output the LLM can read.
  pi.registerTool({
    name: 'mosaic_mission_status',
    label: 'Mosaic Mission Status',
    description:
      'Return the active Mosaic mission, milestone progress, task counts, and latest scratchpad for the current project. Returns a note when no mission is active.',
    promptSnippet: 'Read the active Mosaic mission + task state for the current project',
    promptGuidelines: [
      'Use mosaic_mission_status at the start of a session or task to load the active mission, milestone progress, and open tasks before planning work.',
    ],
    parameters: Type.Object({}),
    async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
      const mission = detectMission(sessionCwd);
      const text = mission
        ? buildMissionSummary(sessionCwd, mission)
        : 'No active Mosaic mission in this project.';
      return {
        content: [{ type: 'text', text }],
        details: mission ? { ...mission } : { active: false },
      };
    },
  });
}
