#!/usr/bin/env python3
"""task-create: deterministic validator for partial-views (R1) DSL.

Mirrors R1's tokenization rules so we can reject malformed input
without a Skill round-trip. Reads DSL from stdin or a file path arg.

Exit codes:
    0  valid (or empty input)
    1  parse error (single message printed to stderr)
"""

import re
import sys

NODE_PATTERNS = [
    re.compile(r"^if-attr:[A-Za-z_][A-Za-z0-9_]*(,[A-Za-z_][A-Za-z0-9_]*)*$"),
    re.compile(r"^if-empty:\s+.+$"),
    re.compile(r"^if:\s+.+$"),
    re.compile(r"^>\s+printAttr:[A-Za-z_][A-Za-z0-9_]*(\s+\{classes:\s*\".*\"\})?$"),
    re.compile(r"^>\s+[A-Za-z_][A-Za-z0-9_]*(\s+\{classes:\s*\".*\"\})?$"),
    re.compile(r"^__svg\s+[A-Za-z0-9_-]+(\s+\{classes:\s*\".*\"\})?$"),
    # DOM element: tag(.cls)*  or  tag#id(.cls)*  or  .cls(.cls)*  or  #id(.cls)*
    re.compile(r"^[A-Za-z][A-Za-z0-9]*(#[A-Za-z0-9_-]+)?(\.[A-Za-z0-9_:\[\]\(\)\{\}\/\-]+)*$"),
    re.compile(r"^(\.[A-Za-z0-9_:\[\]\(\)\{\}\/\-]+)+$"),
]

DECL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$")
CONT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


def strip_comment(line: str) -> str:
    in_brace = 0
    out = []
    for ch in line:
        if ch == "{":
            in_brace += 1
        elif ch == "}":
            in_brace = max(0, in_brace - 1)
        elif ch == "#" and in_brace == 0:
            break
        out.append(ch)
    return "".join(out).rstrip()


def fail(msg: str) -> None:
    print(f"DSL parse error: {msg}", file=sys.stderr)
    sys.exit(1)


def validate(text: str) -> None:
    blocks = []
    current = None
    has_prev_decl = False

    for lineno, raw in enumerate(text.splitlines(), 1):
        stripped = strip_comment(raw)
        if not stripped.strip():
            if current is not None:
                blocks.append(current)
                current = None
            continue

        leading = len(stripped) - len(stripped.lstrip(" "))
        if leading % 2 != 0:
            fail(f"line {lineno}: odd indentation ({leading} spaces) — must be a multiple of 2")
        indent = leading // 2
        body = stripped.strip()

        if indent == 0:
            if DECL_RE.match(body):
                current = {"line": lineno, "decl": body, "first_child_kind": None}
            elif CONT_RE.match(body):
                if not has_prev_decl:
                    fail(f"line {lineno}: continuation block '{body}' has no preceding type declaration")
                current = {"line": lineno, "decl": body, "first_child_kind": None}
            else:
                fail(f"line {lineno}: '{body}' is not a valid partial-view declaration (expected 'type/viewname' or continuation)")
            has_prev_decl = True
            continue

        if current is None:
            fail(f"line {lineno}: child node '{body}' appears before any partial-view declaration")

        # First child guard rule: if-attr/if-empty must be first child at indent 1.
        kind = node_kind(body)
        if kind is None:
            fail(f"line {lineno}: '{body}' does not match any known node pattern")

        if indent == 1 and current["first_child_kind"] is None:
            current["first_child_kind"] = kind
        else:
            if kind in ("if-attr", "if-empty"):
                fail(f"line {lineno}: '{kind}:' guard must be the first child of its block")


def node_kind(body: str) -> str | None:
    if body.startswith("if-attr:"):
        return "if-attr" if NODE_PATTERNS[0].match(body) else None
    if body.startswith("if-empty:"):
        return "if-empty" if NODE_PATTERNS[1].match(body) else None
    if body.startswith("if:"):
        return "if" if NODE_PATTERNS[2].match(body) else None
    if body.startswith("> printAttr:"):
        return "printAttr" if NODE_PATTERNS[3].match(body) else None
    if body.startswith(">"):
        return "printMe" if NODE_PATTERNS[4].match(body) else None
    if body.startswith("__svg"):
        return "svg" if NODE_PATTERNS[5].match(body) else None
    if NODE_PATTERNS[6].match(body) or NODE_PATTERNS[7].match(body):
        return "dom"
    return None


def main() -> None:
    if len(sys.argv) > 1 and sys.argv[1] not in ("-", "--stdin"):
        with open(sys.argv[1], "r", encoding="utf-8") as f:
            text = f.read()
    else:
        text = sys.stdin.read()

    if not text.strip():
        return

    validate(text)


if __name__ == "__main__":
    main()
