import fs from "node:fs";
import path from "node:path";

/**
 * Walk the `docs/work/epics/` tree under `workRoot` and return a structured
 * state object. Each epic folder under `epics/` must contain `_epic.md`;
 * each story subfolder must contain `_story.md`.
 *
 * Returns:
 * {
 *   updated_at: ISO string,
 *   epics: {
 *     <epic-id>: {
 *       status: "todo" | "in-progress" | "done",
 *       title: string,
 *       stories: {
 *         <story-id>: {
 *           status: "todo" | "in-progress" | "done",
 *           title: string,
 *           ac_total: number,    // total checkboxes in Tasks section
 *           ac_completed: number, // - [x] checkboxes
 *         }
 *       }
 *     }
 *   }
 * }
 */
export function buildState(workRoot) {
  const state = {
    updated_at: new Date().toISOString(),
    epics: {},
  };

  const epicsRoot = path.join(workRoot, "epics");
  if (!fs.existsSync(epicsRoot)) return state;

  for (const entry of fs.readdirSync(epicsRoot)) {
    const epicDir = path.join(epicsRoot, entry);
    if (!fs.statSync(epicDir).isDirectory()) continue;
    const epicFile = path.join(epicDir, "_epic.md");
    if (!fs.existsSync(epicFile)) continue;

    const epicMeta = parseFrontmatter(epicFile);
    const epicEntry = {
      status: epicMeta.status ?? "todo",
      title: epicMeta.title ?? entry,
      prd: epicMeta.prd && epicMeta.prd !== "null" ? epicMeta.prd : null,
      stories: {},
    };

    for (const sub of fs.readdirSync(epicDir)) {
      const subPath = path.join(epicDir, sub);
      if (!fs.statSync(subPath).isDirectory()) continue;
      const storyFile = path.join(subPath, "_story.md");
      if (!fs.existsSync(storyFile)) continue;
      const storyMeta = parseFrontmatter(storyFile);
      const storyContent = fs.readFileSync(storyFile, "utf8");
      const { total, completed } = countTaskCheckboxes(storyContent);
      epicEntry.stories[storyMeta.id ?? sub] = {
        status: storyMeta.status ?? "todo",
        title: storyMeta.title ?? sub,
        ac_total: total,
        ac_completed: completed,
        depends_on: Array.isArray(storyMeta["depends-on"])
          ? storyMeta["depends-on"]
          : [],
        blocks: Array.isArray(storyMeta.blocks) ? storyMeta.blocks : [],
      };
    }

    state.epics[epicMeta.id ?? entry] = epicEntry;
  }

  const { ready, blocked } = computeReadyBlocked(state);
  state.ready = ready;
  state.blocked = blocked;
  state.needs_prd_ship = computeNeedsPrdShip(state, workRoot);

  return state;
}

/**
 * Find epics whose status is "done" + have a non-null `prd:` link + the
 * linked PRD's status is NOT yet "shipped". Surfacing these in state lets
 * the reviewer (or a post-merge hook) trigger `pnpm work prd-ship <id>`.
 */
export function computeNeedsPrdShip(state, workRoot) {
  const out = [];
  const prdsDir = path.join(workRoot, "prds");
  if (!fs.existsSync(prdsDir)) return out;

  // Index PRDs by id -> { path, status }
  const prdIndex = new Map();
  for (const file of fs.readdirSync(prdsDir)) {
    if (!file.endsWith(".prd.md")) continue;
    const prdPath = path.join(prdsDir, file);
    const meta = parseFrontmatter(prdPath);
    if (meta.id) prdIndex.set(meta.id, { path: prdPath, status: meta.status });
  }

  for (const [epicId, epic] of Object.entries(state.epics)) {
    if (epic.status !== "done") continue;
    if (!epic.prd) continue;
    const prd = prdIndex.get(epic.prd);
    if (!prd) continue; // PRD link points to a missing file — orchestrator concern, not ours
    if (prd.status === "shipped") continue; // already done
    out.push({
      epic: epicId,
      prd: epic.prd,
      prd_status: prd.status ?? "unknown",
      action: `pnpm work prd-ship ${epic.prd} --auto-commits`,
    });
  }
  return out;
}

