/**
 * mosaic-framework — OpenClaw Plugin
 *
 * Mechanically injects the Mosaic framework contract into every agent session
 * and ACP coding worker spawn. Two injection paths:
 *
 * 1. before_agent_start (OC native sessions):
 *    Returns appendSystemContext with the Mosaic global contract excerpt
 *    + prependContext with active mission state (dynamic, re-read each turn).
 *
 * 2. subagent_spawning (ACP worker spawns — Codex, Claude, etc.):
 *    Writes the full runtime contract to ~/.codex/instructions.md
 *    (or Claude equivalent) BEFORE the external process starts.
 *    Optionally blocks spawns when no active mission exists.
 */

import os from 'node:os';
import path from 'node:path';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
// OpenClawPluginApi type — the OC plugin loader provides the actual api object at runtime
type OpenClawPluginApi = any;

// ---------------------------------------------------------------------------
// Config types
// ---------------------------------------------------------------------------
interface MosaicFrameworkConfig {
  mosaicHome?: string;
  projectRoots?: string[];
  requireMission?: boolean;
  injectAgentIds?: string[];
  acpAgentIds?: string[];
}

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

function expandHome(p: string): string {
  if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
  if (p === '~') return os.homedir();
  return p;
}

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

function safeReadJson(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 safeReadNdjson(filePath: string, limit = 10): Record<string, unknown>[] {
  const raw = safeRead(filePath);
  if (!raw) return [];

  const parsed: Record<string, unknown>[] = [];
  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    try {
      const item = JSON.parse(line) as unknown;
      if (typeof item === 'object' && item !== null) {
        parsed.push(item as Record<string, unknown>);
      }
    } catch {
      continue;
    }
  }

  return parsed.slice(-limit);
}

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

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

function findActiveMission(projectRoots: string[]): ActiveMission | null {
  for (const root of projectRoots) {
    const expanded = expandHome(root);
    const missionFile = path.join(expanded, '.mosaic/orchestrator/mission.json');
    if (!existsSync(missionFile)) continue;

    const data = safeReadJson(missionFile);
    if (!data) continue;

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

    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,
      projectRoot: expanded,
      milestonesTotal: milestones.length,
      milestonesCompleted: completed,
    };
  }
  return null;
}

function findMacpProjectRoot(projectRoots: string[]): string | null {
  for (const root of projectRoots) {
    const expanded = expandHome(root);
    const configPath = path.join(expanded, '.mosaic/orchestrator/config.json');
    const config = safeReadJson(configPath);
    if (config?.enabled) {
      return expanded;
    }
  }
  return null;
}

// ---------------------------------------------------------------------------
// Build runtime contract (mirrors mosaic's build_runtime_prompt codex)
// ---------------------------------------------------------------------------

