"""Base UKO vocabulary types for Layer 3 technology extensions.

Provides the foundational Pydantic models used by all Layer 3
vocabulary modules (``layer3_py``, ``layer3_ts``, ``layer3_rs``,
``layer3_java``):

- ``ProvenanceInfo``    -- Provenance contract (5 required fields)
- ``UKOClass``          -- OWL class definition
- ``UKOProperty``       -- OWL property definition
- ``Layer2Dependency``  -- Declared dependency on a Layer 2 URI
- ``UKOVocabulary``     -- Container for a vocabulary's classes,
                           properties, and layer-2 dependencies

Utility functions:

- ``build_detail_level_map``  -- Merge parent + child level insertions
- ``resolve_detail_level``    -- Walk the inheritance chain for a name

Based on ``docs/specification.md`` ~lines 42501-42513 (provenance)
and ~lines 24923-24952 (DetailLevelMap).
"""

from __future__ import annotations

from datetime import UTC, datetime

from pydantic import BaseModel, ConfigDict, Field, field_validator

from cleveragents.acms.uko.detail_level_maps import OO_DETAIL_LEVEL_MAP
from cleveragents.acms.uko.vocabularies import (
    UKO_FUNC_IRI as UKO_FUNC_IRI,
)
from cleveragents.acms.uko.vocabularies import (
    UKO_OO_IRI as UKO_OO_IRI,
)
from cleveragents.acms.uko.vocabularies import (
    _validate_http_uri,
)

__all__: list[str] = [
    "Layer2Dependency",
    "ProvenanceInfo",
    "UKOClass",
    "UKOProperty",
    "UKOVocabulary",
    "build_detail_level_map",
    "resolve_detail_level",
]


# ---------------------------------------------------------------------------
# Shared namespace constants (used by all Layer 3 modules)
# ---------------------------------------------------------------------------

UKO_CODE_IRI = "https://cleveragents.ai/ontology/uko/code#"
"""Layer 1 general code domain namespace."""


# ---------------------------------------------------------------------------
# Shared OO effective levels (parent map for all Layer 3 vocabularies)
# ---------------------------------------------------------------------------

OO_EFFECTIVE_LEVELS: tuple[tuple[str, int], ...] = tuple(
    (name, depth)
    for name, depth in sorted(OO_DETAIL_LEVEL_MAP.levels.items(), key=lambda x: x[1])
)
"""The uko-oo: effective map (spec lines 24991-25004).

Used as the parent map for all Layer 3 technology vocabularies that
extend the Object-Oriented paradigm.
"""


# ---------------------------------------------------------------------------
# ProvenanceInfo
# ---------------------------------------------------------------------------


