# Copyright 2026 Terrene Foundation
# SPDX-License-Identifier: Apache-2.0
"""D/T/R positional addressing -- the grammar engine for PACT organizational structure.

Implements the PACT addressing scheme where every entity has a globally unique
positional address encoding both containment and accountability
(e.g., ``D1-R1-D1-R1-T1-R1``). The core invariant is that every D or T segment
must be immediately followed by exactly one R segment.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum

from kailash.trust.pact.exceptions import PactError

logger = logging.getLogger(__name__)

__all__ = [
    "Address",
    "AddressSegment",
    "NodeType",
    "AddressError",
    "GrammarError",
]


class NodeType(str, Enum):
    """The three PACT node types."""

    DEPARTMENT = "D"
    TEAM = "T"
    ROLE = "R"


class AddressError(PactError, ValueError):
    """Base error for address parsing/validation.

    Inherits from both ``PactError`` (PACT error hierarchy) and
    ``ValueError`` (backward compatibility).
    """

    pass


class GrammarError(AddressError):
    """The D/T/R grammar constraint was violated."""

    pass


@dataclass(frozen=True)
class AddressSegment:
    """A single segment of a positional address (e.g., D1, R2, T3)."""

    node_type: NodeType
    sequence: int  # 1-based within parent scope

    def __str__(self) -> str:
        return f"{self.node_type.value}{self.sequence}"

    @classmethod
    def parse(cls, s: str) -> AddressSegment:
        """Parse 'D1', 'R2', 'T3' etc.

        Args:
            s: A segment string like 'D1' or 'R2'. Case-insensitive for the type char.

        Returns:
            An AddressSegment with validated node_type and sequence.

        Raises:
            AddressError: If the segment is malformed, has an invalid type, or
                has an invalid/non-positive sequence number.
        """
        if not s or len(s) < 2:
            raise AddressError(f"Invalid segment: {s!r}")
        type_char = s[0].upper()
        try:
            node_type = NodeType(type_char)
        except ValueError:
            raise AddressError(f"Invalid node type '{type_char}' in segment {s!r}")
        try:
            seq = int(s[1:])
        except ValueError:
            raise AddressError(f"Invalid sequence number in segment {s!r}")
        if seq < 1:
            raise AddressError(f"Sequence must be >= 1, got {seq}")
        return cls(node_type=node_type, sequence=seq)


@dataclass(frozen=True)
class Address:
    """A PACT positional address encoding containment and accountability.

    Format: D1-R1-D3-R1-T1-R1

    Grammar constraint: every D or T must be immediately followed by exactly one R.
    """

    segments: tuple[AddressSegment, ...]

    def __str__(self) -> str:
        return "-".join(str(s) for s in self.segments)

    def __len__(self) -> int:
        return len(self.segments)

    @property
    def depth(self) -> int:
        """Number of segments."""
        return len(self.segments)

    @property
    def parent(self) -> Address | None:
        """Address with the last segment removed, or None if root-level.

        Note: The returned parent is a structural parent (segments[:-1]).
        It is NOT grammar-validated because intermediate structural
        addresses (like ``D1``) are valid as containment references even
        though they cannot stand alone as complete addresses.
        """
        if len(self.segments) <= 1:
            return None
        return Address(segments=self.segments[:-1])

    @property
    def containment_unit(self) -> Address | None:
        """The nearest ancestor D or T address (the containing unit).

        Walks backward through segments to find the nearest Department or Team.
        Returns None if no D or T exists in the address (e.g., standalone Role).
        """
        for i in range(len(self.segments) - 1, -1, -1):
            if self.segments[i].node_type in (NodeType.DEPARTMENT, NodeType.TEAM):
                return Address(segments=self.segments[: i + 1])
        return None

    @property
    def accountability_chain(self) -> list[Address]:
        """All R segments in order -- the chain of accountable people.

        Returns a list of Address objects, each truncated to the position
        of a Role segment, representing the accountability chain from
        root to leaf.
        """
        result = []
        for i, seg in enumerate(self.segments):
            if seg.node_type == NodeType.ROLE:
                result.append(Address(segments=self.segments[: i + 1]))
        return result

    @property
    def last_segment(self) -> AddressSegment:
        """The final segment of this address."""
        return self.segments[-1]

    def is_prefix_of(self, other: Address) -> bool:
        """True if self is a proper prefix of other (strictly shorter)."""
        if len(self.segments) >= len(other.segments):
            return False
        return other.segments[: len(self.segments)] == self.segments

    def is_ancestor_of(self, other: Address) -> bool:
        """True if self is a prefix of or equal to other (reflexive)."""
        if len(self.segments) > len(other.segments):
            return False
        return other.segments[: len(self.segments)] == self.segments

    def ancestors(self) -> list[Address]:
        """All ancestor addresses from root to parent (not including self)."""
        result = []
        for i in range(1, len(self.segments)):
            result.append(Address(segments=self.segments[:i]))
        return result

    @staticmethod
    def lowest_common_ancestor(addr_a: Address, addr_b: Address) -> Address | None:
        """Find the lowest common ancestor of two addresses in the D/T/R tree.

        Walks both accountability chains (role positions only) and finds the
        deepest shared ancestor. Returns None if the addresses share no common
        role ancestor (e.g., entirely disjoint trees).

        The accountability chain is used rather than raw segment prefixes because
        governance authority flows through roles, not through structural D/T nodes.

        Args:
            addr_a: First address.
            addr_b: Second address.

        Returns:
            The deepest Address that appears in both accountability chains,
            or None if no common ancestor exists.
        """
        # Build a set of string representations for addr_a's accountability chain
        ancestors_a = set(str(a) for a in addr_a.accountability_chain)
        # Walk addr_b's accountability chain from deepest to shallowest
        for ancestor in reversed(addr_b.accountability_chain):
            if str(ancestor) in ancestors_a:
                return ancestor
        return None

    @classmethod
    def parse(cls, s: str) -> Address:
        """Parse an address string like 'D1-R1-D3-R1-T1-R1'.

        Validates the D/T/R grammar constraint: every D or T must be
        immediately followed by exactly one R.

        Args:
            s: Hyphen-separated address string (e.g., 'D1-R1-T1-R1').

        Returns:
            A validated Address.

        Raises:
            AddressError: If the string is empty or a segment is malformed.
            GrammarError: If the D/T/R grammar is violated.
        """
        if not s or not s.strip():
            raise AddressError("Address string is empty")

        parts = s.strip().split("-")
        segments = tuple(AddressSegment.parse(p) for p in parts)

        _validate_grammar(segments)

        return cls(segments=segments)

    @classmethod
    def from_segments(cls, *segments: AddressSegment) -> Address:
        """Create from segments, validating grammar.

        Args:
            *segments: One or more AddressSegment instances.

        Returns:
            A validated Address.

        Raises:
            AddressError: If no segments are provided.
            GrammarError: If the D/T/R grammar is violated.
        """
        _validate_grammar(segments)
        return cls(segments=tuple(segments))


def _validate_grammar(
    segments: tuple[AddressSegment, ...] | list[AddressSegment],
) -> None:
    """Validate D/T/R grammar: every D or T must be immediately followed by R.

    State machine:
      State 0 (initial/after-R): Accept D, T, R
      State 1 (after D or T): Must see R next

    Args:
        segments: The ordered sequence of address segments to validate.

    Raises:
        AddressError: If segments is empty.
        GrammarError: If any D or T is not immediately followed by R,
            or if the address ends with an unmatched D or T.
    """
    if not segments:
        raise AddressError("Address must have at least one segment")

    state = 0  # 0 = expecting any, 1 = must see R

    for i, seg in enumerate(segments):
        if state == 1:
            # After D/T, must see R
            if seg.node_type != NodeType.ROLE:
                prev = segments[i - 1]
                raise GrammarError(
                    f"Grammar violation at position {i}: {prev.node_type.name} "
                    f"at position {i - 1} must be immediately followed by R (Role), "
                    f"but found {seg.node_type.name}. Every Department or Team "
                    f"must have an accountable person (head Role)."
                )
            state = 0
        else:
            # State 0: accept anything
            if seg.node_type in (NodeType.DEPARTMENT, NodeType.TEAM):
                state = 1  # next must be R
            # R keeps us in state 0

    # If we end in state 1, the last D/T has no R
    if state == 1:
        last = segments[-1]
        raise GrammarError(
            f"Grammar violation: address ends with {last.node_type.name} "
            f"without a head Role. Every Department or Team must be "
            f"immediately followed by a Role."
        )