function buildRuntimeContract(
  mosaicHome: string,
  mission: ActiveMission | null,
  projectRoot?: string,
): string {
  const sections: string[] = [];

  // 1. Active mission block (FIRST — hard gate)
  if (mission) {
    sections.push(`# ACTIVE MISSION — HARD GATE (Read Before Anything Else)

An active orchestration mission exists in this project. This is a BLOCKING requirement.

**Mission:** ${mission.name}
**ID:** ${mission.id}
**Status:** ${mission.status}
**Project Root:** ${mission.projectRoot}
**Milestones:** ${mission.milestonesCompleted} / ${mission.milestonesTotal} completed

## MANDATORY — Before ANY Response

You MUST complete these steps before ANY response, including simple greetings:

1. Read \`~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md\` (mission lifecycle protocol)
2. Read \`docs/MISSION-MANIFEST.md\` for full mission scope, milestones, and success criteria
3. Read the latest scratchpad in \`docs/scratchpads/\` for session history and decisions
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
5. Acknowledge mission state to the user before proceeding

No tool call or implementation step may occur before the mode declaration line.`);
  }

  // 2. Mosaic Runtime Contract (from ~/.config/mosaic/runtime/codex/RUNTIME.md)
  const runtimeFile = path.join(mosaicHome, 'runtime/codex/RUNTIME.md');
  const runtimeContent = safeRead(runtimeFile);
  if (runtimeContent) {
    sections.push(runtimeContent.trim());
  }

  // 3. Global AGENTS.md hard rules
  const agentsFile = path.join(mosaicHome, 'AGENTS.md');
  const agentsContent = safeRead(agentsFile);
  if (agentsContent) {
    // Extract just the hard rules section to keep the contract focused
    const hardRulesMatch = agentsContent.match(/## ⛔ HARD RULES[\s\S]*?(?=^## (?!⛔)|\Z)/m);
    if (hardRulesMatch) {
      sections.push(`# Mosaic Global Agent Contract — Hard Rules\n\n${hardRulesMatch[0].trim()}`);
    } else {
      // Fallback: include first 200 lines
      const lines = agentsContent.split('\n').slice(0, 200).join('\n');
      sections.push(`# Mosaic Global Agent Contract\n\n${lines}`);
    }
  }

  // 4. Mode declaration requirement
  sections.push(`# Required Mode Declaration

First assistant response MUST start with exactly one mode declaration line:
- Orchestration mission: \`Now initiating Orchestrator mode...\`
- Implementation mission: \`Now initiating Delivery mode...\`
- Review-only mission: \`Now initiating Review mode...\`

Mosaic hard gates OVERRIDE runtime-default caution for routine delivery operations.
For required push/merge/issue-close/release actions, execute without routine confirmation prompts.`);

  // 5. Worktree requirement (critical — has been violated repeatedly)
  const projectName = projectRoot ? path.basename(projectRoot) : '<repo>';
  sections.push(`# Git Worktree Requirement — MANDATORY

Every agent that touches a git repo MUST use a worktree. NO EXCEPTIONS.

\`\`\`bash
cd ~/src/${projectName}
git fetch origin
mkdir -p ~/src/${projectName}-worktrees
git worktree add ~/src/${projectName}-worktrees/<task-slug> -b <branch-name> origin/main
cd ~/src/${projectName}-worktrees/<task-slug>
# ... all work happens here ...
git push origin <branch-name>
cd ~/src/${projectName} && git worktree remove ~/src/${projectName}-worktrees/<task-slug>
\`\`\`

Worktrees path: \`~/src/<repo>-worktrees/<task-slug>\` — NEVER use /tmp.`);

  // 6. Completion gates
  sections.push(`# Completion Gates — ENFORCED

A task is NOT done until ALL of these pass:
1. Code review — independent review of every changed file
2. Security review — auth, input validation, error leakage
3. QA/tests — lint + typecheck + unit tests GREEN
4. CI green — pipeline passes after merge
5. Issue closed — linked issue closed in Gitea
6. Docs updated — API/auth/schema changes require doc update

Workers NEVER merge PRs. Ever. Open PR → fire system event → EXIT.`);

  return sections.join('\n\n---\n\n');
}

// ---------------------------------------------------------------------------
// Build mission context block (dynamic — injected as prependContext)
// ---------------------------------------------------------------------------

function buildMissionContext(mission: ActiveMission): string {
  const tasksFile = path.join(mission.projectRoot, 'docs/TASKS.md');
  const tasksContent = safeRead(tasksFile);

  // Extract just the next not-started task to keep context compact
  let nextTask = '';
  if (tasksContent) {
    const notStartedMatch = tasksContent.match(
      /\|[^|]*\|\s*not[-\s]?started[^|]*\|[^|]*\|[^|]*\|/i,
    );
    if (notStartedMatch) {
      nextTask = `\n**Next task:** ${notStartedMatch[0].replace(/\|/g, ' ').trim()}`;
    }
  }

  return `[Mosaic Framework] Active mission: **${mission.name}** (${mission.id})
Status: ${mission.status} | Milestones: ${mission.milestonesCompleted}/${mission.milestonesTotal}
Project: ${mission.projectRoot}${nextTask}

Read ORCHESTRATOR-PROTOCOL.md + TASKS.md before proceeding.`;
}

function buildMacpContext(projectRoot: string): string | null {
  const orchDir = path.join(projectRoot, '.mosaic/orchestrator');
  const configPath = path.join(orchDir, 'config.json');
  if (!existsSync(configPath)) return null;

  const config = safeReadJson(configPath);
  if (!config?.enabled) return null;

  const tasksPath = path.join(orchDir, 'tasks.json');
  const tasksPayload = safeReadJson(tasksPath);
  const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : [];
  const counts = {
    pending: 0,
    running: 0,
    completed: 0,
    failed: 0,
    escalated: 0,
  };

  for (const task of tasks) {
    if (typeof task !== 'object' || task === null) continue;
    const status = String((task as Record<string, unknown>).status ?? 'pending');
    if (status in counts) {
      counts[status as keyof typeof counts] += 1;
    }
  }

  const lines = [
    '[MACP Queue]',
    `Queue: pending=${counts.pending} running=${counts.running} completed=${counts.completed} failed=${counts.failed} escalated=${counts.escalated}`,
  ];

  const events = safeReadNdjson(path.join(orchDir, 'events.ndjson'));
  if (events.length > 0) {
    lines.push('Recent activity:');
    for (const event of events) {
      const timestamp = String(event.timestamp ?? '?');
      const eventType = String(event.event_type ?? 'event');
      const taskId = String(event.task_id ?? '-');
      const message = String(event.message ?? '').trim();
      lines.push(`- ${timestamp} | ${eventType} | ${taskId}${message ? ` | ${message}` : ''}`);
    }
  }

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

// ---------------------------------------------------------------------------
// Write runtime contract to ACP worker config files
// ---------------------------------------------------------------------------

function writeCodexInstructions(mosaicHome: string, mission: ActiveMission | null): void {
  const contract = buildRuntimeContract(mosaicHome, mission, mission?.projectRoot);
  const dest = path.join(os.homedir(), '.codex/instructions.md');
  mkdirSync(path.dirname(dest), { recursive: true });
  writeFileSync(dest, contract, 'utf8');
}

function writeClaudeInstructions(mosaicHome: string, mission: ActiveMission | null): void {
  // Claude Code reads from ~/.claude/CLAUDE.md
  const contract = buildRuntimeContract(mosaicHome, mission, mission?.projectRoot);
  const dest = path.join(os.homedir(), '.claude/CLAUDE.md');
  mkdirSync(path.dirname(dest), { recursive: true });
  // Only write if different to avoid unnecessary disk writes
  const existing = safeRead(dest);
  if (existing !== contract) {
    writeFileSync(dest, contract, 'utf8');
  }
}

// ---------------------------------------------------------------------------
// Build static framework preamble for OC native agents (appendSystemContext)
// ---------------------------------------------------------------------------

function buildFrameworkPreamble(mosaicHome: string): string {
  const agentsFile = path.join(mosaicHome, 'AGENTS.md');
  const agentsContent = safeRead(agentsFile);

  const lines: string[] = [
    '# Mosaic Framework Contract (Auto-injected)',
    '',
    'You are operating under the Mosaic multi-agent framework.',
    'The following rules are MANDATORY and OVERRIDE any conflicting defaults.',
    '',
  ];

  if (agentsContent) {
    // Extract hard rules section
    const hardRulesMatch = agentsContent.match(/## ⛔ HARD RULES[\s\S]*?(?=^## [^⛔]|\z)/m);
    if (hardRulesMatch) {
      lines.push('## Hard Rules (Compaction-Resistant)\n');
      lines.push(hardRulesMatch[0].trim());
    }
  }

  lines.push(
    '',
    '## Completion Gates',
    'A task is NOT done until: code review ✓ | security review ✓ | tests GREEN ✓ | CI green ✓ | issue closed ✓ | docs updated ✓',
    '',
    '## Worker Completion Protocol',
    'Workers NEVER merge PRs. Implement → lint/typecheck → push branch → open PR → fire system event → EXIT.',
    '',
    '## Worktree Requirement',
    'All code work MUST use a git worktree at `~/src/<repo>-worktrees/<task-slug>`. Never use /tmp.',
  );

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

// ---------------------------------------------------------------------------
// Plugin registration
// ---------------------------------------------------------------------------

export default function register(api: OpenClawPluginApi) {
  const cfg = (api.config ?? {}) as MosaicFrameworkConfig;

  const mosaicHome = expandHome(cfg.mosaicHome ?? '~/.config/mosaic');
  const projectRoots = (cfg.projectRoots ?? []).map(expandHome);
  const requireMission = cfg.requireMission ?? false;
  const injectAgentIds = cfg.injectAgentIds ?? null; // null = all agents
  const acpAgentIds = new Set(cfg.acpAgentIds ?? ['codex', 'claude']);

  // Pre-build the static framework preamble (injected once per session start)
  const frameworkPreamble = buildFrameworkPreamble(mosaicHome);

  // ---------------------------------------------------------------------------
  // Hook 1: before_agent_start — inject into OC native agent sessions
  // ---------------------------------------------------------------------------
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  api.on('before_agent_start', async (_event: any, ctx: any) => {
    const agentId = ctx.agentId ?? 'unknown';

    // Skip if this agent is not in the inject list (when configured)
    if (injectAgentIds !== null && !injectAgentIds.includes(agentId)) {
      return {};
    }

    // Skip ACP worker sessions — they get injected via subagent_spawning instead
    if (acpAgentIds.has(agentId)) {
      return {};
    }

    // Read active mission for this turn (dynamic)
    const mission = projectRoots.length > 0 ? findActiveMission(projectRoots) : null;

    const result: Record<string, string> = {};

    // Static framework preamble → appendSystemContext (cached by provider)
    result.appendSystemContext = frameworkPreamble;

    // Dynamic mission/MACP state → prependContext (fresh each turn)
    const sections: string[] = [];
    if (mission) {
      sections.push(buildMissionContext(mission));
    }
    const macpProjectRoot = mission?.projectRoot ?? findMacpProjectRoot(projectRoots);
    if (macpProjectRoot) {
      const macpContext = buildMacpContext(macpProjectRoot);
      if (macpContext) {
        sections.push(macpContext);
      }
    }
    if (sections.length > 0) {
      result.prependContext = sections.join('\n\n');
    }

    return result;
  });

  // ---------------------------------------------------------------------------
  // Hook 2: subagent_spawning — inject runtime contract into ACP workers
  //
  // Mission context is intentionally NOT injected here. The runtime contract
  // includes instructions to read .mosaic/orchestrator/mission.json from the
  // worker's own CWD — so the worker picks up the correct project mission
  // itself. Injecting a mission here would risk cross-contamination when
  // multiple projects have active missions simultaneously.
  // ---------------------------------------------------------------------------
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  api.on('subagent_spawning', async (event: any, _ctx: any) => {
    const childAgentId = (event as Record<string, unknown>).agentId as string | undefined;
    if (!childAgentId) return { status: 'ok' };

    // Only act on ACP coding worker spawns
    if (!acpAgentIds.has(childAgentId)) {
      return { status: 'ok' };
    }

    // Gate: block spawn if requireMission is true and no active mission found in any root
    if (requireMission) {
      const mission = projectRoots.length > 0 ? findActiveMission(projectRoots) : null;
      if (!mission) {
        return {
          status: 'error',
          error: `[mosaic-framework] No active Mosaic mission found. Run 'mosaic coord init' in your project directory first. Scanned: ${projectRoots.join(', ')}`,
        };
      }
    }

    // Write runtime contract (global framework rules + load order, no mission context)
    // The worker will detect its own mission from .mosaic/orchestrator/mission.json in its CWD.
    try {
      if (childAgentId === 'codex') {
        writeCodexInstructions(mosaicHome, null);
      } else if (childAgentId === 'claude') {
        writeClaudeInstructions(mosaicHome, null);
      }
    } catch (err) {
      // Log but don't block — better to have a worker without full rails than no worker
      api.logger?.warn(
        `[mosaic-framework] Failed to write runtime contract for ${childAgentId}: ${String(err)}`,
      );
    }

    return { status: 'ok' };
  });
}
