"""PythonAnalyzer — AST-based extraction of UKO triples from Python files.

Parses Python source into an ``ast`` tree and extracts:

- Module-level declarations (``uko-code:Module``).
- Class definitions with docstrings (``uko-py:Class``).
- Function and method definitions with docstrings (``uko-py:Function``).
- Import statements (``uko:references``).
- Module-level docstrings (``uko-doc:hasDocstring``).

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 code: ``uko-code:Module``
- Layer 3 Python-specific: ``uko-py:Class``, ``uko-py:Function``

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

from __future__ import annotations

import ast
import logging
import re

from cleveragents.domain.models.acms.analyzers import AnalyzerProtocol, UKOTriple

logger = logging.getLogger(__name__)

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

_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.]")


def _safe_name(name: str) -> str:
    """Sanitise a Python identifier for use in a URI path segment."""
    return _SAFE_NAME_RE.sub("_", name)


def _module_uri(resource_uri: str) -> str:
    """Build a UKO module URI from a resource URI."""
    return f"uko://code/module/{_safe_name(resource_uri)}"


def _class_uri(resource_uri: str, class_name: str) -> str:
    """Build a UKO class URI."""
    return f"uko://code/class/{_safe_name(resource_uri)}/{_safe_name(class_name)}"


def _function_uri(resource_uri: str, func_name: str, class_name: str = "") -> str:
    """Build a UKO function/method URI."""
    base = f"uko://code/function/{_safe_name(resource_uri)}"
    if class_name:
        return f"{base}/{_safe_name(class_name)}/{_safe_name(func_name)}"
    return f"{base}/{_safe_name(func_name)}"


def _import_uri(module_name: str) -> str:
    """Build a UKO import reference URI."""
    return f"uko://code/module/{_safe_name(module_name)}"


# ---------------------------------------------------------------------------
# PythonAnalyzer
# ---------------------------------------------------------------------------


class PythonAnalyzer:
    """AST-based Python source analyzer producing UKO triples.

    Satisfies :class:`AnalyzerProtocol`.  Handles ``.py`` and ``.pyi``
    files.  Gracefully returns an empty list for unparsable content.

    Example::

        analyzer = PythonAnalyzer()
        triples = analyzer.analyze("class Foo:\\n    pass\\n", "src/foo.py")
    """

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

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

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

        Args:
            content: Raw Python source code.
            resource_uri: Canonical URI of the resource.

        Returns:
            List of ``UKOTriple`` instances.  Returns an empty list on
            parse failure.

        Raises:
            ValueError: If *content* or *resource_uri* is empty.
        """
        if not content:
            raise ValueError("content must not be empty.")
        if not resource_uri:
            raise ValueError("resource_uri must not be empty.")

        try:
            tree = ast.parse(content, filename=resource_uri)
        except SyntaxError:
            logger.warning(
                "PythonAnalyzer: syntax error in '%s'; returning empty",
                resource_uri,
            )
            return []

        triples: list[UKOTriple] = []
        mod_uri = _module_uri(resource_uri)

        # Module declaration
        triples.append(
            UKOTriple(
                subject_uri=mod_uri,
                predicate="rdf:type",
                object_uri="uko-code:Module",
            )
        )

        # Module docstring
        docstring = ast.get_docstring(tree)
        if docstring:
            triples.append(
                UKOTriple(
                    subject_uri=mod_uri,
                    predicate="uko-doc:hasDocstring",
                    object_value=docstring[:500],
                )
            )

        # Walk top-level nodes
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.ClassDef):
                triples.extend(self._extract_class(node, resource_uri, mod_uri))
            elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                triples.extend(self._extract_function(node, resource_uri, mod_uri))
            elif isinstance(node, ast.Import):
                triples.extend(self._extract_import(node, mod_uri))
            elif isinstance(node, ast.ImportFrom):
                triples.extend(self._extract_import_from(node, mod_uri))

        return triples

    # -- Internal extraction helpers ------------------------------------------

    def _extract_class(
        self,
        node: ast.ClassDef,
        resource_uri: str,
        mod_uri: str,
    ) -> list[UKOTriple]:
        """Extract triples for a class definition."""
        triples: list[UKOTriple] = []
        cls_uri = _class_uri(resource_uri, node.name)

        # Type declaration
        triples.append(
            UKOTriple(
                subject_uri=cls_uri,
                predicate="rdf:type",
                object_uri="uko-py:Class",
            )
        )

        # Containment
        triples.append(
            UKOTriple(
                subject_uri=mod_uri,
                predicate="uko:contains",
                object_uri=cls_uri,
            )
        )

        # Label
        triples.append(
            UKOTriple(
                subject_uri=cls_uri,
                predicate="rdfs:label",
                object_value=node.name,
            )
        )

        # Docstring
        docstring = ast.get_docstring(node)
        if docstring:
            triples.append(
                UKOTriple(
                    subject_uri=cls_uri,
                    predicate="uko-doc:hasDocstring",
                    object_value=docstring[:500],
                )
            )

        # Base classes
        for base in node.bases:
            base_name = _base_name(base)
            if base_name:
                triples.append(
                    UKOTriple(
                        subject_uri=cls_uri,
                        predicate="uko-py:inheritsFrom",
                        object_value=base_name,
                    )
                )

        # Methods inside the class
        for child in ast.iter_child_nodes(node):
            if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
                triples.extend(
                    self._extract_method(child, resource_uri, cls_uri, node.name)
                )

        return triples

    def _extract_function(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
        resource_uri: str,
        mod_uri: str,
    ) -> list[UKOTriple]:
        """Extract triples for a module-level function."""
        triples: list[UKOTriple] = []
        func_uri = _function_uri(resource_uri, node.name)

        triples.append(
            UKOTriple(
                subject_uri=func_uri,
                predicate="rdf:type",
                object_uri="uko-py:Function",
            )
        )
        triples.append(
            UKOTriple(
                subject_uri=mod_uri,
                predicate="uko:contains",
                object_uri=func_uri,
            )
        )
        triples.append(
            UKOTriple(
                subject_uri=func_uri,
                predicate="rdfs:label",
                object_value=node.name,
            )
        )

        docstring = ast.get_docstring(node)
        if docstring:
            triples.append(
                UKOTriple(
                    subject_uri=func_uri,
                    predicate="uko-doc:hasDocstring",
                    object_value=docstring[:500],
                )
            )

        return triples

    def _extract_method(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
        resource_uri: str,
        cls_uri: str,
        class_name: str,
    ) -> list[UKOTriple]:
        """Extract triples for a method inside a class."""
        triples: list[UKOTriple] = []
        method_uri = _function_uri(resource_uri, node.name, class_name)

        triples.append(
            UKOTriple(
                subject_uri=method_uri,
                predicate="rdf:type",
                object_uri="uko-py:Function",
            )
        )
        triples.append(
            UKOTriple(
                subject_uri=cls_uri,
                predicate="uko:contains",
                object_uri=method_uri,
            )
        )
        triples.append(
            UKOTriple(
                subject_uri=method_uri,
                predicate="rdfs:label",
                object_value=node.name,
            )
        )

        docstring = ast.get_docstring(node)
        if docstring:
            triples.append(
                UKOTriple(
                    subject_uri=method_uri,
                    predicate="uko-doc:hasDocstring",
                    object_value=docstring[:500],
                )
            )

        return triples

    def _extract_import(self, node: ast.Import, mod_uri: str) -> list[UKOTriple]:
        """Extract triples for ``import X`` statements."""
        triples: list[UKOTriple] = []
        for alias in node.names:
            triples.append(
                UKOTriple(
                    subject_uri=mod_uri,
                    predicate="uko:references",
                    object_uri=_import_uri(alias.name),
                )
            )
        return triples

    def _extract_import_from(
        self, node: ast.ImportFrom, mod_uri: str
    ) -> list[UKOTriple]:
        """Extract triples for ``from X import Y`` statements."""
        triples: list[UKOTriple] = []
        module_name = node.module or ""
        if module_name:
            triples.append(
                UKOTriple(
                    subject_uri=mod_uri,
                    predicate="uko:references",
                    object_uri=_import_uri(module_name),
                )
            )
        return triples


# ---------------------------------------------------------------------------
# ast helpers
# ---------------------------------------------------------------------------


def _base_name(node: ast.expr) -> str:
    """Extract a human-readable base class name from an AST node."""
    if isinstance(node, ast.Name):
        return node.id
    if isinstance(node, ast.Attribute):
        value_name = _base_name(node.value)
        if value_name:
            return f"{value_name}.{node.attr}"
        return node.attr
    return ""


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