"""
validate-chapter.py — Schema conformance validator for Anakai Kai Behaviour Graph chapter files.

Usage:
    python tools/validate-chapter.py chapters/00-intro.md
    python tools/validate-chapter.py chapters/01-finding-prospects.md

Validates:
- ChapterFrontmatter (YAML frontmatter)
- UserState, Loop, Principle, AntiPattern, Script nodes (per # section + ## headers)
- GapListEntry list
- ValidationScenario list (count: 5–10)
- CrossChapterLink list

Reports per-node PASS/FAIL with field + schema rule for failures.
"""

import sys
import re
import json
import yaml
import jsonschema
from jsonschema import validate, ValidationError
from pathlib import Path

# ---------------------------------------------------------------------------
# Schema loading
# ---------------------------------------------------------------------------

SCHEMA_PATH = Path(__file__).parent.parent / "schema" / "v1.0.json"

with open(SCHEMA_PATH, encoding="utf-8") as f:
    FULL_SCHEMA = json.load(f)

DEFS = FULL_SCHEMA["$defs"]

# Build a resolver so $ref within defs resolves correctly
RESOLVER = jsonschema.RefResolver.from_schema(FULL_SCHEMA)

def get_def_schema(def_name):
    return {"$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": f"#/$defs/{def_name}", "$defs": DEFS}

# ---------------------------------------------------------------------------
# Markdown parsing helpers
# ---------------------------------------------------------------------------

def parse_frontmatter(text):
    """Extract YAML frontmatter from between the first pair of --- delimiters."""
    # The file starts with --- on line 1
    match = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
    if not match:
        return None, text
    yaml_text = match.group(1)
    try:
        data = yaml.safe_load(yaml_text)
    except yaml.YAMLError as e:
        return {"_parse_error": str(e)}, text[match.end():]
    return data, text[match.end():]


def split_top_sections(text):
    """
    Split the markdown body (after frontmatter) into top-level # sections.
    Returns dict: {section_name: section_body}.
    Section names are the raw H1 header text (stripped).
    """
    sections = {}
    # Find all H1 headers (lines starting with exactly one #, not ##)
    pattern = re.compile(r'^# (.+)$', re.MULTILINE)
    matches = list(pattern.finditer(text))
    for i, m in enumerate(matches):
        name = m.group(1).strip()
        start = m.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
        sections[name] = text[start:end]
    return sections


def split_h2_nodes(section_text):
    """
    Split a section body into H2 (##) blocks.
    Returns list of (header_id, block_text).
    """
    pattern = re.compile(r'^## (.+)$', re.MULTILINE)
    matches = list(pattern.finditer(section_text))
    nodes = []
    for i, m in enumerate(matches):
        header = m.group(1).strip()
        # Strip inline comment if present: ## S-foo  <!-- ... -->
        header_clean = re.sub(r'\s*<!--.*?-->', '', header).strip()
        start = m.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(section_text)
        nodes.append((header_clean, section_text[start:end]))
    return nodes


# ---------------------------------------------------------------------------
# Field parsers
# ---------------------------------------------------------------------------

def parse_bullet_fields(block_text):
    """
    Parse lines of the form:
        - **field_name:** value
    Also handles:
        - **field_name:** [item1, item2]
    Returns dict of field_name -> raw string value.
    Multi-line continuation lines (indented, not starting with -) are appended
    to the previous field's value.
    """
    fields = {}
    current_key = None
    current_val_lines = []

    for line in block_text.splitlines():
        # Match bullet field
        m = re.match(r'^\s*-\s+\*\*([a-zA-Z_]+):\*\*\s*(.*)', line)
        if m:
            if current_key:
                fields[current_key] = "\n".join(current_val_lines).strip()
            current_key = m.group(1)
            current_val_lines = [m.group(2)]
        elif current_key and line.startswith('  ') and not line.strip().startswith('-'):
            # Continuation of previous field (but not a new sub-bullet)
            current_val_lines.append(line.strip())
        elif current_key and line.strip().startswith('<!--'):
            # Skip comment lines
            pass
        else:
            # Anything else terminates the current field (e.g. a sub-list or blank line)
            if current_key and line.strip() == '':
                current_val_lines.append('')
            elif current_key:
                # Might be continuation or might be something else
                # Only continue if it looks like prose (no leading '-' at top level)
                if not re.match(r'^\s*-\s+\*\*', line):
                    current_val_lines.append(line.strip())

    if current_key:
        fields[current_key] = "\n".join(current_val_lines).strip()

    return fields