class ProvenanceInfo(BaseModel):
    """Provenance contract for a UKO node.

    Every UKO node carries provenance back to the originating resource.

    Based on ``docs/specification.md`` ~lines 42501-42511.

    Attributes:
        source_resource: URI or ULID of the originating resource.
        source_path: File path within the resource (e.g.
            ``"src/auth/manager.py"``).
        source_range: Line range in ``"startLine:startCol-endLine:endCol"``
            format (e.g. ``"15:1-87:0"``).
        valid_from: UTC timestamp when this node became valid.
        is_current: Whether this is the current (non-superseded) version.
    """

    source_resource: str = Field(
        ...,
        min_length=1,
        description="URI or ULID of the originating resource.",
    )
    source_path: str = Field(
        ...,
        min_length=1,
        description="File path within the resource.",
    )
    source_range: str = Field(
        default="",
        description=(
            'Line range "startLine:startCol-endLine:endCol" (e.g. "15:1-87:0").'
        ),
    )
    valid_from: datetime = Field(
        default_factory=lambda: datetime.now(tz=UTC),
        description="UTC timestamp when this node became valid.",
    )
    is_current: bool = Field(
        default=True,
        description="Whether this is the current version.",
    )

    @field_validator("source_resource", mode="before")
    @classmethod
    def _validate_source_resource(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("source_resource must not be whitespace-only.")
        return value

    @field_validator("source_path", mode="before")
    @classmethod
    def _validate_source_path(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("source_path must not be whitespace-only.")
        return value

    model_config = ConfigDict(frozen=True, str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# UKOClass
# ---------------------------------------------------------------------------


class UKOClass(BaseModel):
    """An OWL class definition within a UKO vocabulary.

    Attributes:
        uri: Full IRI of the class (e.g.
            ``"https://cleveragents.ai/ontology/uko/py#PythonClass"``).
        label: Human-readable label.
        comment: Description of the class.
        subclass_of: Tuple of parent class URIs.
        provenance: Optional provenance metadata for this node.
    """

    uri: str = Field(..., min_length=1, description="Full IRI of the class.")
    label: str = Field(..., min_length=1, description="Human-readable label.")
    comment: str = Field(default="", description="Description of the class.")
    subclass_of: tuple[str, ...] = Field(
        default=(),
        description="Parent class URIs (rdfs:subClassOf).",
    )
    provenance: ProvenanceInfo | None = Field(
        default=None, description="Provenance metadata for this node."
    )

    @field_validator("uri", mode="before")
    @classmethod
    def _validate_uri(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("uri must not be whitespace-only.")
        if isinstance(value, str) and value.strip():
            _validate_http_uri(value, field_name="uri")
        return value

    @field_validator("label", mode="before")
    @classmethod
    def _validate_label(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("label must not be whitespace-only.")
        return value

    model_config = ConfigDict(frozen=True, str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# UKOProperty
# ---------------------------------------------------------------------------


class UKOProperty(BaseModel):
    """An OWL property definition within a UKO vocabulary.

    Attributes:
        uri: Full IRI of the property.
        label: Human-readable label.
        comment: Description of the property.
        domain: Tuple of class URIs this property applies to.
        range_uri: Object type or datatype URI.
        is_object_property: True for owl:ObjectProperty, False for
            owl:DatatypeProperty.
        provenance: Optional provenance metadata for this node.
    """

    uri: str = Field(..., min_length=1, description="Full IRI of the property.")
    label: str = Field(..., min_length=1, description="Human-readable label.")
    comment: str = Field(default="", description="Description of the property.")
    domain: tuple[str, ...] = Field(
        default=(), description="Class URIs this property applies to."
    )
    range_uri: str = Field(default="", description="Object type or datatype URI.")
    is_object_property: bool = Field(
        default=True,
        description="True for owl:ObjectProperty, False for DatatypeProperty.",
    )
    provenance: ProvenanceInfo | None = Field(
        default=None, description="Provenance metadata for this node."
    )

    @field_validator("uri", mode="before")
    @classmethod
    def _validate_uri(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("uri must not be whitespace-only.")
        if isinstance(value, str) and value.strip():
            _validate_http_uri(value, field_name="uri")
        return value

    @field_validator("label", mode="before")
    @classmethod
    def _validate_label(cls, value: str) -> str:
        if isinstance(value, str) and not value.strip():
            raise ValueError("label must not be whitespace-only.")
        return value

    model_config = ConfigDict(frozen=True, str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# Layer2Dependency
# ---------------------------------------------------------------------------


class Layer2Dependency(BaseModel):
    """A declared dependency on a parent-layer URI.

    Documents which parent-layer classes/properties this Layer 3
    vocabulary references via ``rdfs:subClassOf`` or
    ``rdfs:subPropertyOf``.  Most dependencies target Layer 2
    (``uko-oo:``, ``uko-func:``, ``uko-proc:``), but some target
    Layer 1 (``uko-code:``) when a class directly extends a Layer 1
    type (e.g. ``PythonModule`` extends ``uko-code:Module``).

    Attributes:
        uri: The parent-layer URI depended upon.
        layer: The UKO layer number (1-3).
        prefix: The parent-layer prefix (e.g. ``"uko-oo:"``,
            ``"uko-code:"``).
        description: What this URI represents.
    """

    uri: str = Field(..., min_length=1, description="Parent-layer URI depended upon.")
    layer: int = Field(default=2, ge=1, le=3, description="UKO layer number.")
    prefix: str = Field(
        ..., min_length=1, description='Parent-layer prefix (e.g. "uko-oo:").'
    )
    description: str = Field(default="", description="What this URI represents.")

    model_config = ConfigDict(frozen=True, str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# UKOVocabulary
# ---------------------------------------------------------------------------


class UKOVocabulary(BaseModel):
    """Container for a UKO vocabulary's classes, properties, and deps.

    Each Layer 3 vocabulary module exposes a single ``UKOVocabulary``
    instance that describes the OWL/Turtle definitions, Layer 2
    dependencies, and DetailLevelMap insertions.

    Attributes:
        namespace: Full IRI namespace for this vocabulary.
        prefix: Short prefix (e.g. ``"uko-py:"``).
        layer: UKO layer number (3 for technology-specific).
        label: Human-readable name.
        comment: Description.
        parent_prefixes: Tuple of parent Layer 2 prefixes extended.
        classes: Tuple of OWL class definitions.
        properties: Tuple of OWL property definitions.
        layer2_dependencies: Tuple of Layer 2 URIs depended on.
        inserted_levels: Mapping of level name to insertion description.
        provenance: Optional provenance metadata for this node.
    """

    namespace: str = Field(..., min_length=1, description="Full IRI namespace.")
    prefix: str = Field(..., min_length=1, description='Short prefix (e.g. "uko-py:").')
    layer: int = Field(default=3, ge=0, description="UKO layer number.")
    label: str = Field(..., min_length=1, description="Human-readable name.")
    comment: str = Field(default="", description="Description.")
    parent_prefixes: tuple[str, ...] = Field(
        default=(),
        description="Parent Layer 2 prefixes extended.",
    )
    classes: tuple[UKOClass, ...] = Field(
        default=(), description="OWL class definitions."
    )
    properties: tuple[UKOProperty, ...] = Field(
        default=(), description="OWL property definitions."
    )
    layer2_dependencies: tuple[Layer2Dependency, ...] = Field(
        default=(), description="Layer 2 URIs depended on."
    )
    inserted_levels: tuple[tuple[str, str], ...] = Field(
        default=(),
        description="Tuple of (level_name, description) for inserted levels.",
    )
    provenance: ProvenanceInfo | None = Field(
        default=None, description="Provenance metadata for this node."
    )

    model_config = ConfigDict(frozen=True, str_strip_whitespace=True)


# ---------------------------------------------------------------------------
# DetailLevelMap helpers
# ---------------------------------------------------------------------------


def build_detail_level_map(
    parent_levels: tuple[tuple[str, int], ...],
    insertions: tuple[tuple[str, int], ...],
) -> tuple[tuple[str, int], ...]:
    """Merge parent levels with child insertions, re-numbering.

    Combines parent named levels with child-inserted levels and
    produces a new consecutive integer-numbered map.  Insertions
    specify a position *after which* the new level is placed in
    the ordering derived from sorting all entries by their original
    integer.

    The algorithm:

    1. Combine parent and insertion entries into one list.
    2. Sort by the original integer value (stable sort preserves
       insertion order for ties — insertions at the same depth
       appear after the parent entry at that depth).
    3. Re-number from 0 with consecutive integers.

    Args:
        parent_levels: Tuple of ``(name, original_depth)`` from the
            parent map.
        insertions: Tuple of ``(name, original_depth)`` for new levels
            to insert.  The ``original_depth`` indicates the sort
            position: the new level is placed after all existing
            entries at depths <= this value and before entries at
            depths > this value.

    Returns:
        Tuple of ``(name, new_depth)`` with consecutive integers
        starting at 0.

    Raises:
        ValueError: If *parent_levels* or *insertions* contain
            duplicate names.
    """
    all_names: list[str] = [n for n, _ in parent_levels] + [n for n, _ in insertions]
    seen: set[str] = set()
    for name in all_names:
        if name in seen:
            raise ValueError(f"Duplicate level name: {name!r}")
        seen.add(name)

    # Tag entries: parent entries get tag=0, insertions get tag=1
    # so insertions sort after same-depth parent entries.
    tagged: list[tuple[int, int, str]] = []
    for name, depth in parent_levels:
        tagged.append((depth, 0, name))
    for name, depth in insertions:
        tagged.append((depth, 1, name))

    tagged.sort(key=lambda t: (t[0], t[1]))

    return tuple((name, idx) for idx, (_, _, name) in enumerate(tagged))


def resolve_detail_level(
    name: str,
    levels: tuple[tuple[str, int], ...],
    parent_levels: tuple[tuple[str, int], ...] | None = None,
) -> int:
    """Resolve a named detail level to its integer depth.

    Searches *levels* first, then *parent_levels* if provided.
    Supports 2-level lookup (child + optional parent fallback).

    In practice, full 4-layer chain resolution (Layer 3 -> Layer 2 ->
    Layer 1 -> Layer 0) is achieved at *build* time by
    ``build_detail_level_map``, which flattens the hierarchy into a
    single merged map.  This function provides an additional parent
    fallback for cases where un-merged maps are used.

    Args:
        name: Named level to resolve (e.g. ``"DECORATED_SIGNATURES"``).
        levels: Current map as tuple of ``(name, depth)`` pairs.
        parent_levels: Optional parent map for fallback resolution.

    Returns:
        Integer depth for the named level.

    Raises:
        ValueError: If *name* is not found in *levels* or
            *parent_levels*.
    """
    name = name.strip()
    if not name:
        raise ValueError("Level name must not be empty or whitespace-only.")
    for level_name, depth in levels:
        if level_name == name:
            return depth
    if parent_levels is not None:
        for level_name, depth in parent_levels:
            if level_name == name:
                return depth
    raise ValueError(f"Unknown detail level: {name!r}")
