/**
 * Lightweight markdown walker tuned to the HbM book layout.
 *
 * Supports two formats:
 *
 * 1. Legacy Google-Docs export (plain text, `_books/`):
 *      ## Chapter heading
 *      Section heading (plain text / bold)
 *      Item Name
 *      * Field: value
 *
 * 2. Native Obsidian/repo files (`rules/`, `disciplines/`, etc.):
 *      ---                            ← YAML frontmatter (skipped)
 *      ...
 *      ---
 *      ## Rozdział I - ...            ← chapter (any heading level)
 *      ### Sub-section                ← section / sub-section
 *      #### Spell Name                ← item name
 *      - [[#Anchor|Label]]            ← Obsidian TOC bullet (skipped)
 *      * Field: value
 *
 * The walker emits one `BookBlock` per item it can find. Section context
 * is carried through so parsers know e.g. which discipline a spell lives
 * under.
 */

import { readFileSync } from 'node:fs';

export interface BookBlock {
  /** The line directly preceding the bullet block — usually the item name. */
  name: string;
  /** Bullet keys/values, preserved in order. */
  fields: Array<{ key: string; value: string }>;
  /** Free-form paragraph text following the bullet block (description). */
  description: string;
  /** Most recent chapter (any `#+ ...`) heading. */
  chapter: string;
  /** Most recent non-bullet, non-empty line above the item name (sub-section). */
  section: string;
  /** 1-based line number of the item name in the source file. */
  line: number;
}

const SEPARATOR = /^_{3,}$/;
const BULLET = /^\*\s+([^:]+):\s*(.+)$/;
// Match any markdown heading level (1-6 `#` chars).
const HEADING = /^#{1,6}\s+(.+)$/;
// Chapter headings are the two highest heading levels we encounter — heuristic:
// treat ## headings as "chapter" and ### headings as "section".
const CHAPTER_HEADING = /^#{1,2}\s+(.+)$/;
const SECTION_HEADING = /^#{3,4}\s+(.+)$/;
// Obsidian TOC bullets: `- [[#Anchor|Label]]` or `* [[#Anchor|Label]]`
const OBSIDIAN_TOC_BULLET = /^[-*]\s+\[\[#/;
// Frontmatter fence
const FRONTMATTER_FENCE = /^---\s*$/;

/** Strip leading markdown heading markers and bold `**` from a raw line. */
function stripHeadingMarkers(line: string): string {
  return line.replace(/^#{1,6}\s+/, '').replace(/^\*\*(.+)\*\*$/, '$1').trim();
}

/** Skip YAML frontmatter block at start of file. Returns the index of the first non-frontmatter line. */
function skipFrontmatter(lines: string[]): number {
  if (lines.length === 0 || !FRONTMATTER_FENCE.test(lines[0].trim())) return 0;
  for (let i = 1; i < lines.length; i++) {
    if (FRONTMATTER_FENCE.test(lines[i].trim())) return i + 1;
  }
  return 0; // malformed — start from 0
}

export interface WalkOptions {
  /**
   * Predicate that returns `true` when the line (after stripping heading markers)
   * is a known section header (e.g. matches a discipline / school / deity label).
   * Used by the walker to track the current section across separators and prose.
   */
  isSectionHeader?: (line: string) => boolean;
}

export function walkBook(path: string, opts: WalkOptions = {}): BookBlock[] {
  const text = readFileSync(path, 'utf8');
  const lines = text.split(/\r?\n/);
  const blocks: BookBlock[] = [];

  let chapter = '';
  let section = '';
  const isSectionHeader = opts.isSectionHeader ?? (() => false);

  const startIdx = skipFrontmatter(lines);

  // Look ahead for bullet-block starts; track the line above as the name.
  for (let i = startIdx; i < lines.length; i++) {
    const rawLine = lines[i].trimEnd();

    // Skip Obsidian TOC bullets — they look like `- [[#Section|Label]]`.
    if (OBSIDIAN_TOC_BULLET.test(rawLine.trim())) continue;

    // Chapter headings: ## or # level (the two highest we honour).
    const chMatch = rawLine.match(CHAPTER_HEADING);
    if (chMatch) {
      chapter = stripHeadingMarkers(rawLine);
      // Reset section only for top-level (##) headings.
      if (rawLine.startsWith('## ') || rawLine.startsWith('# ')) section = '';
      continue;
    }

    // Section sub-headings: ### or #### level become section markers.
    const secMatch = rawLine.match(SECTION_HEADING);
    if (secMatch) {
      const bare = stripHeadingMarkers(rawLine);
      const isL3 = rawLine.startsWith('### ');
      if (isL3 || isSectionHeader(bare)) {
        section = bare;
        continue;
      }
    }

    // Track plain-text section headers (legacy format).
    const trimmed = rawLine.trim();
    if (trimmed && !SEPARATOR.test(trimmed) && !BULLET.test(trimmed) && isSectionHeader(trimmed)) {
      section = trimmed;
      continue;
    }

    if (BULLET.test(rawLine)) {
      // Walk up to find the name line (first non-empty, non-separator, non-heading line above).
      let nameLine = '';
      let nameIdx = i - 1;
      while (nameIdx >= startIdx) {
        const candidate = lines[nameIdx].trim();
        // Skip empty lines, separators, TOC links, list items, and requirements lines
        if (
          candidate &&
          !SEPARATOR.test(candidate) &&
          !OBSIDIAN_TOC_BULLET.test(candidate) &&
          !/^[*-]\s+/.test(candidate) &&
          !/^(?:[*-]\s+)?Wymagania?:/i.test(candidate) &&
          !/^>/.test(candidate)
        ) {
          // Strip any heading markers; skip pure chapter/section headings as the name
          // (they are already captured in `chapter`/`section`).
          nameLine = stripHeadingMarkers(candidate);
          break;
        }
        nameIdx--;
      }

      // Collect the bullet block.
      const fields: Array<{ key: string; value: string }> = [];
      let j = i;
      while (j < lines.length) {
        const m = lines[j].trimEnd().match(BULLET);
        if (!m) break;
        fields.push({ key: m[1].trim(), value: m[2].trim() });
        j++;
      }

      // Description = following lines until next bullet block, separator, or heading.
      const descLines: string[] = [];
      let k = j;
      while (k < lines.length) {
        const cur = lines[k].trimEnd();
        if (SEPARATOR.test(cur)) break;
        if (HEADING.test(cur)) break;
        if (BULLET.test(cur)) break;
        if (OBSIDIAN_TOC_BULLET.test(cur.trim())) break;
        descLines.push(cur);
        k++;
      }

      blocks.push({
        name: nameLine,
        fields,
        description: descLines.join('\n').trim(),
        chapter,
        section,
        line: nameIdx + 1,
      });

      i = k - 1; // advance past description
    }
  }

  return blocks;
}