def parse_value(raw):
    """
    Convert a raw string value to Python type:
    - "[a, b, c]" -> ["a","b","c"]
    - "[]" -> []
    - bare string -> string
    - strip trailing <!-- comment -->
    """
    if raw is None:
        return None
    # Strip inline comments
    raw = re.sub(r'\s*<!--.*?-->', '', raw, flags=re.DOTALL).strip()
    # Strip outer backticks for source_ref values
    if raw.startswith('`') and raw.endswith('`'):
        raw = raw[1:-1]

    # Check if it's an array: starts with [ and ends with ]
    if raw.startswith('[') and raw.endswith(']'):
        inner = raw[1:-1].strip()
        if inner == '':
            return []
        # Split by comma, respecting possible quotes
        items = [item.strip().strip('"').strip("'") for item in inner.split(',')]
        # Filter empty
        items = [i for i in items if i]
        return items

    return raw


def parse_steps_block(block_text):
    """
    Parse the steps: sub-block from a loop node body text.
    Looks for the **steps:** field, then parses YAML-like indented sub-items.
    Returns a list of dicts with keys: id, kai_intent, script_ref (optional).
    """
    steps = []
    # Find the steps: field marker
    steps_match = re.search(r'\*\*steps:\*\*', block_text)
    if not steps_match:
        return steps

    steps_section = block_text[steps_match.end():]
    # The steps section runs until the next top-level bullet field
    # (a line that starts with - **<something>:**)
    next_field = re.search(r'\n\s*-\s+\*\*[a-zA-Z_]+:\*\*', steps_section)
    if next_field:
        steps_section = steps_section[:next_field.start()]

    # Parse individual step entries — each starts with "  - id: step-..."
    # Format (per template):
    #   - id: step-1-name
    #     kai_intent: "..."
    #     script_ref: Sc-XX-YY (optional)
    step_pattern = re.compile(
        r'-\s+id:\s+(step-[^\n]+?)\s*\n'
        r'\s+kai_intent:\s+"?(.*?)"?\s*\n'
        r'(?:\s+script_ref:\s+([^\n]+))?',
        re.DOTALL
    )

    # Alternative: parse line-by-line
    current_step = None
    for line in steps_section.splitlines():
        stripped = line.strip()
        # Strip comments
        stripped = re.sub(r'<!--.*?-->', '', stripped).strip()
        if not stripped:
            continue

        id_m = re.match(r'-\s+id:\s+(.+)', stripped)
        if id_m:
            if current_step:
                steps.append(current_step)
            current_step = {"id": id_m.group(1).strip()}
            continue

        intent_m = re.match(r'kai_intent:\s+"?(.*?)"?\s*$', stripped)
        if intent_m and current_step is not None:
            val = intent_m.group(1).strip().strip('"')
            current_step["kai_intent"] = val
            continue

        # Handle multi-line kai_intent (continues on next line without a key)
        if current_step and "kai_intent" not in current_step:
            # This might be a continuation — skip for now; single-line assumed
            pass

        ref_m = re.match(r'script_ref:\s+(.+)', stripped)
        if ref_m and current_step is not None:
            val = ref_m.group(1).strip()
            # Could be null, omit, or an actual id
            if val.lower() not in ('null', 'none', 'omit', '<optional>', ''):
                # Also strip any angle-bracket hints
                val = re.sub(r'<[^>]+>', '', val).strip()
                if val:
                    current_step["script_ref"] = val
            continue

    if current_step:
        steps.append(current_step)

    return steps


# ---------------------------------------------------------------------------
# Node constructors — build JSON objects from parsed field dicts
# ---------------------------------------------------------------------------

def build_user_state(node_id, block_text):
    fields = parse_bullet_fields(block_text)
    obj = {"id": node_id, "type": "user_state"}
    str_fields = ["title", "body", "source_ref", "kai_voice_draft", "entry_criteria", "exit_criteria", "notes"]
    arr_fields = ["voice_context_tags", "linked_triggers", "linked_vulnerable_to"]
    for f in str_fields:
        if f in fields:
            v = parse_value(fields[f])
            if v:
                obj[f] = v
    for f in arr_fields:
        if f in fields:
            obj[f] = parse_value(fields[f])
    return obj


