"""PostgreSQLAnalyzer -- regex-based extraction of UKO triples from DDL.

Parses PostgreSQL DDL content and extracts:

- Table declarations (``uko-data:Table``).
- Column definitions with type, nullability, and primary key
  (``uko-data:Column``).
- Foreign key constraints (``uko-data:ForeignKey``).
- View declarations (``uko-data:View``).
- Schema declarations and qualified-name detection
  (``uko-data:Schema``).

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

- Layer 0 core: ``uko:contains``
- Layer 1 data: ``uko-data:Table``, ``uko-data:Column``,
  ``uko-data:View``, ``uko-data:Schema``, ``uko-data:ForeignKey``

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

Based on ``docs/specification.md`` section 42151-42272 ACMS Extensions --
PostgreSQLAnalyzer.
"""

from __future__ import annotations

import logging

from cleveragents.domain.models.acms._postgresql_helpers import (
    CREATE_SCHEMA_RE,
    CREATE_TABLE_RE,
    CREATE_VIEW_RE,
    extract_body,
    find_unquoted_semicolon,
    ident_pair,
    parse_table_body,
    schema_uri,
    strip_sql_comments,
    table_uri,
    view_uri,
)
from cleveragents.domain.models.acms.analyzers import AnalyzerProtocol, UKOTriple

__all__ = ["PostgreSQLAnalyzer"]

logger = logging.getLogger(__name__)

# Maximum DDL content size in bytes (S1 fix — DoS guard).
_MAX_DDL_BYTES = 5_242_880  # 5 MiB