/**
 * Given a built state object, compute the ready + blocked story sets.
 *
 * "ready" = not done AND all depends_on stories are done
 * "blocked" = not done AND at least one depends_on story is not done
 *
 * depends_on references are either same-epic (just story id) or cross-epic
 * (`<epic>/<story>`).
 *
 * Returns { ready: [...], blocked: [...] } where each entry is:
 *   { epic, story, title, [waiting_on?] }
 */
export function computeReadyBlocked(state) {
  // Build a flat map: "<epic>/<story>" => { status, title, depends_on }
  const flat = new Map();
  for (const [epic, epicEntry] of Object.entries(state.epics)) {
    for (const [story, storyEntry] of Object.entries(epicEntry.stories)) {
      flat.set(`${epic}/${story}`, {
        epic,
        story,
        status: storyEntry.status,
        title: storyEntry.title,
        depends_on: storyEntry.depends_on,
      });
    }
  }

  // Resolve a depends_on reference (which may be `<epic>/<story>` or just `<story>`)
  // relative to the current epic.
  function resolveRef(ref, currentEpic) {
    if (ref.includes("/")) return ref;
    return `${currentEpic}/${ref}`;
  }

  const ready = [];
  const blocked = [];
  for (const entry of flat.values()) {
    if (entry.status === "done") continue;
    const refs = entry.depends_on.map((r) => resolveRef(r, entry.epic));
    const waitingOn = refs.filter((r) => {
      const dep = flat.get(r);
      return !dep || dep.status !== "done";
    });
    if (waitingOn.length === 0) {
      ready.push({ epic: entry.epic, story: entry.story, title: entry.title });
    } else {
      blocked.push({
        epic: entry.epic,
        story: entry.story,
        title: entry.title,
        waiting_on: waitingOn,
      });
    }
  }
  return { ready, blocked };
}

/**
 * Read a markdown file's YAML frontmatter (between leading `---` delimiters)
 * and return a flat object of string or array values. Numeric / bool values are
 * left as strings; array values like `[a, b, "c"]` are parsed into JS arrays.
 */
export function parseFrontmatter(filePath) {
  const src = fs.readFileSync(filePath, "utf8");
  const match = src.match(/^---\n([\s\S]+?)\n---/);
  if (!match) return {};
  const out = {};
  for (const line of match[1].split("\n")) {
    const m = line.match(/^([\w-]+):\s*(.*)$/);
    if (!m) continue;
    let value = m[2].trim();
    // Array values like [a, b, "c"] or []
    if (value.startsWith("[") && value.endsWith("]")) {
      const inner = value.slice(1, -1).trim();
      if (inner === "") {
        out[m[1]] = [];
      } else {
        out[m[1]] = inner.split(",").map((s) => {
          let v = s.trim();
          if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
          if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
          return v;
        });
      }
      continue;
    }
    // Strings (with optional quotes)
    if (value.startsWith('"') && value.endsWith('"'))
      value = value.slice(1, -1);
    if (value.startsWith("'") && value.endsWith("'"))
      value = value.slice(1, -1);
    out[m[1]] = value;
  }
  return out;
}

/**
 * Count `- [x]` and `- [ ]` checkboxes inside the file's `## Tasks` section.
 * Returns { total, completed }.
 */
export function countTaskCheckboxes(content) {
  const lines = content.split("\n");
  let inTasks = false;
  let total = 0;
  let completed = 0;
  for (const line of lines) {
    if (line.startsWith("## ")) {
      inTasks = /^##\s+Tasks\b/i.test(line);
      continue;
    }
    if (!inTasks) continue;
    const m = line.match(/^[\s>-]*\[(.)\]/);
    if (m) {
      total++;
      if (m[1] === "x" || m[1] === "X") completed++;
    }
  }
  return { total, completed };
}
