/**
 * Plan progress parser — reads phase-*-tasks.md trackers to derive
 * phase/task state for the inbox progress bar.
 *
 * @module plan-progress
 */

import { readdirSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';

const WORKING_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes

/**
 * Parse simple key:value pairs from a YAML frontmatter block (---…---).
 * Handles single-line scalar values only — enough for feature/phase keys.
 *
 * @param {string} content
 * @returns {Record<string, string>}
 */
function parseFrontmatter(content) {
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
  if (!match) return {};
  const result = {};
  for (const line of match[1].split('\n')) {
    const m = line.match(/^([\w-]+):\s*(.+)$/);
    if (m) result[m[1]] = m[2].trim();
  }
  return result;
}

/**
 * Extract task definitions from "## Task list" section.
 * Matches headings like:
 *   ### A0 — name
 *   ### B1 step 5 — multi-part name
 *
 * @param {string} content
 * @returns {Array<{id: string, name: string}>}
 */
function extractTaskDefs(content) {
  const sectionMatch = content.match(/^## Task list\s*\n([\s\S]*)/m);
  if (!sectionMatch) return [];

  const taskSection = sectionMatch[1];
  const tasks = [];
  const re = /^###\s+([A-Z]\d+)\b([^\n]*)/gm;
  let m;
  while ((m = re.exec(taskSection)) !== null) {
    const id = m[1];
    const rest = m[2].trim();
    // Strip leading em-dash, double-hyphen, or plain hyphen separator
    const name = rest.replace(/^\s*[—\-]{1,2}\s*/, '').trim() || id;
    tasks.push({ id, name });
  }
  return tasks;
}

/**
 * Extract completed task IDs from the CP0 log comment block.
 * Matches lines like: A0 (2026-05-05): ...
 *                      A2 (2026-05-05, rebase): ...
 *
 * @param {string} content
 * @returns {Set<string>}
 */
function extractCp0Completed(content) {
  const completed = new Set();
  const logMatch = content.match(/<!--\s*CP0 log[\s\S]*?-->/);
  if (!logMatch) return completed;

  const re = /^([A-Z]\d+)\s*\(/gm;
  let m;
  while ((m = re.exec(logMatch[0])) !== null) {
    completed.add(m[1]);
  }
  return completed;
}

/**
 * Extract completed task IDs from an item-format Status table.
 * Matches rows like: | A1 | Tool loader index | done |
 *
 * @param {string} content
 * @returns {Set<string>}
 */
function extractItemTableCompleted(content) {
  const completed = new Set();
  // No `m` flag — `$` must mean end-of-string so the lazy quantifier captures
  // the whole table, not just the first line.
  const statusMatch = content.match(/## Status\s*\n([\s\S]*?)(?=\n##\s|$)/);
  if (!statusMatch) return completed;

  const re = /^\|\s*([A-Z]\d+)\s*\|[^|]+\|\s*done\s*\|/gim;
  let m;
  while ((m = re.exec(statusMatch[1])) !== null) {
    completed.add(m[1]);
  }
  return completed;
}

/**
 * Extract the authoritative done count from a count-format Status table.
 * Matches rows like: | done | 3 |
 *
 * @param {string} content
 * @returns {number|null}
 */
function extractDoneCount(content) {
  const m = content.match(/\|\s*done\s*\|\s*(\d+)\s*\|/i);
  return m ? parseInt(m[1], 10) : null;
}

/**
 * Resolve the feature slug from a branch name or by scanning docs/plans/.
 *
 * Strategy:
 *   1. Strip "feat/" prefix + optional "-phase-X" suffix from branch.
 *   2. Exact match against plans subdirectory names.
 *   3. Prefix match (branch slug starts with a plan dir name).
 *   4. Fallback: most-recently-modified plans dir that has phase trackers.
 *
 * @param {string} repoPath - path to the git checkout
 * @param {string|null} branch
 * @returns {string|null}
 */
function resolveFeatureSlug(repoPath, branch) {
  const plansDir = path.join(repoPath, 'docs', 'plans');
  let entries;
  try {
    entries = readdirSync(plansDir, { withFileTypes: true })
      .filter((d) => d.isDirectory())
      .map((d) => d.name);
  } catch {
    return null;
  }

  if (branch) {
    // Strip feat/ prefix, any nested path, and trailing -phase-X suffix
    const slug = branch
      .replace(/^feat\//, '')
      .replace(/\/.*$/, '')
      .replace(/-phase-[a-z]$/, '');

    // Exact match
    if (entries.includes(slug)) return slug;

    // Prefix match (slug starts with a plan dir name)
    const prefixMatch = entries.find((d) => slug.startsWith(d));
    if (prefixMatch) return prefixMatch;

    // Branch provided but no name match — don't guess
    return null;
  }

  // No branch: fallback to most-recently-modified dir with phase tracker files
  let newest = null;
  let newestMtime = 0;
  for (const dir of entries) {
    const dirPath = path.join(plansDir, dir);
    try {
      const files = readdirSync(dirPath);
      if (!files.some((f) => /^phase-[a-z]-tasks\.md$/.test(f))) continue;
      const mtime = statSync(dirPath).mtimeMs;
      if (mtime > newestMtime) {
        newestMtime = mtime;
        newest = dir;
      }
    } catch {
      // skip unreadable entries
    }
  }

  return newest;
}

/**
 * Parse a single phase tracker file into phase/task state.
 *
 * @param {string} filePath
 * @param {boolean} isRecentlyActive - whether the world had recent activity
 * @param {{ workingMarked: boolean }} state - mutable flag shared across phases
 * @returns {{ id: string, name: string, status: string, tasks: Array }|null}
 */
function parseTrackerFile(filePath, isRecentlyActive, state) {
  let content;
  try {
    content = readFileSync(filePath, 'utf8');
  } catch {
    return null;
  }

  const fm = parseFrontmatter(content);

  // Phase ID: frontmatter "phase" field or filename "phase-X-tasks.md"
  const phaseId =
    fm.phase ||
    path.basename(filePath).match(/^phase-([a-z])-tasks\.md$/)?.[1] ||
    '?';

  const phaseName = `Phase ${phaseId.toUpperCase()}`;

  const taskDefs = extractTaskDefs(content);
  if (taskDefs.length === 0) return null;

  // Collect completions from all sources
  const cp0Completed = extractCp0Completed(content);
  const itemTableCompleted = extractItemTableCompleted(content);
  const doneCount = extractDoneCount(content);

  // Merge CP0 log + item-table; count-format overrides if present
  const mergedCompleted = new Set([...cp0Completed, ...itemTableCompleted]);

  const tasks = taskDefs.map((t, i) => {
    const isComplete =
      doneCount !== null
        ? i < doneCount          // count format is authoritative
        : mergedCompleted.has(t.id);

    if (isComplete) return { id: t.id, name: t.name, status: 'complete' };

    // First pending task across all phases = candidate for "working"
    if (!state.workingMarked) {
      state.workingMarked = true;
      return {
        id: t.id,
        name: t.name,
        status: isRecentlyActive ? 'working' : 'pending',
      };
    }

    return { id: t.id, name: t.name, status: 'pending' };
  });

  const allComplete = tasks.every((t) => t.status === 'complete');
  const anyWorking = tasks.some((t) => t.status === 'working');
  const phaseStatus = allComplete ? 'complete' : anyWorking ? 'working' : 'pending';

  return { id: phaseId, name: phaseName, status: phaseStatus, tasks };
}

/**
 * Read plan progress from a world's git checkout.
 *
 * @param {string} repoPath - absolute path to the git checkout
 * @param {string|null} branch - current branch name (e.g. "feat/foo-phase-a")
 * @param {{ lastActivityAtMs?: number|null }} [opts]
 * @returns {{ feature: string, phases: Array }|null}
 *   null when no plan tracker is found (caller falls back to legacy bar)
 */
export function readPlanProgress(repoPath, branch, { lastActivityAtMs = null } = {}) {
  const feature = resolveFeatureSlug(repoPath, branch);
  if (!feature) return null;

  const plansDir = path.join(repoPath, 'docs', 'plans', feature);
  let phaseFiles;
  try {
    phaseFiles = readdirSync(plansDir)
      .filter((f) => /^phase-[a-z]-tasks\.md$/.test(f))
      .sort();
  } catch {
    return null;
  }

  if (phaseFiles.length === 0) return null;

  const isRecentlyActive =
    lastActivityAtMs != null
      ? Date.now() - lastActivityAtMs <= WORKING_THRESHOLD_MS
      : false;

  const state = { workingMarked: false };

  const phases = phaseFiles
    .map((file) =>
      parseTrackerFile(path.join(plansDir, file), isRecentlyActive, state),
    )
    .filter(Boolean);

  if (phases.length === 0) return null;

  return { feature, phases };
}