class PostgreSQLAnalyzer:
    """Regex-based PostgreSQL DDL analyzer producing UKO triples.

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

    .. note::

       The ``uko-data:Database`` container is intentionally omitted
       because DDL files do not inherently carry database context.
       Provenance is added by ``UKOIndexer`` (section 43205), not by
       analyzers.

    Example::

        analyzer = PostgreSQLAnalyzer()
        triples = analyzer.analyze(
            "CREATE TABLE users (id SERIAL PRIMARY KEY);\\n",
            "db/migrations/001.sql",
        )
    """

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

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

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

        Args:
            content: Raw DDL/SQL text.
            resource_uri: Canonical URI of the resource.

        Returns:
            List of ``UKOTriple`` instances.  Returns an empty list when
            no DDL statements are detected.

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

        # Size guard: reject oversized inputs (S1 fix).
        if len(content.encode("utf-8")) > _MAX_DDL_BYTES:
            logger.warning(
                "PostgreSQLAnalyzer: content exceeds %d byte limit; skipping '%s'",
                _MAX_DDL_BYTES,
                resource_uri,
            )
            return []

        content = strip_sql_comments(content)

        triples: list[UKOTriple] = []
        schemas_seen: set[str] = set()
        tables_seen: set[str] = set()

        try:
            triples.extend(self._extract_schemas(content, resource_uri, schemas_seen))
            triples.extend(
                self._extract_tables(content, resource_uri, schemas_seen, tables_seen)
            )
            triples.extend(self._extract_views(content, resource_uri, schemas_seen))
        except Exception:
            logger.warning(
                "PostgreSQLAnalyzer: parse error in '%s'; returning partial results",
                resource_uri,
                exc_info=True,
            )

        return triples

    # -- Schema extraction ----------------------------------------------------

    def _extract_schemas(
        self,
        content: str,
        resource_uri: str,
        schemas_seen: set[str],
    ) -> list[UKOTriple]:
        """Extract triples for explicit ``CREATE SCHEMA`` statements."""
        triples: list[UKOTriple] = []

        for match in CREATE_SCHEMA_RE.finditer(content):
            s_name = (ident_pair(match, 1, 2) or "").lower()
            if s_name in schemas_seen:
                continue
            schemas_seen.add(s_name)
            s_uri = schema_uri(resource_uri, s_name)

            triples.append(
                UKOTriple(
                    subject_uri=s_uri,
                    predicate="rdf:type",
                    object_uri="uko-data:Schema",
                )
            )
            triples.append(
                UKOTriple(
                    subject_uri=s_uri,
                    predicate="rdfs:label",
                    object_value=s_name,
                )
            )

        return triples

    def _ensure_schema(
        self,
        schema_name: str,
        resource_uri: str,
        schemas_seen: set[str],
        triples: list[UKOTriple],
    ) -> str:
        """Ensure a schema node exists and return its URI."""
        schema_lower = schema_name.lower()
        s_uri = schema_uri(resource_uri, schema_lower)

        if schema_lower not in schemas_seen:
            schemas_seen.add(schema_lower)
            triples.append(
                UKOTriple(
                    subject_uri=s_uri,
                    predicate="rdf:type",
                    object_uri="uko-data:Schema",
                )
            )
            triples.append(
                UKOTriple(
                    subject_uri=s_uri,
                    predicate="rdfs:label",
                    object_value=schema_lower,
                )
            )

        return s_uri

    # -- Table extraction -----------------------------------------------------

    def _extract_tables(
        self,
        content: str,
        resource_uri: str,
        schemas_seen: set[str],
        tables_seen: set[str] | None = None,
    ) -> list[UKOTriple]:
        """Extract triples for ``CREATE TABLE`` statements."""
        triples: list[UKOTriple] = []
        if tables_seen is None:
            tables_seen = set()

        for match in CREATE_TABLE_RE.finditer(content):
            s_name = ident_pair(match, 1, 2) or ""
            t_name = ident_pair(match, 3, 4) or ""
            schema_lower = s_name.lower() if s_name else ""
            t_uri = table_uri(resource_uri, t_name, schema_lower)

            table_key = f"{schema_lower}.{t_name.lower()}"
            if table_key in tables_seen:
                continue
            tables_seen.add(table_key)

            triples.append(
                UKOTriple(
                    subject_uri=t_uri,
                    predicate="rdf:type",
                    object_uri="uko-data:Table",
                )
            )
            triples.append(
                UKOTriple(
                    subject_uri=t_uri,
                    predicate="rdfs:label",
                    object_value=t_name,
                )
            )

            if s_name:
                s_uri = self._ensure_schema(s_name, resource_uri, schemas_seen, triples)
                triples.append(
                    UKOTriple(
                        subject_uri=s_uri,
                        predicate="uko:contains",
                        object_uri=t_uri,
                    )
                )

            body = extract_body(content, match.end() - 1)
            if body:
                triples.extend(
                    parse_table_body(body, resource_uri, t_name, t_uri, schema_lower)
                )

        return triples

    # -- View extraction ------------------------------------------------------

    def _extract_views(
        self,
        content: str,
        resource_uri: str,
        schemas_seen: set[str],
    ) -> list[UKOTriple]:
        """Extract triples for ``CREATE VIEW`` statements.

        Emits ``uko-data:viewDefinition`` with the SQL body of the view
        (spec section 42263-42266).
        """
        triples: list[UKOTriple] = []

        for match in CREATE_VIEW_RE.finditer(content):
            s_name = ident_pair(match, 1, 2) or ""
            schema_lower = s_name.lower() if s_name else ""
            v_name = ident_pair(match, 3, 4) or ""
            v_uri = view_uri(resource_uri, v_name, schema_lower)

            triples.append(
                UKOTriple(
                    subject_uri=v_uri,
                    predicate="rdf:type",
                    object_uri="uko-data:View",
                )
            )
            triples.append(
                UKOTriple(
                    subject_uri=v_uri,
                    predicate="rdfs:label",
                    object_value=v_name,
                )
            )

            as_start = match.end()
            semi_pos = find_unquoted_semicolon(content, as_start)
            view_sql = (
                content[as_start:semi_pos].strip()
                if semi_pos != -1
                else content[as_start:].strip()
            )
            if view_sql:
                triples.append(
                    UKOTriple(
                        subject_uri=v_uri,
                        predicate="uko-data:viewDefinition",
                        object_value=view_sql,
                    )
                )

            if s_name:
                s_uri = self._ensure_schema(s_name, resource_uri, schemas_seen, triples)
                triples.append(
                    UKOTriple(
                        subject_uri=s_uri,
                        predicate="uko:contains",
                        object_uri=v_uri,
                    )
                )

        return triples


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