"""Format-agnostic markdown parser for skill and role files.

Extracts a normalized structure from either dotai or Claude-native format.
"""

import re
from pathlib import Path

from pydantic import BaseModel, Field


class ParsedArtifact(BaseModel):
    """Normalized representation of a parsed skill or role file."""

    name: str = ""
    description: str = ""
    entity_type: str = "unknown"  # "skill" or "role"
    format: str = "unknown"  # "dotai-skill", "dotai-role", "claude-native", "unknown"

    # Shared
    tags: list[str] = Field(default_factory=list)

    # Skill fields
    steps: list[str] = Field(default_factory=list)
    inputs: list[dict] = Field(default_factory=list)
    examples: list[str] = Field(default_factory=list)
    gotchas: list[str] = Field(default_factory=list)
    trigger: str = ""
    category: str = ""
    allowed_tools: list[str] = Field(default_factory=list)

    # Role fields
    persona: str = ""
    principles: list[str] = Field(default_factory=list)
    anti_patterns: list[str] = Field(default_factory=list)

    # Raw
    raw_body: str = ""
    sections: dict[str, str] = Field(default_factory=dict)
    frontmatter: dict[str, str] = Field(default_factory=dict)

    file_path: Path | None = None


def _strip_details_blocks(body: str) -> str:
    """Remove <details>...</details> blocks from body.

    These contain embedded role definitions in Claude Code SKILL.md files
    and should not influence format detection or content extraction.
    """
    return re.sub(r"<details>.*?</details>", "", body, flags=re.DOTALL).strip()


def detect_format(path: Path) -> str:
    """Detect file format: dotai-skill, dotai-role, claude-native, mcp-config, or unknown."""
    if not path.exists():
        return "unknown"

    if path.is_dir():
        if (path / "SKILL.md").exists():
            return "claude-native"
        if (path / "main.md").exists():
            return "dotai-skill"
        return "unknown"

    # MCP config files
    if path.name in ("mcp.json", "claude_desktop_config.json"):
        return "mcp-config"

    content = path.read_text()
    fm = _extract_frontmatter(content)
    body = _extract_body(content)

    # Claude Code auto-generated SKILL.md (has argument-hint or Auto-generated marker)
    if "argument-hint" in fm or "Auto-generated by dotai" in body:
        return "claude-native"

    # SKILL.md filename convention
    if path.name == "SKILL.md":
        return "claude-native"

    # Claude-native markers in frontmatter
    claude_keys = {"compatibility", "license"}
    if claude_keys & set(fm.keys()):
        return "claude-native"

    # dotai-skill markers in frontmatter
    dotai_skill_keys = {"trigger", "allowed-tools", "category"}
    if dotai_skill_keys & set(fm.keys()):
        return "dotai-skill"

    # dotai-skill markers in body
    if re.search(r"^## (Steps|Inputs)\b", body, re.MULTILINE):
        return "dotai-skill"

    # Strip <details> blocks before checking for role patterns — embedded roles
    # in skill files shouldn't trigger role detection
    clean_body = _strip_details_blocks(body)

    # dotai-role: has persona-like content + principles (outside <details>)
    if re.search(r"^## Principles\b", clean_body, re.MULTILINE) and re.search(r"^## Anti-patterns", clean_body, re.MULTILINE):
        return "dotai-role"

    # Role heuristics (persona without skill markers)
    if re.search(r"^You are a\b", clean_body, re.MULTILINE):
        return "dotai-role"

    return "unknown"