def build_loop(node_id, block_text):
    fields = parse_bullet_fields(block_text)
    obj = {"id": node_id, "type": "loop"}
    str_fields = ["title", "body", "source_ref", "kai_voice_draft", "notes"]
    arr_fields = ["voice_context_tags", "linked_transitions_to", "linked_backed_by",
                  "linked_precedes", "linked_edifies"]
    for f in str_fields:
        if f in fields:
            v = parse_value(fields[f])
            if v:
                obj[f] = v
    for f in arr_fields:
        if f in fields:
            obj[f] = parse_value(fields[f])
    # Steps are special
    obj["steps"] = parse_steps_block(block_text)
    return obj


def build_principle(node_id, block_text):
    fields = parse_bullet_fields(block_text)
    obj = {"id": node_id, "type": "principle"}
    str_fields = ["title", "body", "source_ref", "kai_voice_draft", "notes"]
    arr_fields = ["voice_context_tags", "linked_corrects", "linked_depends_on"]
    for f in str_fields:
        if f in fields:
            v = parse_value(fields[f])
            if v:
                obj[f] = v
    for f in arr_fields:
        if f in fields:
            obj[f] = parse_value(fields[f])
    return obj


def build_anti_pattern(node_id, block_text):
    fields = parse_bullet_fields(block_text)
    obj = {"id": node_id, "type": "anti_pattern"}
    str_fields = ["title", "body", "source_ref", "kai_voice_draft", "trigger_pattern", "cost", "notes"]
    arr_fields = ["voice_context_tags"]
    for f in str_fields:
        if f in fields:
            v = parse_value(fields[f])
            if v:
                obj[f] = v
    for f in arr_fields:
        if f in fields:
            obj[f] = parse_value(fields[f])
    return obj


def build_script(node_id, block_text):
    fields = parse_bullet_fields(block_text)
    obj = {"id": node_id, "type": "script"}
    str_fields = ["title", "body", "source_ref", "kai_voice_draft", "situation", "language", "notes"]
    arr_fields = ["voice_context_tags", "requires_variants", "linked_backed_by",
                  "linked_implements_step", "linked_paraphrase_of", "linked_edifies"]
    for f in str_fields:
        if f in fields:
            v = parse_value(fields[f])
            if v:
                obj[f] = v
    for f in arr_fields:
        if f in fields:
            obj[f] = parse_value(fields[f])
    return obj


# ---------------------------------------------------------------------------
# Gap list parser
# ---------------------------------------------------------------------------

def parse_gap_list(section_text):
    """
    Parse gap list entries. Format:
        - [ch.NN p.NN para.N] tag: explanation
        - [ch.NN p.NN para.Nb] tag: explanation

    Tags can be bare (anecdote, transitional, recap, meta-commentary,
    forward-reference) or compound (restatement-of-AP-00-03, etc.).
    They are separated from the explanation by a colon followed by a space.
    """
    entries = []
    # Match: - [para_ref] tag: optional note
    # Tag extends to the first colon that is followed by whitespace (or end-of-line).
    # This handles tags like restatement-of-AP-00-03 which contain uppercase and digits.
    pattern = re.compile(
        r'^\s*-\s+\[([^\]]+)\]\s+'   # - [para_ref]
        r'([A-Za-z0-9-]+)'            # tag (may include uppercase chars for artifact ids)
        r'(?::\s*(.*?))?$',            # optional : note
        re.MULTILINE
    )
    for m in pattern.finditer(section_text):
        para_ref = m.group(1).strip()
        tag = m.group(2).strip()
        note = (m.group(3) or "").strip()
        entry = {"para_ref": para_ref, "tag": tag}
        if note:
            entry["note"] = note
        entries.append(entry)
    return entries


# ---------------------------------------------------------------------------
# Validation scenarios parser
# ---------------------------------------------------------------------------

def parse_validation_scenarios(section_text):
    """
    Parse validation scenarios. Each starts with:
        ### Scenario N: Title
    Followed by numbered steps:
        1. text
        2. text
    """
    scenarios = []
    # Split by ### headers
    scenario_pattern = re.compile(r'^### Scenario\s+\d+:\s+(.+)$', re.MULTILINE)
    matches = list(scenario_pattern.finditer(section_text))
    for i, m in enumerate(matches):
        title = m.group(1).strip()
        start = m.end()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(section_text)
        block = section_text[start:end]
        # Parse numbered steps
        step_pattern = re.compile(r'^\s*(\d+)\.\s+(.+)$', re.MULTILINE)
        steps = []
        for sm in step_pattern.finditer(block):
            steps.append({"step_number": int(sm.group(1)), "text": sm.group(2).strip()})
        scenarios.append({"title": title, "steps": steps})
    return scenarios


