"""Parser for Nhandu Python literate documents."""

from __future__ import annotations

import re

import yaml

from nhandu.models import (
    CodeBlock,
    Document,
    DocumentMetadata,
    InlineCode,
    MarkdownBlock,
)


class PythonLiterateParser:
    """Parser for Python files with literate comments."""

    def __init__(self) -> None:
        # Matches #' at start of line, captures rest of line
        self.markdown_line_pattern = re.compile(r"^#'\s?(.*?)$", re.MULTILINE)
        # Matches #| hide / #| block markers
        self.hide_marker_pattern = re.compile(r"^#\|\s*hide\s*$", re.MULTILINE)
        self.end_hide_marker_pattern = re.compile(r"^#\|\s*$", re.MULTILINE)
        # Inline code pattern (same as markdown parser)
        self.inline_code_pattern = re.compile(r"<%(?:=)?\s*(.*?)\s*%>")

    def parse(self, content: str, source_path: str | None = None) -> Document:
        """Parse a Python literate document from text content."""
        metadata = self._extract_metadata(content)
        blocks = self._parse_blocks(content)

        doc = Document(blocks=blocks, metadata=metadata)
        if source_path:
            from pathlib import Path

            doc.source_path = Path(source_path)

        return doc

    def _extract_metadata(self, content: str) -> DocumentMetadata:
        """Extract YAML frontmatter from initial comment block."""
        yaml_lines, _ = self._scan_frontmatter(content.split("\n"))
        if not yaml_lines:
            return DocumentMetadata()
        try:
            data = yaml.safe_load("\n".join(yaml_lines)) or {}
            return DocumentMetadata.from_dict(data)
        except yaml.YAMLError:
            return DocumentMetadata()

    def _parse_blocks(self, content: str) -> list[MarkdownBlock | CodeBlock]:
        """Parse content into blocks of markdown and code."""
        blocks: list[MarkdownBlock | CodeBlock] = []
        lines = content.split("\n")

        # Skip YAML frontmatter lines so they don't appear as markdown content
        _, start_line = self._scan_frontmatter(lines)

        md_lines: list[str] = []
        code_lines: list[str] = []
        in_hide_block = False
        hide_block_start_line = 0
        code_start_line = 0

        def flush_markdown(line_num: int) -> None:
            """Append accumulated markdown lines as a block, then clear."""
            if md_lines:
                content = "\n".join(md_lines)
                if content.strip():
                    blocks.append(MarkdownBlock(content, line_num - len(md_lines)))
                md_lines.clear()

        def flush_code(hidden: bool, start_line: int) -> None:
            """Append accumulated code lines as a block, then clear."""
            if code_lines:
                content = "\n".join(code_lines)
                if not self._is_empty_code(content):
                    blocks.append(
                        CodeBlock(
                            content=content,
                            hidden=hidden,
                            line_number=start_line,
                        )
                    )
                code_lines.clear()

        for line_num, line in enumerate(lines, start=1):
            if line_num < start_line:
                continue

            # Check for hide markers
            if self.hide_marker_pattern.match(line):
                flush_markdown(line_num)
                flush_code(hidden=False, start_line=code_start_line)
                in_hide_block = True
                hide_block_start_line = line_num + 1
                continue

            if self.end_hide_marker_pattern.match(line) and in_hide_block:
                flush_code(hidden=True, start_line=hide_block_start_line)
                in_hide_block = False
                continue

            # Check if line is a markdown comment
            markdown_match = self.markdown_line_pattern.match(line)

            if markdown_match:
                flush_code(hidden=in_hide_block, start_line=code_start_line)
                md_lines.append(markdown_match.group(1))
            else:
                flush_markdown(line_num)
                if not code_lines:
                    code_start_line = line_num
                code_lines.append(line)

        # Flush any remaining content
        flush_markdown(len(lines) + 1)
        flush_code(hidden=in_hide_block, start_line=code_start_line)

        return blocks

    def _scan_frontmatter(self, lines: list[str]) -> tuple[list[str], int]:
        """
        Scan for YAML frontmatter in initial comment lines.

        @param lines: All lines of the document.
        @return: (yaml_lines, first_content_line). yaml_lines is empty if
                 no valid frontmatter is found. first_content_line is the
                 1-based line number where content after frontmatter begins
                 (1 if no frontmatter).
        """
        yaml_lines: list[str] = []
        in_frontmatter = False

        for line_num, line in enumerate(lines, start=1):
            if line.strip().startswith("#'"):
                markdown_content = line[2:].lstrip()
                if markdown_content.strip() == "---":
                    if not in_frontmatter:
                        in_frontmatter = True
                        continue
                    else:
                        # Found closing ---
                        return yaml_lines, line_num + 1
                elif not in_frontmatter:
                    return [], 1
                else:
                    yaml_lines.append(markdown_content)
            elif in_frontmatter and not line.strip().startswith("#"):
                return [], 1
            elif not in_frontmatter and line.strip():
                return [], 1

        return [], 1

    def _is_empty_code(self, code: str) -> bool:
        """
        Check if Python code is empty (only whitespace, comments, or blank lines).

        @param code: The Python code content to check
        @return: True if code is effectively empty
        """
        lines = code.split("\n")
        return all(line.strip() == "" or line.strip().startswith("#") for line in lines)

    def extract_inline_code(self, text: str) -> list[InlineCode]:
        """Extract inline code from markdown text."""
        inline_codes = []
        for match in self.inline_code_pattern.finditer(text):
            full_match = match.group(0)
            code = match.group(1)
            is_statement = not full_match.startswith("<%=")
            inline_codes.append(
                InlineCode(
                    expression=code,
                    is_statement=is_statement,
                )
            )
        return inline_codes


_parser = PythonLiterateParser()


def parse(content: str, source_path: str | None = None) -> Document:
    """Parse a Nhandu Python literate document."""
    return _parser.parse(content, source_path)
