"""Temporal Data Model service for revision-aware RDF with 3 storage tiers.

Orchestrates temporal node lifecycle: creating revisions, marking nodes
as historical, querying by temporal scope and storage tier, and
traversing revision chains.  All operations are delegated to an
injected :class:`TemporalBackend` implementation.

Based on ``docs/specification.md`` lines 42468--42499 (Temporal Data
Model, Revision-Aware RDF, Three Storage Tiers).
"""

from __future__ import annotations

from datetime import UTC, datetime

import structlog

from cleveragents.domain.models.acms._validation import (
    validate_non_blank as _validate_non_blank,
)
from cleveragents.domain.models.acms.temporal import (
    RevisionChain,
    TemporalBackend,
    TemporalMetadata,
    TemporalNode,
    TierQueryResult,
    TierRetentionConfig,
)
from cleveragents.domain.models.acms.tiers import ContextTier
from cleveragents.domain.models.core.project import TemporalScope

__all__ = [
    "TemporalService",
]

logger = structlog.get_logger(__name__)


class TemporalService:
    """Service layer for temporal data model operations.

    Wraps a :class:`TemporalBackend` with structured logging,
    input validation, and higher-level convenience methods.

    Based on ``docs/specification.md`` lines 42468--42499.

    Args:
        backend: Injected temporal backend implementation.
        retention: Tier retention policy (defaults to spec defaults).
    """

    # INVARIANTS (must hold after every public method returns):
    # 1. _backend is never None (set in __init__).
    # 2. _retention is never None (set in __init__).
    # 3. All public methods validate string inputs for non-blank.
    # 4. All public methods log entry and result via structlog.

    def __init__(
        self,
        backend: TemporalBackend,
        retention: TierRetentionConfig | None = None,
    ) -> None:
        """Initialise the temporal service.

        Args:
            backend: Temporal backend for storage operations.
            retention: Retention policy; defaults to spec defaults
                (24h warm, 90d cold).

        Raises:
            TypeError: If *backend* is ``None``.
        """
        if backend is None:
            msg = "backend must not be None"
            raise TypeError(msg)
        self._backend = backend
        self._retention = retention or TierRetentionConfig()
        logger.info(
            "temporal_service.init",
            warm_hours=self._retention.warm_retention_hours,
            cold_days=self._retention.cold_retention_days,
        )

    @property
    def retention(self) -> TierRetentionConfig:
        """Current tier retention configuration."""
        return self._retention

    def create_revision(
        self,
        current_uri: str,
        new_node_uri: str,
        source_resource: str,
        source_path: str,
        source_range: str | None = None,
    ) -> TemporalNode:
        """Create a new revision of an existing node.

        Marks the existing node (identified by *current_uri*) as
        historical and creates a new head node with
        ``is_revision_of`` linking back.

        Args:
            current_uri: URI of the current (to-be-superseded) node.
            new_node_uri: URI for the new revision node.
            source_resource: ULID of the originating resource.
            source_path: File path within the resource.
            source_range: Optional source range.

        Returns:
            The newly created temporal node.

        Raises:
            ValueError: If any required string is empty or
                whitespace-only, or if ``current_uri == new_node_uri``.
            KeyError: If *current_uri* does not exist in the backend.
        """
        _validate_non_blank(current_uri, "current_uri")
        _validate_non_blank(new_node_uri, "new_node_uri")
        _validate_non_blank(source_resource, "source_resource")
        _validate_non_blank(source_path, "source_path")
        if current_uri == new_node_uri:
            msg = (
                "new_node_uri must differ from current_uri; "
                "same URI would overwrite the historical record"
            )
            raise ValueError(msg)

        log = logger.bind(
            current_uri=current_uri,
            new_node_uri=new_node_uri,
        )
        log.info("temporal_service.create_revision.start")

        timestamp = datetime.now(tz=UTC)
        new_node = TemporalNode(
            node_uri=new_node_uri,
            source_resource=source_resource,
            source_path=source_path,
            source_range=source_range,
            temporal=TemporalMetadata(
                valid_from=timestamp,
                is_current=True,
                is_revision_of=current_uri,
            ),
        )

        result = self._backend.create_revision(
            current_uri=current_uri,
            new_node=new_node,
            timestamp=timestamp,
        )

        log.info(
            "temporal_service.create_revision.done",
            new_uri=result.node_uri,
        )
        return result

    def store_initial_node(
        self,
        node_uri: str,
        source_resource: str,
        source_path: str,
        source_range: str | None = None,
    ) -> TemporalNode:
        """Store the first version of a temporal node (no predecessor).

        This is used when a resource is first analysed and has no
        prior revision history.

        Args:
            node_uri: URI for the new node.
            source_resource: ULID of the originating resource.
            source_path: File path within the resource.
            source_range: Optional source range.

        Returns:
            The stored temporal node.

        Raises:
            ValueError: If any required string is empty or
                whitespace-only.
        """
        _validate_non_blank(node_uri, "node_uri")
        _validate_non_blank(source_resource, "source_resource")
        _validate_non_blank(source_path, "source_path")

        log = logger.bind(node_uri=node_uri)
        log.info("temporal_service.store_initial_node.start")

        timestamp = datetime.now(tz=UTC)
        node = TemporalNode(
            node_uri=node_uri,
            source_resource=source_resource,
            source_path=source_path,
            source_range=source_range,
            temporal=TemporalMetadata(
                valid_from=timestamp,
                is_current=True,
                is_revision_of=None,
            ),
        )

        self._backend.store_node(node)

        log.info("temporal_service.store_initial_node.done")
        return node

    def get_current(self, node_uri_base: str) -> TemporalNode | None:
        """Get the current version of a node.

        Hot-tier access pattern: only ``isCurrent = true`` nodes.

        Args:
            node_uri_base: Base URI pattern to match.

        Returns:
            The current node, or ``None`` if not found.

        Raises:
            ValueError: If *node_uri_base* is empty or whitespace-only.
        """
        _validate_non_blank(node_uri_base, "node_uri_base")
        log = logger.bind(node_uri_base=node_uri_base)
        log.debug("temporal_service.get_current")
        result = self._backend.get_current(node_uri_base)
        log.debug(
            "temporal_service.get_current.done",
            found=result is not None,
        )
        return result

    def get_history(
        self,
        node_uri_base: str,
        temporal_scope: TemporalScope = TemporalScope.ALL,
    ) -> tuple[TemporalNode, ...]:
        """Get node versions filtered by temporal scope.

        Args:
            node_uri_base: Base URI pattern to match.
            temporal_scope: Scope controlling which versions.

        Returns:
            Matching nodes ordered by ``valid_from`` descending.

        Raises:
            ValueError: If *node_uri_base* is empty or whitespace-only.
        """
        _validate_non_blank(node_uri_base, "node_uri_base")
        log = logger.bind(
            node_uri_base=node_uri_base,
            temporal_scope=temporal_scope.value,
        )
        log.debug("temporal_service.get_history")
        result = self._backend.get_history(
            node_uri_base, temporal_scope, self._retention
        )
        log.debug(
            "temporal_service.get_history.done",
            count=len(result),
        )
        return result

    def get_revision_chain(self, node_uri: str) -> RevisionChain:
        """Get the full revision chain for a node.

        Args:
            node_uri: URI of any node in the chain.

        Returns:
            The complete revision chain.

        Raises:
            ValueError: If *node_uri* is empty or whitespace-only.
            KeyError: If *node_uri* does not exist.
        """
        _validate_non_blank(node_uri, "node_uri")
        log = logger.bind(node_uri=node_uri)
        log.debug("temporal_service.get_revision_chain")
        chain = self._backend.get_revision_chain(node_uri)
        log.debug(
            "temporal_service.get_revision_chain.done",
            depth=chain.depth,
        )
        return chain

    def query_by_tier(
        self,
        tier: ContextTier,
        temporal_scope: TemporalScope | None = None,
    ) -> TierQueryResult:
        """Query nodes aligned to a specific storage tier.

        If *temporal_scope* is ``None``, uses the tier's natural
        alignment from the spec:

        | Tier | Default Scope |
        |------|---------------|
        | HOT  | CURRENT       |
        | WARM | RECENT        |
        | COLD | ALL           |

        Args:
            tier: Storage tier to query.
            temporal_scope: Optional override for temporal scope.

        Returns:
            A :class:`TierQueryResult` with matching nodes.
        """
        scope = _tier_default_scope(tier) if temporal_scope is None else temporal_scope

        log = logger.bind(tier=tier.value, temporal_scope=scope.value)
        log.debug("temporal_service.query_by_tier")
        result = self._backend.query_by_tier(tier, scope, self._retention)
        log.debug(
            "temporal_service.query_by_tier.done",
            node_count=len(result.nodes),
        )
        return result

    def mark_historical(
        self,
        node_uri: str,
    ) -> TemporalNode:
        """Mark a node as historical (no longer current).

        Sets ``valid_until`` to now and ``is_current`` to ``False``.

        Args:
            node_uri: URI of the node to mark.

        Returns:
            The updated node.

        Raises:
            ValueError: If *node_uri* is empty or whitespace-only,
                or if the node is already historical.
            KeyError: If *node_uri* does not exist.
        """
        _validate_non_blank(node_uri, "node_uri")
        log = logger.bind(node_uri=node_uri)
        log.info("temporal_service.mark_historical")
        timestamp = datetime.now(tz=UTC)
        result = self._backend.mark_historical(node_uri, timestamp)
        log.info("temporal_service.mark_historical.done")
        return result


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _tier_default_scope(tier: ContextTier) -> TemporalScope:
    """Return the spec-defined default temporal scope for a tier.

    Based on ``docs/specification.md`` lines 42493--42499.
    """
    if tier == ContextTier.HOT:
        return TemporalScope.CURRENT
    if tier == ContextTier.WARM:
        return TemporalScope.RECENT
    return TemporalScope.ALL
