/**
 * Init composition handlers — compound init commands for workflow bootstrapping.
 *
 * Composes existing atomic SDK queries into the same flat JSON bundles
 * that CJS init.cjs produces, enabling workflow migration. Each handler
 * follows the QueryHandler signature and returns { data: <flat JSON> }.
 *
 * Port of get-shit-done/bin/lib/init.cjs (13 of 16 handlers).
 * The 3 complex handlers (new-project, progress, manager) are in init-complex.ts.
 *
 * @example
 * ```typescript
 * import { initExecutePhase, withProjectRoot } from './init.js';
 *
 * const result = await initExecutePhase(['9'], '/project');
 * // { data: { executor_model: 'opus', phase_found: true, ... } }
 * ```
 */

import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
import { readFile, readdir } from 'node:fs/promises';
import { join, relative, basename } from 'node:path';
import { execSync } from 'node:child_process';
import { homedir } from 'node:os';

import { loadConfig } from '../config.js';
import { resolveModel, MODEL_PROFILES } from './config-query.js';
import { findPhase } from './phase.js';
import { roadmapGetPhase, getMilestoneInfo } from './roadmap.js';
import { planningPaths, normalizePhaseName, toPosixPath } from './helpers.js';
import type { QueryHandler } from './utils.js';

// ─── Internal helpers ──────────────────────────────────────────────────────

/**
 * Extract model alias string from a resolveModel result.
 */
async function getModelAlias(agentType: string, projectDir: string): Promise<string> {
  const result = await resolveModel([agentType], projectDir);
  const data = result.data as Record<string, unknown>;
  return (data.model as string) || 'sonnet';
}

/**
 * Generate a slug from text (inline, matches CJS generateSlugInternal).
 */
function generateSlugInternal(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .substring(0, 60);
}

/**
 * Check if a path exists on disk.
 */
function pathExists(base: string, relPath: string): boolean {
  return existsSync(join(base, relPath));
}

/**
 * Get the latest completed milestone from MILESTONES.md.
 * Port of getLatestCompletedMilestone from init.cjs lines 10-25.
 */
function getLatestCompletedMilestone(projectDir: string): { version: string; name: string } | null {
  const milestonesPath = join(projectDir, '.planning', 'MILESTONES.md');
  if (!existsSync(milestonesPath)) return null;

  try {
    const content = readFileSync(milestonesPath, 'utf-8');
    const match = content.match(/^##\s+(v[\d.]+)\s+(.+?)\s+\(Shipped:/m);
    if (!match) return null;
    return { version: match[1], name: match[2].trim() };
  } catch {
    return null;
  }
}

/**
 * Check which GSD agents are installed on disk.
 * Port of checkAgentsInstalled from core.cjs lines 1274-1306.
 */
function checkAgentsInstalled(): { agents_installed: boolean; missing_agents: string[] } {
  const agentsDir = process.env.GSD_AGENTS_DIR
    || join(homedir(), '.claude', 'get-shit-done', 'agents');
  const expectedAgents = Object.keys(MODEL_PROFILES);

  if (!existsSync(agentsDir)) {
    return { agents_installed: false, missing_agents: expectedAgents };
  }

  const missing: string[] = [];
  for (const agent of expectedAgents) {
    const agentFile = join(agentsDir, `${agent}.md`);
    const agentFileCopilot = join(agentsDir, `${agent}.agent.md`);
    if (!existsSync(agentFile) && !existsSync(agentFileCopilot)) {
      missing.push(agent);
    }
  }

  return {
    agents_installed: missing.length === 0,
    missing_agents: missing,
  };
}

/**
 * Extract phase info from findPhase result, or build fallback from roadmap.
 */
async function getPhaseInfoWithFallback(
  phase: string,
  projectDir: string,
): Promise<{ phaseInfo: Record<string, unknown> | null; roadmapPhase: Record<string, unknown> | null }> {
  const phaseResult = await findPhase([phase], projectDir);
  let phaseInfo = phaseResult.data as Record<string, unknown> | null;

  const roadmapResult = await roadmapGetPhase([phase], projectDir);
  const roadmapPhase = roadmapResult.data as Record<string, unknown> | null;

  // Fallback to ROADMAP.md if no phase directory exists yet
  if ((!phaseInfo || !phaseInfo.found) && roadmapPhase?.found) {
    const phaseName = roadmapPhase.phase_name as string;
    phaseInfo = {
      found: true,
      directory: null,
      phase_number: roadmapPhase.phase_number,
      phase_name: phaseName,
      phase_slug: phaseName ? generateSlugInternal(phaseName) : null,
      plans: [],
      summaries: [],
      incomplete_plans: [],
      has_research: false,
      has_context: false,
      has_verification: false,
      has_reviews: false,
    };
  }

  return { phaseInfo, roadmapPhase };
}

/**
 * Extract requirement IDs from roadmap section text.
 */
function extractReqIds(roadmapPhase: Record<string, unknown> | null): string | null {
  const section = roadmapPhase?.section as string | undefined;
  const reqMatch = section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
  const reqExtracted = reqMatch
    ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s: string) => s.trim()).filter(Boolean).join(', ')
    : null;
  return (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
}

