"""Types for encrypted shared context.

Defines the semantic types, access scopes, and data structures used by
:class:`~skytale_sdk.context.SharedContext`.

Usage::

    from skytale_sdk.context_types import ContextType, ContextEntry, AccessScope

    entry = ContextEntry(
        key="task_status",
        value={"phase": "running"},
        context_type=ContextType.STATE_UPDATE,
        scope=AccessScope.all_members(),
    )
"""

from __future__ import annotations

import os
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, List, Optional


class ContextType(str, Enum):
    """Semantic types for context entries.

    Example::

        >>> ContextType.ARTIFACT.value
        'artifact'
    """

    STATE_UPDATE = "state_update"
    ARTIFACT = "artifact"
    HANDOFF = "handoff"
    MEMORY = "memory"


@dataclass(frozen=True)
class AccessScope:
    """Defines who can read a context entry.

    Use the factory methods :meth:`all_members`, :meth:`specific` to create
    scopes rather than constructing directly.

    Example::

        >>> scope = AccessScope.all_members()
        >>> scope.access
        'all'
    """

    access: str
    ids: tuple[str, ...] = ()

    @staticmethod
    def all_members() -> AccessScope:
        """All channel members can read this entry.

        Example::

            >>> AccessScope.all_members().access
            'all'
        """
        return AccessScope(access="all")

    @staticmethod
    def specific(member_ids: List[str]) -> AccessScope:
        """Only the listed members can read this entry.

        Args:
            member_ids: List of DID URIs that should have access.

        Example::

            >>> scope = AccessScope.specific(["did:key:z6Mk..."])
            >>> scope.ids
            ('did:key:z6Mk...',)
        """
        return AccessScope(access="members", ids=tuple(member_ids))

    def to_dict(self) -> dict:
        """Serialize to wire format.

        Example::

            >>> AccessScope.all_members().to_dict()
            {'access': 'all'}
        """
        d: dict = {"access": self.access}
        if self.ids:
            d["ids"] = list(self.ids)
        return d

    @staticmethod
    def from_dict(d: dict) -> AccessScope:
        """Deserialize from wire format.

        Args:
            d: Dict with ``access`` and optional ``ids`` keys.

        Example::

            >>> AccessScope.from_dict({"access": "all"}).access
            'all'
        """
        return AccessScope(
            access=d.get("access", "all"),
            ids=tuple(d.get("ids", ())),
        )


@dataclass
class HLC:
    """Hybrid Logical Clock for conflict resolution.

    Combines wall-clock time with a logical counter and node ID
    to produce globally unique, monotonically increasing timestamps.

    Example::

        >>> hlc = HLC(node_id="aabb")
        >>> hlc.tick()
        >>> hlc.wall_ms > 0
        True
    """

    wall_ms: int = 0
    counter: int = 0
    node_id: str = field(default_factory=lambda: os.urandom(4).hex())

    def tick(self) -> None:
        """Advance the clock for a local event.

        Example::

            >>> hlc = HLC(node_id="0011")
            >>> hlc.tick()
            >>> hlc.counter >= 0
            True
        """
        now_ms = int(time.time() * 1000)
        if now_ms > self.wall_ms:
            self.wall_ms = now_ms
            self.counter = 0
        else:
            self.counter += 1

    def update(self, remote_wall: int, remote_counter: int) -> None:
        """Merge with a remote clock value (receive event).

        Args:
            remote_wall: Remote wall-clock milliseconds.
            remote_counter: Remote logical counter.

        Example::

            >>> hlc = HLC(node_id="0011")
            >>> hlc.update(1000, 5)
            >>> hlc.wall_ms >= 1000
            True
        """
        now_ms = int(time.time() * 1000)
        if now_ms > self.wall_ms and now_ms > remote_wall:
            self.wall_ms = now_ms
            self.counter = 0
        elif remote_wall > self.wall_ms:
            self.wall_ms = remote_wall
            self.counter = remote_counter + 1
        elif self.wall_ms > remote_wall:
            self.counter += 1
        else:
            self.counter = max(self.counter, remote_counter) + 1

    def to_list(self) -> list:
        """Serialize to wire format ``[wall_ms, counter, node_hex]``.

        Example::

            >>> HLC(wall_ms=1000, counter=1, node_id="ab").to_list()
            [1000, 1, 'ab']
        """
        return [self.wall_ms, self.counter, self.node_id]

    @staticmethod
    def from_list(data: list) -> HLC:
        """Deserialize from wire format.

        Args:
            data: List of ``[wall_ms, counter, node_hex]``.

        Example::

            >>> HLC.from_list([1000, 1, "ab"]).wall_ms
            1000
        """
        return HLC(wall_ms=data[0], counter=data[1], node_id=data[2])

    def __gt__(self, other: HLC) -> bool:
        if self.wall_ms != other.wall_ms:
            return self.wall_ms > other.wall_ms
        if self.counter != other.counter:
            return self.counter > other.counter
        return self.node_id > other.node_id

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, HLC):
            return NotImplemented
        return (
            self.wall_ms == other.wall_ms
            and self.counter == other.counter
            and self.node_id == other.node_id
        )


@dataclass
class ContextEntry:
    """A single entry in the shared context store.

    Attributes:
        key: Entry key.
        value: Entry value (any JSON-serializable data).
        context_type: Semantic type of the entry.
        scope: Access control scope.
        hlc: Hybrid logical clock timestamp.
        ttl_ms: Optional time-to-live in milliseconds.
        created_at: Wall-clock time when the entry was created (ms).

    Example::

        >>> entry = ContextEntry(key="status", value="running")
        >>> entry.is_expired()
        False
    """

    key: str
    value: Any
    context_type: ContextType = ContextType.STATE_UPDATE
    scope: AccessScope = field(default_factory=AccessScope.all_members)
    hlc: HLC = field(default_factory=HLC)
    ttl_ms: Optional[int] = None
    created_at: int = field(default_factory=lambda: int(time.time() * 1000))

    def is_expired(self) -> bool:
        """Check if this entry has expired based on TTL.

        Example::

            >>> ContextEntry(key="k", value="v").is_expired()
            False
        """
        if self.ttl_ms is None:
            return False
        return int(time.time() * 1000) - self.created_at > self.ttl_ms