def parse_file(path: Path, force_format: str | None = None) -> ParsedArtifact:
    """Parse a skill or role file into a normalized structure.

    Args:
        path: Path to the file (or directory with SKILL.md/main.md)
        force_format: Override auto-detection
    """
    if path.is_dir():
        if (path / "SKILL.md").exists():
            actual_path = path / "SKILL.md"
        elif (path / "main.md").exists():
            actual_path = path / "main.md"
        else:
            return ParsedArtifact(file_path=path)
        return parse_file(actual_path, force_format)

    fmt = force_format or detect_format(path)
    content = path.read_text()
    fm = _extract_frontmatter(content)
    body = _extract_body(content)

    artifact = ParsedArtifact(
        file_path=path,
        format=fmt,
        frontmatter=fm,
        raw_body=body,
        name=fm.get("name", path.stem.replace("-", " ").replace("_", " ").title()),
        description=fm.get("description", ""),
        tags=_parse_list(fm.get("tags", "")),
    )

    if fmt in ("dotai-role",):
        artifact.entity_type = "role"
        _parse_role_body(artifact, body)
    elif fmt in ("dotai-skill", "claude-native", "unknown"):
        artifact.entity_type = "skill"
        _parse_skill_body(artifact, body, fm)

    # Skill-specific frontmatter
    if artifact.entity_type == "skill":
        artifact.trigger = fm.get("trigger", "")
        artifact.category = fm.get("category", "")
        artifact.allowed_tools = _parse_list(fm.get("allowed-tools", ""))

    return artifact


def _parse_skill_body(artifact: ParsedArtifact, body: str, fm: dict) -> None:
    """Extract skill sections from body."""
    # Strip <details> blocks (embedded role definitions in Claude Code SKILL.md)
    clean_body = _strip_details_blocks(body)
    sections = _split_sections(clean_body)
    artifact.sections = sections
    # Use clean body for raw_body so rubrics score the actual skill content
    artifact.raw_body = clean_body

    # Description: first content before any ## or # header in clean body
    desc_match = re.match(r"^(.*?)(?=^##? |\Z)", clean_body, re.DOTALL | re.MULTILINE)
    if desc_match:
        desc = desc_match.group(1).strip()
        if desc and not artifact.description:
            artifact.description = desc.split("\n\n")[0].strip()

    # Steps — try AST-based extraction first, fall back to regex
    steps_body = sections.get("steps") or sections.get("workflow") or sections.get("instructions") or ""
    try:
        ast_steps = _extract_steps_ast(steps_body)
        if ast_steps:
            artifact.steps.extend(ast_steps)
        else:
            raise ValueError("no steps from AST")
    except Exception:
        # Regex fallback: capture numbered items with their continuation lines (sub-bullets)
        step_blocks = re.split(r"(?=^\d+\.\s)", steps_body, flags=re.MULTILINE)
        for block in step_blocks:
            block = block.strip()
            if not block:
                continue
            match = re.match(r"^\d+\.\s*(.+)", block, re.DOTALL)
            if match:
                # Join multi-line step into a single string, preserving sub-bullets
                step_text = match.group(1).strip()
                artifact.steps.append(step_text)

    # Inputs
    inputs_body = sections.get("inputs") or sections.get("parameters") or sections.get("arguments") or sections.get("configuration") or ""
    for match in re.finditer(r"-\s*`(\w+)`\s*(?:\((\w+)\))?\s*:?\s*(.+)", inputs_body):
        artifact.inputs.append({
            "name": match.group(1),
            "required": match.group(2) == "required" if match.group(2) else False,
            "description": match.group(3).strip(),
        })

    # Examples
    examples_body = sections.get("examples") or sections.get("usage") or ""
    for match in re.finditer(r"^-\s*(.+)$", examples_body, re.MULTILINE):
        artifact.examples.append(match.group(1).strip())

    # Gotchas
    gotchas_body = sections.get("gotchas") or sections.get("caveats") or sections.get("warnings") or sections.get("notes") or sections.get("limitations") or ""
    for match in re.finditer(r"^-\s*(.+)$", gotchas_body, re.MULTILINE):
        artifact.gotchas.append(match.group(1).strip())

    # Only fall back to body-wide numbered list extraction if we also found
    # other structured sections (inputs, examples, gotchas). Otherwise the
    # numbered items are likely content, not workflow steps.
    if not artifact.steps and (artifact.inputs or artifact.examples or artifact.gotchas):
        for match in re.finditer(r"^\d+\.\s*(.+)$", clean_body, re.MULTILINE):
            artifact.steps.append(match.group(1).strip())