// ─── withProjectRoot ─────────────────────────────────────────────────────

/**
 * Inject project_root, agents_installed, missing_agents, and response_language
 * into an init result object.
 *
 * Port of withProjectRoot from init.cjs lines 32-48.
 *
 * @param projectDir - Absolute project root path
 * @param result - The result object to augment
 * @param config - Optional loaded config (avoids re-reading config.json)
 * @returns The augmented result object
 */
export function withProjectRoot(
  projectDir: string,
  result: Record<string, unknown>,
  config?: Record<string, unknown>,
): Record<string, unknown> {
  result.project_root = projectDir;

  const agentStatus = checkAgentsInstalled();
  result.agents_installed = agentStatus.agents_installed;
  result.missing_agents = agentStatus.missing_agents;

  const responseLang = config?.response_language;
  if (responseLang) {
    result.response_language = responseLang;
  }

  return result;
}

// ─── initExecutePhase ─────────────────────────────────────────────────────

/**
 * Init handler for execute-phase workflow.
 * Port of cmdInitExecutePhase from init.cjs lines 50-171.
 */
export const initExecutePhase: QueryHandler = async (args, projectDir) => {
  const phase = args[0];
  if (!phase) {
    return { data: { error: 'phase required for init execute-phase' } };
  }

  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');

  const { phaseInfo, roadmapPhase } = await getPhaseInfoWithFallback(phase, projectDir);
  const phase_req_ids = extractReqIds(roadmapPhase);

  const [executorModel, verifierModel] = await Promise.all([
    getModelAlias('gsd-executor', projectDir),
    getModelAlias('gsd-verifier', projectDir),
  ]);

  const milestone = await getMilestoneInfo(projectDir);

  const phaseFound = !!(phaseInfo && phaseInfo.found);
  const phaseNumber = (phaseInfo?.phase_number as string) || null;
  const phaseSlug = (phaseInfo?.phase_slug as string) || null;
  const plans = (phaseInfo?.plans || []) as string[];
  const summaries = (phaseInfo?.summaries || []) as string[];
  const incompletePlans = (phaseInfo?.incomplete_plans || []) as string[];
  const projectCode = (config as Record<string, unknown>).project_code as string || '';

  const result: Record<string, unknown> = {
    executor_model: executorModel,
    verifier_model: verifierModel,
    commit_docs: config.commit_docs,
    sub_repos: (config as Record<string, unknown>).sub_repos ?? [],
    parallelization: config.parallelization,
    context_window: (config as Record<string, unknown>).context_window ?? 200000,
    branching_strategy: config.git.branching_strategy,
    phase_branch_template: config.git.phase_branch_template,
    milestone_branch_template: config.git.milestone_branch_template,
    verifier_enabled: config.workflow.verifier,
    phase_found: phaseFound,
    phase_dir: (phaseInfo?.directory as string) ?? null,
    phase_number: phaseNumber,
    phase_name: (phaseInfo?.phase_name as string) ?? null,
    phase_slug: phaseSlug,
    phase_req_ids,
    plans,
    summaries,
    incomplete_plans: incompletePlans,
    plan_count: plans.length,
    incomplete_count: incompletePlans.length,
    branch_name: config.git.branching_strategy === 'phase' && phaseInfo
      ? config.git.phase_branch_template
          .replace('{project}', projectCode)
          .replace('{phase}', phaseNumber || '')
          .replace('{slug}', phaseSlug || 'phase')
      : config.git.branching_strategy === 'milestone'
        ? config.git.milestone_branch_template
            .replace('{milestone}', milestone.version)
            .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone')
        : null,
    milestone_version: milestone.version,
    milestone_name: milestone.name,
    milestone_slug: generateSlugInternal(milestone.name),
    state_exists: existsSync(join(planningDir, 'STATE.md')),
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    config_exists: existsSync(join(planningDir, 'config.json')),
    state_path: toPosixPath(relative(projectDir, join(planningDir, 'STATE.md'))),
    roadmap_path: toPosixPath(relative(projectDir, join(planningDir, 'ROADMAP.md'))),
    config_path: toPosixPath(relative(projectDir, join(planningDir, 'config.json'))),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initPlanPhase ────────────────────────────────────────────────────────

/**
 * Init handler for plan-phase workflow.
 * Port of cmdInitPlanPhase from init.cjs lines 173-293.
 */
export const initPlanPhase: QueryHandler = async (args, projectDir) => {
  const phase = args[0];
  if (!phase) {
    return { data: { error: 'phase required for init plan-phase' } };
  }

  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');

  const { phaseInfo, roadmapPhase } = await getPhaseInfoWithFallback(phase, projectDir);
  const phase_req_ids = extractReqIds(roadmapPhase);

  const [researcherModel, plannerModel, checkerModel] = await Promise.all([
    getModelAlias('gsd-phase-researcher', projectDir),
    getModelAlias('gsd-planner', projectDir),
    getModelAlias('gsd-plan-checker', projectDir),
  ]);

  const phaseFound = !!(phaseInfo && phaseInfo.found);
  const phaseNumber = (phaseInfo?.phase_number as string) || null;
  const plans = (phaseInfo?.plans || []) as string[];

  const result: Record<string, unknown> = {
    researcher_model: researcherModel,
    planner_model: plannerModel,
    checker_model: checkerModel,
    research_enabled: config.workflow.research,
    plan_checker_enabled: config.workflow.plan_check,
    nyquist_validation_enabled: config.workflow.nyquist_validation,
    commit_docs: config.commit_docs,
    text_mode: config.workflow.text_mode,
    phase_found: phaseFound,
    phase_dir: (phaseInfo?.directory as string) ?? null,
    phase_number: phaseNumber,
    phase_name: (phaseInfo?.phase_name as string) ?? null,
    phase_slug: (phaseInfo?.phase_slug as string) ?? null,
    padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
    phase_req_ids,
    has_research: (phaseInfo?.has_research as boolean) || false,
    has_context: (phaseInfo?.has_context as boolean) || false,
    has_reviews: (phaseInfo?.has_reviews as boolean) || false,
    has_plans: plans.length > 0,
    plan_count: plans.length,
    planning_exists: existsSync(planningDir),
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    state_path: toPosixPath(relative(projectDir, join(planningDir, 'STATE.md'))),
    roadmap_path: toPosixPath(relative(projectDir, join(planningDir, 'ROADMAP.md'))),
    requirements_path: toPosixPath(relative(projectDir, join(planningDir, 'REQUIREMENTS.md'))),
  };

  // Add artifact paths if phase directory exists
  if (phaseInfo?.directory) {
    const phaseDirFull = join(projectDir, phaseInfo.directory as string);
    try {
      const files = readdirSync(phaseDirFull);
      const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
      if (contextFile) result.context_path = toPosixPath(join(phaseInfo.directory as string, contextFile));
      const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
      if (researchFile) result.research_path = toPosixPath(join(phaseInfo.directory as string, researchFile));
      const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
      if (verificationFile) result.verification_path = toPosixPath(join(phaseInfo.directory as string, verificationFile));
      const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
      if (uatFile) result.uat_path = toPosixPath(join(phaseInfo.directory as string, uatFile));
      const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
      if (reviewsFile) result.reviews_path = toPosixPath(join(phaseInfo.directory as string, reviewsFile));
    } catch { /* intentionally empty */ }
  }

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initNewMilestone ─────────────────────────────────────────────────────

/**
 * Init handler for new-milestone workflow.
 * Port of cmdInitNewMilestone from init.cjs lines 401-446.
 */
export const initNewMilestone: QueryHandler = async (_args, projectDir) => {
  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');
  const milestone = await getMilestoneInfo(projectDir);
  const latestCompleted = getLatestCompletedMilestone(projectDir);

  const phasesDir = join(planningDir, 'phases');
  let phaseDirCount = 0;
  try {
    if (existsSync(phasesDir)) {
      phaseDirCount = readdirSync(phasesDir, { withFileTypes: true })
        .filter(entry => entry.isDirectory())
        .length;
    }
  } catch { /* intentionally empty */ }

  const [researcherModel, synthesizerModel, roadmapperModel] = await Promise.all([
    getModelAlias('gsd-project-researcher', projectDir),
    getModelAlias('gsd-research-synthesizer', projectDir),
    getModelAlias('gsd-roadmapper', projectDir),
  ]);

  const result: Record<string, unknown> = {
    researcher_model: researcherModel,
    synthesizer_model: synthesizerModel,
    roadmapper_model: roadmapperModel,
    commit_docs: config.commit_docs,
    research_enabled: config.workflow.research,
    current_milestone: milestone.version,
    current_milestone_name: milestone.name,
    latest_completed_milestone: latestCompleted?.version || null,
    latest_completed_milestone_name: latestCompleted?.name || null,
    phase_dir_count: phaseDirCount,
    phase_archive_path: latestCompleted
      ? toPosixPath(relative(projectDir, join(projectDir, '.planning', 'milestones', `${latestCompleted.version}-phases`)))
      : null,
    project_exists: pathExists(projectDir, '.planning/PROJECT.md'),
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    state_exists: existsSync(join(planningDir, 'STATE.md')),
    project_path: '.planning/PROJECT.md',
    roadmap_path: toPosixPath(relative(projectDir, join(planningDir, 'ROADMAP.md'))),
    state_path: toPosixPath(relative(projectDir, join(planningDir, 'STATE.md'))),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initQuick ────────────────────────────────────────────────────────────

/**
 * Init handler for quick workflow.
 * Port of cmdInitQuick from init.cjs lines 448-504.
 */
export const initQuick: QueryHandler = async (args, projectDir) => {
  const description = args[0] || null;
  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');
  const now = new Date();
  const slug = description ? generateSlugInternal(description).substring(0, 40) : null;

  // Generate collision-resistant quick task ID: YYMMDD-xxx
  const yy = String(now.getFullYear()).slice(-2);
  const mm = String(now.getMonth() + 1).padStart(2, '0');
  const dd = String(now.getDate()).padStart(2, '0');
  const dateStr = yy + mm + dd;
  const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
  const timeBlocks = Math.floor(secondsSinceMidnight / 2);
  const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
  const quickId = dateStr + '-' + timeEncoded;
  const branchSlug = slug || 'quick';
  const quickBranchName = config.git.quick_branch_template
    ? config.git.quick_branch_template
        .replace('{num}', quickId)
        .replace('{quick}', quickId)
        .replace('{slug}', branchSlug)
    : null;

  const [plannerModel, executorModel, checkerModel, verifierModel] = await Promise.all([
    getModelAlias('gsd-planner', projectDir),
    getModelAlias('gsd-executor', projectDir),
    getModelAlias('gsd-plan-checker', projectDir),
    getModelAlias('gsd-verifier', projectDir),
  ]);

  const result: Record<string, unknown> = {
    planner_model: plannerModel,
    executor_model: executorModel,
    checker_model: checkerModel,
    verifier_model: verifierModel,
    commit_docs: config.commit_docs,
    branch_name: quickBranchName,
    quick_id: quickId,
    slug,
    description,
    date: now.toISOString().split('T')[0],
    timestamp: now.toISOString(),
    quick_dir: '.planning/quick',
    task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    planning_exists: existsSync(join(projectDir, '.planning')),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initResume ───────────────────────────────────────────────────────────

/**
 * Init handler for resume-project workflow.
 * Port of cmdInitResume from init.cjs lines 506-536.
 */
export const initResume: QueryHandler = async (_args, projectDir) => {
  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');

  let interruptedAgentId: string | null = null;
  try {
    interruptedAgentId = readFileSync(join(projectDir, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
  } catch { /* intentionally empty */ }

  const result: Record<string, unknown> = {
    state_exists: existsSync(join(planningDir, 'STATE.md')),
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    project_exists: pathExists(projectDir, '.planning/PROJECT.md'),
    planning_exists: existsSync(join(projectDir, '.planning')),
    state_path: toPosixPath(relative(projectDir, join(planningDir, 'STATE.md'))),
    roadmap_path: toPosixPath(relative(projectDir, join(planningDir, 'ROADMAP.md'))),
    project_path: '.planning/PROJECT.md',
    has_interrupted_agent: !!interruptedAgentId,
    interrupted_agent_id: interruptedAgentId,
    commit_docs: config.commit_docs,
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initVerifyWork ───────────────────────────────────────────────────────

/**
 * Init handler for verify-work workflow.
 * Port of cmdInitVerifyWork from init.cjs lines 538-586.
 */
export const initVerifyWork: QueryHandler = async (args, projectDir) => {
  const phase = args[0];
  if (!phase) {
    return { data: { error: 'phase required for init verify-work' } };
  }

  const config = await loadConfig(projectDir);
  const { phaseInfo } = await getPhaseInfoWithFallback(phase, projectDir);

  const [plannerModel, checkerModel] = await Promise.all([
    getModelAlias('gsd-planner', projectDir),
    getModelAlias('gsd-plan-checker', projectDir),
  ]);

  const result: Record<string, unknown> = {
    planner_model: plannerModel,
    checker_model: checkerModel,
    commit_docs: config.commit_docs,
    phase_found: !!(phaseInfo && phaseInfo.found),
    phase_dir: (phaseInfo?.directory as string) ?? null,
    phase_number: (phaseInfo?.phase_number as string) ?? null,
    phase_name: (phaseInfo?.phase_name as string) ?? null,
    has_verification: (phaseInfo?.has_verification as boolean) || false,
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initPhaseOp ──────────────────────────────────────────────────────────

/**
 * Init handler for discuss-phase and similar phase operations.
 * Port of cmdInitPhaseOp from init.cjs lines 588-697.
 */
export const initPhaseOp: QueryHandler = async (args, projectDir) => {
  const phase = args[0];
  if (!phase) {
    return { data: { error: 'phase required for init phase-op' } };
  }

  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');

  // findPhase with archived override: if only match is archived, prefer ROADMAP
  const phaseResult = await findPhase([phase], projectDir);
  let phaseInfo = phaseResult.data as Record<string, unknown> | null;

  const roadmapResult = await roadmapGetPhase([phase], projectDir);
  const roadmapPhase = roadmapResult.data as Record<string, unknown> | null;

  // If the only match comes from an archived milestone, prefer current ROADMAP
  if (phaseInfo?.archived && roadmapPhase?.found) {
    const phaseName = roadmapPhase.phase_name as string;
    phaseInfo = {
      found: true,
      directory: null,
      phase_number: roadmapPhase.phase_number,
      phase_name: phaseName,
      phase_slug: phaseName ? generateSlugInternal(phaseName) : null,
      plans: [],
      summaries: [],
      incomplete_plans: [],
      has_research: false,
      has_context: false,
      has_verification: false,
    };
  }

  // Fallback to ROADMAP.md if no directory exists
  if (!phaseInfo || !phaseInfo.found) {
    if (roadmapPhase?.found) {
      const phaseName = roadmapPhase.phase_name as string;
      phaseInfo = {
        found: true,
        directory: null,
        phase_number: roadmapPhase.phase_number,
        phase_name: phaseName,
        phase_slug: phaseName ? generateSlugInternal(phaseName) : null,
        plans: [],
        summaries: [],
        incomplete_plans: [],
        has_research: false,
        has_context: false,
        has_verification: false,
      };
    }
  }

  const phaseFound = !!(phaseInfo && phaseInfo.found);
  const phaseNumber = (phaseInfo?.phase_number as string) || null;
  const plans = (phaseInfo?.plans || []) as string[];

  const result: Record<string, unknown> = {
    commit_docs: config.commit_docs,
    brave_search: config.brave_search,
    firecrawl: config.firecrawl,
    exa_search: config.exa_search,
    phase_found: phaseFound,
    phase_dir: (phaseInfo?.directory as string) ?? null,
    phase_number: phaseNumber,
    phase_name: (phaseInfo?.phase_name as string) ?? null,
    phase_slug: (phaseInfo?.phase_slug as string) ?? null,
    padded_phase: phaseNumber ? normalizePhaseName(phaseNumber) : null,
    has_research: (phaseInfo?.has_research as boolean) || false,
    has_context: (phaseInfo?.has_context as boolean) || false,
    has_plans: plans.length > 0,
    has_verification: (phaseInfo?.has_verification as boolean) || false,
    has_reviews: (phaseInfo?.has_reviews as boolean) || false,
    plan_count: plans.length,
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    planning_exists: existsSync(planningDir),
    state_path: toPosixPath(relative(projectDir, join(planningDir, 'STATE.md'))),
    roadmap_path: toPosixPath(relative(projectDir, join(planningDir, 'ROADMAP.md'))),
    requirements_path: toPosixPath(relative(projectDir, join(planningDir, 'REQUIREMENTS.md'))),
  };

  // Add artifact paths if phase directory exists
  if (phaseInfo?.directory) {
    const phaseDirFull = join(projectDir, phaseInfo.directory as string);
    try {
      const files = readdirSync(phaseDirFull);
      const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
      if (contextFile) result.context_path = toPosixPath(join(phaseInfo.directory as string, contextFile));
      const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
      if (researchFile) result.research_path = toPosixPath(join(phaseInfo.directory as string, researchFile));
      const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
      if (verificationFile) result.verification_path = toPosixPath(join(phaseInfo.directory as string, verificationFile));
      const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
      if (uatFile) result.uat_path = toPosixPath(join(phaseInfo.directory as string, uatFile));
      const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md');
      if (reviewsFile) result.reviews_path = toPosixPath(join(phaseInfo.directory as string, reviewsFile));
    } catch { /* intentionally empty */ }
  }

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initTodos ────────────────────────────────────────────────────────────

/**
 * Init handler for check-todos and add-todo workflows.
 * Port of cmdInitTodos from init.cjs lines 699-756.
 */
export const initTodos: QueryHandler = async (args, projectDir) => {
  const area = args[0] || null;
  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');
  const now = new Date();

  const pendingDir = join(planningDir, 'todos', 'pending');
  let count = 0;
  const todos: Array<Record<string, unknown>> = [];

  try {
    const files = readdirSync(pendingDir).filter(f => f.endsWith('.md'));
    for (const file of files) {
      try {
        const content = readFileSync(join(pendingDir, file), 'utf-8');
        const createdMatch = content.match(/^created:\s*(.+)$/m);
        const titleMatch = content.match(/^title:\s*(.+)$/m);
        const areaMatch = content.match(/^area:\s*(.+)$/m);
        const todoArea = areaMatch ? areaMatch[1].trim() : 'general';

        if (area && todoArea !== area) continue;

        count++;
        todos.push({
          file,
          created: createdMatch ? createdMatch[1].trim() : 'unknown',
          title: titleMatch ? titleMatch[1].trim() : 'Untitled',
          area: todoArea,
          path: toPosixPath(relative(projectDir, join(pendingDir, file))),
        });
      } catch { /* intentionally empty */ }
    }
  } catch { /* intentionally empty */ }

  const result: Record<string, unknown> = {
    commit_docs: config.commit_docs,
    date: now.toISOString().split('T')[0],
    timestamp: now.toISOString(),
    todo_count: count,
    todos,
    area_filter: area,
    pending_dir: toPosixPath(relative(projectDir, join(planningDir, 'todos', 'pending'))),
    completed_dir: toPosixPath(relative(projectDir, join(planningDir, 'todos', 'completed'))),
    planning_exists: existsSync(planningDir),
    todos_dir_exists: existsSync(join(planningDir, 'todos')),
    pending_dir_exists: existsSync(pendingDir),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initMilestoneOp ─────────────────────────────────────────────────────

/**
 * Init handler for complete-milestone and audit-milestone workflows.
 * Port of cmdInitMilestoneOp from init.cjs lines 758-817.
 */
export const initMilestoneOp: QueryHandler = async (_args, projectDir) => {
  const config = await loadConfig(projectDir);
  const planningDir = join(projectDir, '.planning');
  const milestone = await getMilestoneInfo(projectDir);

  const phasesDir = join(planningDir, 'phases');
  let phaseCount = 0;
  let completedPhases = 0;

  try {
    const entries = readdirSync(phasesDir, { withFileTypes: true });
    const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
    phaseCount = dirs.length;

    for (const dir of dirs) {
      try {
        const phaseFiles = readdirSync(join(phasesDir, dir));
        const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
        if (hasSummary) completedPhases++;
      } catch { /* intentionally empty */ }
    }
  } catch { /* intentionally empty */ }

  const archiveDir = join(projectDir, '.planning', 'archive');
  let archivedMilestones: string[] = [];
  try {
    archivedMilestones = readdirSync(archiveDir, { withFileTypes: true })
      .filter(e => e.isDirectory())
      .map(e => e.name);
  } catch { /* intentionally empty */ }

  const result: Record<string, unknown> = {
    commit_docs: config.commit_docs,
    milestone_version: milestone.version,
    milestone_name: milestone.name,
    milestone_slug: generateSlugInternal(milestone.name),
    phase_count: phaseCount,
    completed_phases: completedPhases,
    all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
    archived_milestones: archivedMilestones,
    archive_count: archivedMilestones.length,
    project_exists: pathExists(projectDir, '.planning/PROJECT.md'),
    roadmap_exists: existsSync(join(planningDir, 'ROADMAP.md')),
    state_exists: existsSync(join(planningDir, 'STATE.md')),
    archive_exists: existsSync(archiveDir),
    phases_dir_exists: existsSync(phasesDir),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initMapCodebase ──────────────────────────────────────────────────────

/**
 * Init handler for map-codebase workflow.
 * Port of cmdInitMapCodebase from init.cjs lines 819-852.
 */
export const initMapCodebase: QueryHandler = async (_args, projectDir) => {
  const config = await loadConfig(projectDir);
  const now = new Date();
  const codebaseDir = join(projectDir, '.planning', 'codebase');
  let existingMaps: string[] = [];
  try {
    existingMaps = readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
  } catch { /* intentionally empty */ }

  const mapperModel = await getModelAlias('gsd-codebase-mapper', projectDir);

  const result: Record<string, unknown> = {
    mapper_model: mapperModel,
    commit_docs: config.commit_docs,
    search_gitignored: config.search_gitignored,
    parallelization: config.parallelization,
    subagent_timeout: (config as Record<string, unknown>).subagent_timeout ?? undefined,
    date: now.toISOString().split('T')[0],
    timestamp: now.toISOString(),
    codebase_dir: '.planning/codebase',
    existing_maps: existingMaps,
    has_maps: existingMaps.length > 0,
    planning_exists: pathExists(projectDir, '.planning'),
    codebase_dir_exists: pathExists(projectDir, '.planning/codebase'),
  };

  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── initNewWorkspace ─────────────────────────────────────────────────────

/**
 * Init handler for new-workspace workflow.
 * Port of cmdInitNewWorkspace from init.cjs lines 1311-1335.
 * T-14-01: Validates workspace name rejects path separators.
 */
export const initNewWorkspace: QueryHandler = async (_args, projectDir) => {
  const home = process.env.HOME || homedir();
  const defaultBase = join(home, 'gsd-workspaces');

  // Detect child git repos (one level deep)
  const childRepos: Array<{ name: string; path: string; has_uncommitted: boolean }> = [];
  try {
    const entries = readdirSync(projectDir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
      const fullPath = join(projectDir, entry.name);
      if (existsSync(join(fullPath, '.git'))) {
        let hasUncommitted = false;
        try {
          const status = execSync('git status --porcelain', { cwd: fullPath, encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
          hasUncommitted = status.trim().length > 0;
        } catch { /* best-effort */ }
        childRepos.push({ name: entry.name, path: fullPath, has_uncommitted: hasUncommitted });
      }
    }
  } catch { /* intentionally empty */ }

  let worktreeAvailable = false;
  try {
    execSync('git --version', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
    worktreeAvailable = true;
  } catch { /* no git */ }

  const result: Record<string, unknown> = {
    default_workspace_base: defaultBase,
    child_repos: childRepos,
    child_repo_count: childRepos.length,
    worktree_available: worktreeAvailable,
    is_git_repo: pathExists(projectDir, '.git'),
    cwd_repo_name: basename(projectDir),
  };

  return { data: withProjectRoot(projectDir, result) };
};

// ─── initListWorkspaces ───────────────────────────────────────────────────

/**
 * Init handler for list-workspaces workflow.
 * Port of cmdInitListWorkspaces from init.cjs lines 1337-1381.
 */
export const initListWorkspaces: QueryHandler = async (_args, _projectDir) => {
  const home = process.env.HOME || homedir();
  const defaultBase = join(home, 'gsd-workspaces');

  const workspaces: Array<Record<string, unknown>> = [];
  if (existsSync(defaultBase)) {
    let entries: Dirent[] = [];
    try {
      entries = readdirSync(defaultBase, { withFileTypes: true });
    } catch { entries = []; }
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const wsPath = join(defaultBase, String(entry.name));
      const manifestPath = join(wsPath, 'WORKSPACE.md');
      if (!existsSync(manifestPath)) continue;

      let repoCount = 0;
      let strategy = 'unknown';
      try {
        const manifest = readFileSync(manifestPath, 'utf8');
        const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
        if (strategyMatch) strategy = strategyMatch[1].trim();
        const tableRows = manifest.split('\n').filter(l => l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---'));
        repoCount = tableRows.length;
      } catch { /* best-effort */ }
      const hasProject = existsSync(join(wsPath, '.planning', 'PROJECT.md'));

      workspaces.push({
        name: entry.name,
        path: wsPath,
        repo_count: repoCount,
        strategy,
        has_project: hasProject,
      });
    }
  }

  const result: Record<string, unknown> = {
    workspace_base: defaultBase,
    workspaces,
    workspace_count: workspaces.length,
  };

  return { data: result };
};

// ─── initRemoveWorkspace ──────────────────────────────────────────────────

/**
 * Init handler for remove-workspace workflow.
 * Port of cmdInitRemoveWorkspace from init.cjs lines 1383-1443.
 * T-14-01: Validates workspace name rejects path separators and '..' sequences.
 */
export const initRemoveWorkspace: QueryHandler = async (args, _projectDir) => {
  const name = args[0];
  if (!name) {
    return { data: { error: 'workspace name required for init remove-workspace' } };
  }

  // T-14-01: Reject path traversal attempts
  if (name.includes('/') || name.includes('\\') || name.includes('..')) {
    return { data: { error: `Invalid workspace name: ${name} (path separators not allowed)` } };
  }

  const home = process.env.HOME || homedir();
  const defaultBase = join(home, 'gsd-workspaces');
  const wsPath = join(defaultBase, name);
  const manifestPath = join(wsPath, 'WORKSPACE.md');

  if (!existsSync(wsPath)) {
    return { data: { error: `Workspace not found: ${wsPath}` } };
  }

  const repos: Array<Record<string, unknown>> = [];
  let strategy = 'unknown';
  if (existsSync(manifestPath)) {
    try {
      const manifest = readFileSync(manifestPath, 'utf8');
      const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
      if (strategyMatch) strategy = strategyMatch[1].trim();

      const lines = manifest.split('\n');
      for (const line of lines) {
        const match = line.match(/^\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|$/);
        if (match && match[1] !== 'Repo' && !match[1].includes('---')) {
          repos.push({ name: match[1], source: match[2], branch: match[3], strategy: match[4] });
        }
      }
    } catch { /* best-effort */ }
  }

  // Check for uncommitted changes in workspace repos
  const dirtyRepos: string[] = [];
  for (const repo of repos) {
    const repoPath = join(wsPath, repo.name as string);
    if (!existsSync(repoPath)) continue;
    try {
      const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
      if (status.trim().length > 0) {
        dirtyRepos.push(repo.name as string);
      }
    } catch { /* best-effort */ }
  }

  const result: Record<string, unknown> = {
    workspace_name: name,
    workspace_path: wsPath,
    has_manifest: existsSync(manifestPath),
    strategy,
    repos,
    repo_count: repos.length,
    dirty_repos: dirtyRepos,
    has_dirty_repos: dirtyRepos.length > 0,
  };

  return { data: result };
};

// ─── initIngestDocs ───────────────────────────────────────────────────────

/**
 * Init handler for ingest-docs workflow.
 * Mirrors `initResume` shape but without current-agent-id lookup — the
 * ingest-docs workflow reads `project_exists`, `planning_exists`, `has_git`,
 * and `project_path` to branch between new-project vs merge-milestone modes.
 */
export const initIngestDocs: QueryHandler = async (_args, projectDir) => {
  const config = await loadConfig(projectDir);
  const result: Record<string, unknown> = {
    project_exists: pathExists(projectDir, '.planning/PROJECT.md'),
    planning_exists: pathExists(projectDir, '.planning'),
    has_git: pathExists(projectDir, '.git'),
    project_path: '.planning/PROJECT.md',
    commit_docs: config.commit_docs,
  };
  return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
};

// ─── docsInit ────────────────────────────────────────────────────────────

export const docsInit: QueryHandler = async (_args, projectDir) => {
  return {
    data: {
      project_exists: existsSync(join(projectDir, '.planning', 'PROJECT.md')),
      roadmap_exists: existsSync(join(projectDir, '.planning', 'ROADMAP.md')),
      docs_dir: '.planning/docs',
      project_root: projectDir,
    },
  };
};