# ---------------------------------------------------------------------------
# Cross-chapter links parser
# ---------------------------------------------------------------------------

def parse_cross_chapter_links(section_text):
    """
    Parse cross-chapter link entries. Format:
        - `source-id` --[edge-type]--> `target-id`  <!-- note -->
    """
    links = []
    pattern = re.compile(
        r'^\s*-\s+`([^`]+)`\s+--\[([a-z_]+)\]-->\s+`([^`]+)`(?:\s+<!--\s*(.*?)\s*-->)?',
        re.MULTILINE
    )
    for m in pattern.finditer(section_text):
        link = {
            "source_id": m.group(1).strip(),
            "edge_type": m.group(2).strip(),
            "target_id": m.group(3).strip()
        }
        if m.group(4):
            link["note"] = m.group(4).strip()
        links.append(link)
    return links


# ---------------------------------------------------------------------------
# Validator
# ---------------------------------------------------------------------------

class Failure:
    def __init__(self, node_id, field, message, category=None):
        self.node_id = node_id
        self.field = field
        self.message = message
        self.category = category  # true_bug | schema_too_strict | field_rename

    def __str__(self):
        cat = f" [{self.category}]" if self.category else ""
        return f"  FAIL {self.node_id} / {self.field}: {self.message}{cat}"


def validate_object(node_id, obj, def_name, failures):
    """Validate obj against schema $defs/<def_name>. Appends Failure items."""
    schema = get_def_schema(def_name)
    try:
        validate(instance=obj, schema=schema, resolver=RESOLVER)
        return True
    except ValidationError as e:
        # e.path gives the field path within the object
        field = ".".join(str(p) for p in e.absolute_path) if e.absolute_path else "(root)"
        failures.append(Failure(node_id, field, e.message))
        return False
    except Exception as e:
        failures.append(Failure(node_id, "(validator)", str(e)))
        return False


def validate_object_collect_all(node_id, obj, def_name, failures):
    """Validate obj and collect ALL schema violations (not just first)."""
    schema = get_def_schema(def_name)
    validator = jsonschema.Draft202012Validator(schema, resolver=RESOLVER)
    errors = list(validator.iter_errors(obj))
    if not errors:
        return True
    for e in errors:
        field = ".".join(str(p) for p in e.absolute_path) if e.absolute_path else "(root)"
        failures.append(Failure(node_id, field, e.message))
    return False