def _parse_role_body(artifact: ParsedArtifact, body: str) -> None:
    """Extract role sections from body."""
    sections = _split_sections(body)
    artifact.sections = sections

    # Persona: everything before the first ## header
    persona_match = re.match(r"^(.*?)(?=^## |\Z)", body, re.DOTALL | re.MULTILINE)
    if persona_match:
        artifact.persona = persona_match.group(1).strip()
        if not artifact.description:
            # First sentence as description
            first_line = artifact.persona.split("\n")[0].strip()
            artifact.description = first_line[:200]

    # Principles
    principles_body = sections.get("principles") or ""
    for match in re.finditer(r"^-\s*(.+)$", principles_body, re.MULTILINE):
        artifact.principles.append(match.group(1).strip())

    # Anti-patterns
    anti_body = sections.get("anti-patterns (avoid these)") or sections.get("anti-patterns") or ""
    for match in re.finditer(r"^-\s*(.+)$", anti_body, re.MULTILINE):
        artifact.anti_patterns.append(match.group(1).strip())


def _split_sections_ast(body: str) -> dict[str, str]:
    """Split markdown body into sections by ## headers using mistletoe AST."""
    import mistletoe
    from mistletoe.block_token import Heading

    doc = mistletoe.Document(body)
    sections: dict[str, str] = {}
    lines = body.split("\n")

    # Collect heading positions from the AST
    heading_positions: list[tuple[str, int]] = []
    for token in doc.children:
        if isinstance(token, Heading) and token.level == 2:
            # Extract the raw text from the heading's children
            heading_text = _render_span_tokens(token.children).strip().lower()
            # Find the line number — mistletoe tokens have a .line_number attribute (1-based) if available
            # But line_number can be unreliable; find the heading line by matching
            heading_positions.append((heading_text, getattr(token, 'line_number', None)))

    if not heading_positions:
        return sections

    # Rebuild sections by finding heading lines in the source
    # This approach ensures we capture all content between headings exactly as the regex would
    header_line_indices: list[tuple[str, int]] = []
    for line_idx, line in enumerate(lines):
        header_match = re.match(r"^##\s+(.+)$", line)
        if header_match:
            name = header_match.group(1).strip().lower()
            header_line_indices.append((name, line_idx))

    for i, (name, start_idx) in enumerate(header_line_indices):
        if i + 1 < len(header_line_indices):
            end_idx = header_line_indices[i + 1][1]
        else:
            end_idx = len(lines)
        content = "\n".join(lines[start_idx + 1 : end_idx]).strip()
        sections[name] = content

    return sections


def _split_sections_regex(body: str) -> dict[str, str]:
    """Split markdown body into sections by ## headers (regex fallback)."""
    sections: dict[str, str] = {}
    current_name = ""
    current_lines: list[str] = []

    for line in body.split("\n"):
        header_match = re.match(r"^##\s+(.+)$", line)
        if header_match:
            if current_name:
                sections[current_name] = "\n".join(current_lines).strip()
            current_name = header_match.group(1).strip().lower()
            current_lines = []
        else:
            current_lines.append(line)

    if current_name:
        sections[current_name] = "\n".join(current_lines).strip()

    return sections


def _split_sections(body: str) -> dict[str, str]:
    """Split markdown body into sections by ## headers.

    Uses mistletoe AST parsing for reliable structure extraction,
    falling back to regex if AST parsing fails.
    """
    try:
        return _split_sections_ast(body)
    except Exception:
        return _split_sections_regex(body)


def _render_span_tokens(children) -> str:
    """Render inline/span tokens back to plain text."""
    parts: list[str] = []
    for child in children:
        # Composite tokens (Strong, Emphasis, etc.) have children with the text
        if hasattr(child, 'children') and child.children:
            parts.append(_render_span_tokens(child.children))
        elif hasattr(child, 'content') and child.content:
            parts.append(child.content)
    return "".join(parts)


