"""MarkdownAnalyzer — heading/code-block/link extraction into UKO triples.

Parses Markdown documents and extracts:

- Document declaration (``uko-doc:Document``).
- Sections by heading level with containment hierarchy
  (``uko-doc:Section``).
- Code blocks with language annotations (``uko-code:CodeBlock``).
- Links and references (``uko:references``).
- Heading text and level properties.

All extracted elements are represented as ``UKOTriple`` instances with
``uko://`` URI schemes following the UKO ontology hierarchy:

- Layer 0 core: ``uko:contains``, ``uko:references``
- Layer 1 doc: ``uko-doc:Document``, ``uko-doc:Section``
- Layer 1 code: ``uko-code:CodeBlock``

Uses line-by-line regex parsing; no external Markdown library required.

Based on ``docs/specification.md`` ACMS Extensions — MarkdownAnalyzer.
"""

from __future__ import annotations

import logging
import re

from cleveragents.domain.models.acms.analyzers import (
    AnalyzerProtocol,
    UKOTriple,
    safe_uri_segment,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------

_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)(?:\s*#*\s*)?$")
_FENCED_OPEN_RE = re.compile(r"^```(\w*)")
_FENCED_CLOSE_RE = re.compile(r"^```\s*$")
_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")

# Maximum content size to parse (1 MiB, same cap as DockerComposeAnalyzer).
_MAX_MARKDOWN_BYTES = 1024 * 1024

# ---------------------------------------------------------------------------
# URI helpers
# ---------------------------------------------------------------------------

_safe = safe_uri_segment  # B4 fix: shared implementation


def _doc_uri(resource_uri: str) -> str:
    """Build a UKO document URI from a resource URI."""
    return f"uko://docs/document/{_safe(resource_uri)}"


def _section_uri(resource_uri: str, heading_text: str, index: int) -> str:
    """Build a UKO section URI."""
    slug = _safe(heading_text.lower())
    return f"uko://docs/section/{_safe(resource_uri)}/{slug}_{index}"


def _codeblock_uri(resource_uri: str, index: int) -> str:
    """Build a UKO code-block URI."""
    return f"uko://docs/codeblock/{_safe(resource_uri)}/{index}"


# ---------------------------------------------------------------------------
# MarkdownAnalyzer
# ---------------------------------------------------------------------------


class MarkdownAnalyzer:
    """Markdown document analyzer producing UKO triples.

    Satisfies :class:`AnalyzerProtocol`.  Handles ``.md`` and
    ``.markdown`` files.  Performs line-by-line parsing for headings,
    fenced code blocks, and Markdown links.

    Example::

        analyzer = MarkdownAnalyzer()
        triples = analyzer.analyze("# Hello\\nWorld\\n", "docs/readme.md")
    """

    @property
    def supported_extensions(self) -> frozenset[str]:
        """File extensions handled by this analyzer."""
        return frozenset({".md", ".markdown", ".mdx"})

    @property
    def domain(self) -> str:
        """Human-readable domain label."""
        return "markdown"

    def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]:
        """Parse *content* as Markdown and extract UKO triples.

        Args:
            content: Raw Markdown text.
            resource_uri: Canonical URI of the resource.

        Returns:
            List of ``UKOTriple`` instances.

        Raises:
            ValueError: If *content* or *resource_uri* is empty.
        """
        if not content or not content.strip():
            raise ValueError("content must not be empty.")
        if not resource_uri:
            raise ValueError("resource_uri must not be empty.")
        if len(content.encode("utf-8", errors="replace")) > _MAX_MARKDOWN_BYTES:
            logger.warning(
                "Markdown content exceeds %d bytes; truncating",
                _MAX_MARKDOWN_BYTES,
            )
            content = content[:_MAX_MARKDOWN_BYTES]

        triples: list[UKOTriple] = []
        doc_uri = _doc_uri(resource_uri)

        # Document declaration
        triples.append(
            UKOTriple(
                subject_uri=doc_uri,
                predicate="rdf:type",
                object_uri="uko-doc:Document",
            )
        )

        lines = content.splitlines()
        section_idx = 0
        codeblock_idx = 0
        in_code_block = False
        # Stack of (heading_level, section_uri) for containment
        section_stack: list[tuple[int, str]] = []

        for line in lines:
            # -- Fenced code block tracking --
            if in_code_block:
                if _FENCED_CLOSE_RE.match(line):
                    in_code_block = False
                continue

            fence_match = _FENCED_OPEN_RE.match(line)
            if fence_match and line.strip() != "```":
                # Opening fence with or without language
                in_code_block = True
                codeblock_idx += 1
                cb_uri = _codeblock_uri(resource_uri, codeblock_idx)
                lang = fence_match.group(1)

                triples.append(
                    UKOTriple(
                        subject_uri=cb_uri,
                        predicate="rdf:type",
                        object_uri="uko-code:CodeBlock",
                    )
                )

                # Containment: inside nearest section or document
                parent = section_stack[-1][1] if section_stack else doc_uri
                triples.append(
                    UKOTriple(
                        subject_uri=parent,
                        predicate="uko:contains",
                        object_uri=cb_uri,
                    )
                )

                if lang:
                    triples.append(
                        UKOTriple(
                            subject_uri=cb_uri,
                            predicate="uko-code:language",
                            object_value=lang,
                        )
                    )
                continue

            # Bare ``` that opens a fence without language
            if line.strip() == "```":
                in_code_block = True
                codeblock_idx += 1
                cb_uri = _codeblock_uri(resource_uri, codeblock_idx)

                triples.append(
                    UKOTriple(
                        subject_uri=cb_uri,
                        predicate="rdf:type",
                        object_uri="uko-code:CodeBlock",
                    )
                )
                parent = section_stack[-1][1] if section_stack else doc_uri
                triples.append(
                    UKOTriple(
                        subject_uri=parent,
                        predicate="uko:contains",
                        object_uri=cb_uri,
                    )
                )
                continue

            # -- Headings --
            heading_match = _HEADING_RE.match(line)
            if heading_match:
                level = len(heading_match.group(1))
                text = heading_match.group(2).strip()
                section_idx += 1
                sec_uri = _section_uri(resource_uri, text, section_idx)

                triples.append(
                    UKOTriple(
                        subject_uri=sec_uri,
                        predicate="rdf:type",
                        object_uri="uko-doc:Section",
                    )
                )
                triples.append(
                    UKOTriple(
                        subject_uri=sec_uri,
                        predicate="uko-doc:headingLevel",
                        object_value=str(level),
                    )
                )
                triples.append(
                    UKOTriple(
                        subject_uri=sec_uri,
                        predicate="uko-doc:headingText",
                        object_value=text,
                    )
                )

                # Pop sections at same or deeper level
                while section_stack and section_stack[-1][0] >= level:
                    section_stack.pop()

                # Containment
                parent = section_stack[-1][1] if section_stack else doc_uri
                triples.append(
                    UKOTriple(
                        subject_uri=parent,
                        predicate="uko:contains",
                        object_uri=sec_uri,
                    )
                )

                section_stack.append((level, sec_uri))
                continue

            # -- Links --
            for link_match in _LINK_RE.finditer(line):
                link_text = link_match.group(1)
                link_url = link_match.group(2)
                # Current context is nearest section or document
                context_uri = section_stack[-1][1] if section_stack else doc_uri
                triples.append(
                    UKOTriple(
                        subject_uri=context_uri,
                        predicate="uko:references",
                        object_value=link_url,
                        confidence=0.9,
                    )
                )
                if link_text:
                    triples.append(
                        UKOTriple(
                            subject_uri=context_uri,
                            predicate="rdfs:label",
                            object_value=link_text,
                        )
                    )

        return triples


# Protocol compliance assertion (zero-cost at import time).
_: type[AnalyzerProtocol] = MarkdownAnalyzer