def validate_chapter(filepath):
    filepath = Path(filepath)
    print(f"\n{'='*70}")
    print(f"VALIDATING: {filepath.name}")
    print(f"{'='*70}")

    text = filepath.read_text(encoding="utf-8")

    failures = []
    counts = {
        "frontmatter": 0,
        "user_state": 0,
        "loop": 0,
        "principle": 0,
        "anti_pattern": 0,
        "script": 0,
        "gap_list_entry": 0,
        "validation_scenario": 0,
        "cross_chapter_link": 0
    }
    passed = 0
    failed = 0

    # ---- Frontmatter ----
    frontmatter_data, body = parse_frontmatter(text)
    if frontmatter_data is None:
        print("  ERROR: No YAML frontmatter found")
        failures.append(Failure("frontmatter", "(parse)", "No frontmatter block found"))
    elif "_parse_error" in frontmatter_data:
        failures.append(Failure("frontmatter", "(yaml)", frontmatter_data["_parse_error"]))
    else:
        counts["frontmatter"] = 1
        node_failures = []
        ok = validate_object_collect_all("frontmatter", frontmatter_data, "ChapterFrontmatter", node_failures)
        if ok:
            print(f"  PASS frontmatter ({frontmatter_data.get('id', '?')})")
            passed += 1
        else:
            failed += 1
            for f in node_failures:
                failures.append(f)
                print(str(f))

    # ---- Parse sections ----
    sections = split_top_sections(body)

    section_to_type = {
        "user_states": ("user_state", "UserState", build_user_state),
        "loops": ("loop", "Loop", build_loop),
        "principles": ("principle", "Principle", build_principle),
        "anti_patterns": ("anti_pattern", "AntiPattern", build_anti_pattern),
        "scripts": ("script", "Script", build_script),
    }

    for section_key, (type_name, def_name, builder) in section_to_type.items():
        if section_key not in sections:
            print(f"  WARN: Section '# {section_key}' not found")
            continue
        section_text = sections[section_key]
        nodes = split_h2_nodes(section_text)
        if not nodes:
            print(f"  INFO: # {section_key} — 0 nodes (empty section)")
            continue
        for node_id, block_text in nodes:
            counts[type_name] += 1
            obj = builder(node_id, block_text)
            node_failures = []
            ok = validate_object_collect_all(node_id, obj, def_name, node_failures)
            if ok:
                print(f"  PASS {node_id} ({type_name})")
                passed += 1
            else:
                failed += 1
                for f in node_failures:
                    failures.append(f)
                    print(str(f))

    # ---- Gap list ----
    if "gap_list" in sections:
        gap_entries = parse_gap_list(sections["gap_list"])
        for i, entry in enumerate(gap_entries):
            counts["gap_list_entry"] += 1
            node_id = f"gap[{entry.get('para_ref', i)}]"
            node_failures = []
            ok = validate_object_collect_all(node_id, entry, "GapListEntry", node_failures)
            if ok:
                passed += 1
            else:
                failed += 1
                for f in node_failures:
                    failures.append(f)
                    print(str(f))
        print(f"  INFO: gap_list — {len(gap_entries)} entries ({sum(1 for f in failures if 'gap[' in f.node_id)} with errors)")

    # ---- Validation scenarios ----
    if "validation_scenarios" in sections:
        scenarios = parse_validation_scenarios(sections["validation_scenarios"])
        counts["validation_scenario"] = len(scenarios)
        # Check count constraint (5–10)
        if len(scenarios) < 5:
            failures.append(Failure("validation_scenarios", "count",
                                    f"Too few scenarios: {len(scenarios)} (minimum 5)"))
            print(f"  FAIL validation_scenarios: only {len(scenarios)} (minimum 5)")
            failed += 1
        elif len(scenarios) > 10:
            failures.append(Failure("validation_scenarios", "count",
                                    f"Too many scenarios: {len(scenarios)} (maximum 10)"))
            print(f"  FAIL validation_scenarios: {len(scenarios)} exceeds maximum 10")
            failed += 1
        else:
            print(f"  INFO: validation_scenarios — {len(scenarios)} scenarios (OK)")

        for i, sc in enumerate(scenarios):
            node_id = f"scenario[{i+1}]"
            node_failures = []
            ok = validate_object_collect_all(node_id, sc, "ValidationScenario", node_failures)
            if ok:
                passed += 1
            else:
                failed += 1
                for f in node_failures:
                    failures.append(f)
                    print(str(f))
    else:
        print("  WARN: Section '# validation_scenarios' not found")

    # ---- Cross-chapter links ----
    if "cross_chapter_links" in sections:
        links = parse_cross_chapter_links(sections["cross_chapter_links"])
        counts["cross_chapter_link"] = len(links)
        for i, link in enumerate(links):
            node_id = f"cross_chapter_link[{i+1}]"
            node_failures = []
            ok = validate_object_collect_all(node_id, link, "CrossChapterLink", node_failures)
            if ok:
                passed += 1
            else:
                failed += 1
                for f in node_failures:
                    failures.append(f)
                    print(str(f))
        print(f"  INFO: cross_chapter_links — {len(links)} entries")
    else:
        print("  WARN: Section '# cross_chapter_links' not found")

    # ---- Summary ----
    print(f"\n{'─'*50}")
    print(f"Node counts: user_state={counts['user_state']}, loop={counts['loop']}, "
          f"principle={counts['principle']}, anti_pattern={counts['anti_pattern']}, "
          f"script={counts['script']}")
    print(f"Gap list entries: {counts['gap_list_entry']}")
    print(f"Validation scenarios: {counts['validation_scenario']}")
    print(f"Cross-chapter links: {counts['cross_chapter_link']}")
    total = passed + failed
    print(f"\nResult: {passed} passed, {failed} failed (total checks: {total})")

    if failures:
        print(f"\n{'─'*50}")
        print("ALL FAILURES:")
        for f in failures:
            print(f"  • [{f.node_id}] field={f.field}: {f.message}")
    else:
        print("\nSTATUS: CLEAN — 0 failures")

    return failures, counts


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python tools/validate-chapter.py <chapter-file.md>")
        sys.exit(1)

    target = sys.argv[1]
    # Resolve relative to the anakai-kai-coach dir if needed
    p = Path(target)
    if not p.is_absolute():
        p = Path(__file__).parent.parent / target

    failures, counts = validate_chapter(p)
    sys.exit(1 if failures else 0)