def _parse_body_ast(body: str) -> dict:
    """Parse markdown body into structured components using mistletoe AST.

    Returns a dict with:
      - headings: list of (level, text) tuples
      - code_blocks: list of (language, content) tuples
      - lists: list of dicts with 'ordered' bool and 'items' list (supports nesting)
      - paragraphs: list of raw text strings
    """
    import mistletoe
    from mistletoe.block_token import (
        Heading,
        CodeFence,
        BlockCode,
        List as MdList,
        Paragraph,
    )

    doc = mistletoe.Document(body)
    result: dict = {
        "headings": [],
        "code_blocks": [],
        "lists": [],
        "paragraphs": [],
    }

    def _extract_list_items(list_token) -> list[dict]:
        """Recursively extract list items, including nested lists."""
        items = []
        for item in list_token.children:
            text_parts: list[str] = []
            sub_items: list[dict] = []
            for child in (item.children or []):
                if isinstance(child, Paragraph):
                    text_parts.append(_render_span_tokens(child.children))
                elif isinstance(child, MdList):
                    sub_items.extend(_extract_list_items(child))
                elif hasattr(child, 'children') and child.children:
                    text_parts.append(_render_span_tokens(child.children))
            text = " ".join(text_parts).strip()
            items.append({"text": text, "children": sub_items})
        return items

    for token in doc.children:
        if isinstance(token, Heading):
            text = _render_span_tokens(token.children).strip()
            result["headings"].append((token.level, text))
        elif isinstance(token, (CodeFence, BlockCode)):
            lang = getattr(token, 'language', '') or ''
            content = (getattr(token, 'content', '') or
                       (token.children[0].content if token.children else ''))
            result["code_blocks"].append((lang.lower().strip(), content.strip()))
        elif isinstance(token, MdList):
            ordered = hasattr(token, 'start_at') or getattr(token, 'start', None) is not None
            items = _extract_list_items(token)
            result["lists"].append({"ordered": ordered, "items": items})
        elif isinstance(token, Paragraph):
            text = _render_span_tokens(token.children).strip()
            if text:
                result["paragraphs"].append(text)

    return result


def _extract_steps_ast(steps_body: str) -> list[str]:
    """Extract numbered list steps from a section body using mistletoe AST."""
    import mistletoe
    from mistletoe.block_token import List as MdList, Paragraph

    doc = mistletoe.Document(steps_body)
    steps: list[str] = []

    for token in doc.children:
        if isinstance(token, MdList):
            for item in token.children:
                text_parts: list[str] = []
                for child in (item.children or []):
                    if isinstance(child, Paragraph):
                        text_parts.append(_render_span_tokens(child.children))
                    elif isinstance(child, MdList):
                        # Include sub-list text as part of the step
                        for sub_item in child.children:
                            for sub_child in (sub_item.children or []):
                                if isinstance(sub_child, Paragraph):
                                    text_parts.append(
                                        "- " + _render_span_tokens(sub_child.children)
                                    )
                    elif hasattr(child, 'children') and child.children:
                        text_parts.append(_render_span_tokens(child.children))
                text = "\n".join(text_parts).strip()
                if text:
                    steps.append(text)

    return steps


def _extract_frontmatter(content: str) -> dict[str, str]:
    """Extract YAML-like frontmatter."""
    fm: dict[str, str] = {}
    if content.startswith("---"):
        parts = content.split("---", 2)
        if len(parts) >= 3:
            for line in parts[1].strip().split("\n"):
                if ":" in line:
                    key, value = line.split(":", 1)
                    fm[key.strip().lower()] = value.strip()
    return fm


def _extract_body(content: str) -> str:
    """Extract body after frontmatter."""
    if content.startswith("---"):
        parts = content.split("---", 2)
        if len(parts) >= 3:
            return parts[2].strip()
    return content.strip()


def _parse_list(raw: str) -> list[str]:
    """Parse comma-separated list."""
    return [item.strip() for item in raw.split(",") if item.strip()]
