"""Earned Agency — trust-tiered behavioral gating (AD-357)."""

from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from typing import Any
from probos.crew_profile import Rank


class AgencyLevel(str, Enum):
    """What an agent is permitted to do at its current trust tier."""
    REACTIVE = "reactive"           # Ensign: responds only when @mentioned
    SUGGESTIVE = "suggestive"       # Lieutenant: participates in own department
    AUTONOMOUS = "autonomous"       # Commander: full Ward Room participation
    UNRESTRICTED = "unrestricted"   # Senior: cross-department, mentoring (future)


class InitiativeLevel(int, Enum):
    """Graduated initiative scale (AD-674).

    Combines rank-based gates with trust-based graduation.
    Higher levels = more autonomous behavior.

    Level 0: DIRECTED     — Only acts on explicit instructions
    Level 1: RESPONSIVE   — Responds to @mentions and direct assignments
    Level 2: CONTRIBUTORY — Participates in department discussions
    Level 3: PROACTIVE    — Self-initiates within department scope
    Level 4: STRATEGIC    — Cross-department coordination, mentoring
    """

    DIRECTED = 0
    RESPONSIVE = 1
    CONTRIBUTORY = 2
    PROACTIVE = 3
    STRATEGIC = 4


@dataclass(frozen=True)
class UncertaintyContext:
    """Captures confidence factors for initiative calibration (AD-675)."""

    oracle_confidence: float = 1.0
    health_confidence: float = 1.0
    data_freshness: float = 1.0

    @property
    def aggregate_confidence(self) -> float:
        """Minimum confidence across all factors."""
        return min(self.oracle_confidence, self.health_confidence, self.data_freshness)


class RecallTier(str, Enum):
    """Memory recall capability tier — mapped from Earned Agency rank (AD-462c)."""
    BASIC = "basic"            # Vector similarity only, small budget
    ENHANCED = "enhanced"      # Vector + keyword + salience weights, standard budget
    FULL = "full"              # Full recall_weighted + recall_by_anchor, large budget
    ORACLE = "oracle"          # All recall paths + Oracle Service (AD-462e)


def recall_tier_from_rank(rank: Rank) -> RecallTier:
    """Map rank to recall capability tier."""
    return {
        Rank.ENSIGN: RecallTier.BASIC,
        Rank.LIEUTENANT: RecallTier.ENHANCED,
        Rank.COMMANDER: RecallTier.FULL,
        Rank.SENIOR: RecallTier.ORACLE,
    }[rank]


@dataclass(frozen=True)
class ClearanceGrant:
    """AD-622: Temporary elevated access record.

    Captain-issued, time-limited, scoped, revocable.
    SAP analog for project/duty-based elevated recall access.
    """
    id: str                         # UUID
    target_agent_id: str            # Who receives the grant (sovereign ID)
    recall_tier: RecallTier         # Granted tier level
    scope: str = "general"          # "general" | "project:{name}" | "investigation:{id}"
    reason: str = ""                # Justification (audit trail)
    issued_by: str = "captain"      # Issuer identity
    issued_at: float = 0.0          # Timestamp
    expires_at: float | None = None # None = until revoked
    revoked: bool = False           # Soft-delete
    revoked_at: float | None = None # When revoked (audit trail)


# AD-620: Ordering map — RecallTier is a str enum so we need explicit
# numeric ordering for comparison.
_TIER_ORDER: dict[RecallTier, int] = {
    RecallTier.BASIC: 0,
    RecallTier.ENHANCED: 1,
    RecallTier.FULL: 2,
    RecallTier.ORACLE: 3,
}


def effective_recall_tier(
    rank: Rank | None,
    billet_clearance: str = "",
    grants: Sequence[ClearanceGrant] = (),
) -> RecallTier:
    """AD-620/622: Resolve effective recall tier — max(rank-based, billet-based, grant-based).

    Billet clearance comes from the Post.clearance field in organization.yaml.
    Grants come from active ClearanceGrants issued by the Captain.
    Takes the highest of all three sources.
    """
    rank_tier = recall_tier_from_rank(rank) if rank else RecallTier.ENHANCED

    if not billet_clearance:
        best = rank_tier
    else:
        try:
            billet_tier = RecallTier(billet_clearance.lower())
        except ValueError:
            billet_tier = rank_tier
        best = billet_tier if _TIER_ORDER.get(billet_tier, 0) > _TIER_ORDER.get(rank_tier, 0) else rank_tier

    # AD-622: Apply active grants
    for grant in grants:
        grant_order = _TIER_ORDER.get(grant.recall_tier, 0)
        if grant_order > _TIER_ORDER.get(best, 0):
            best = grant.recall_tier

    return best


def resolve_billet_clearance(
    agent_type: str,
    ontology: Any | None,
) -> str:
    """AD-620: Look up billet clearance for an agent type from the ontology.

    Returns the Post.clearance string, or "" if ontology unavailable or
    agent has no post assignment.
    """
    if not ontology:
        return ""
    try:
        post = ontology.get_post_for_agent(agent_type)
        return post.clearance if post else ""
    except Exception:
        return ""


def resolve_active_grants(
    agent_id: str,
    grant_store: Any | None,
) -> list[ClearanceGrant]:
    """AD-622: Look up active grants for an agent.

    Returns empty list if grant store unavailable. Law of Demeter —
    callers don't reach through runtime to query grants directly.

    NOTE: Synchronous — grant_store.get_active_grants_sync() reads
    from an in-memory cache. See ClearanceGrantStore for cache details.
    """
    if not grant_store:
        return []
    try:
        return grant_store.get_active_grants_sync(agent_id)
    except Exception:
        return []


def proficiency_promotion_eligibility(
    *,
    profile: Any,
    next_rank: Rank,
    pcc_floor: int = 3,
    role_floor: int = 3,
) -> dict[str, Any]:
    """AD-428b v1: pure inspector for proficiency-based promotion readiness.

    Returns:
        {
            "next_rank": str,
            "pcc_floor": int,
            "role_floor": int,
            "passes": bool,
            "blockers": list[str],
            "pcc_count_at_floor": int,
            "role_count_at_floor": int,
            "required_pcc_count": int,
            "required_role_count": int,
        }

    Rank thresholds (Dreyfus mapping):
        ENSIGN -> LIEUTENANT: 2 PCCs at APPLY (3), 1 ROLE at APPLY (3)
        LIEUTENANT -> COMMANDER: 4 PCCs at APPLY (3), 2 ROLE at ENABLE (4)
        COMMANDER -> SENIOR: 6 PCCs at ENABLE (4), 3 ROLE at ADVISE (5)

    Does NOT modify Rank.from_trust(). NOT called from any existing decision
    path; v1 is inspection only. Captain or AD-428b-iv (gating consumer)
    decides whether to enforce.
    """
    blockers: list[str] = []
    if next_rank == Rank.LIEUTENANT:
        req_pcc, req_role = 2, 1
        pcc_floor, role_floor = 3, 3
    elif next_rank == Rank.COMMANDER:
        req_pcc, req_role = 4, 2
        pcc_floor, role_floor = 3, 4
    elif next_rank == Rank.SENIOR:
        req_pcc, req_role = 6, 3
        pcc_floor, role_floor = 4, 5
    else:
        # ENSIGN promotion to ENSIGN is the boot state — no gate.
        req_pcc, req_role = 0, 0

    if profile is None:
        return {
            "next_rank": next_rank.value,
            "pcc_floor": pcc_floor,
            "role_floor": role_floor,
            "passes": req_pcc == 0 and req_role == 0,
            "blockers": ["no_skill_profile"] if (req_pcc + req_role) > 0 else [],
            "pcc_count_at_floor": 0,
            "role_count_at_floor": 0,
            "required_pcc_count": req_pcc,
            "required_role_count": req_role,
        }

    pccs_ok = sum(
        1 for r in getattr(profile, "pccs", [])
        if not getattr(r, "suspended", False)
        and getattr(getattr(r, "proficiency", None), "value", 0) >= pcc_floor
    )
    role_ok = sum(
        1 for r in getattr(profile, "role_skills", [])
        if not getattr(r, "suspended", False)
        and getattr(getattr(r, "proficiency", None), "value", 0) >= role_floor
    )

    if pccs_ok < req_pcc:
        blockers.append(
            f"need {req_pcc} PCCs at level {pcc_floor}+ (have {pccs_ok})"
        )
    if role_ok < req_role:
        blockers.append(
            f"need {req_role} role skills at level {role_floor}+ (have {role_ok})"
        )

    return {
        "next_rank": next_rank.value,
        "pcc_floor": pcc_floor,
        "role_floor": role_floor,
        "passes": not blockers,
        "blockers": blockers,
        "pcc_count_at_floor": pccs_ok,
        "role_count_at_floor": role_ok,
        "required_pcc_count": req_pcc,
        "required_role_count": req_role,
    }


def agency_from_rank(rank: Rank) -> AgencyLevel:
    """Map rank to agency level."""
    return {
        Rank.ENSIGN: AgencyLevel.REACTIVE,
        Rank.LIEUTENANT: AgencyLevel.SUGGESTIVE,
        Rank.COMMANDER: AgencyLevel.AUTONOMOUS,
        Rank.SENIOR: AgencyLevel.UNRESTRICTED,
    }[rank]


def resolve_initiative_level(
    rank: "Rank",
    trust_score: float,
    *,
    thresholds: dict[str, float] | None = None,
) -> InitiativeLevel:
    """Resolve graduated initiative level from rank and trust (AD-674).

    Maps:
      Rank.ENSIGN + trust < 0.3       → DIRECTED (0)
      Rank.ENSIGN + trust ≥ 0.3       → RESPONSIVE (1)
      Rank.LIEUTENANT + trust < 0.5   → RESPONSIVE (1)
      Rank.LIEUTENANT + trust ≥ 0.5   → CONTRIBUTORY (2)
      Rank.COMMANDER + trust < 0.7    → CONTRIBUTORY (2)
      Rank.COMMANDER + trust ≥ 0.7    → PROACTIVE (3)
      Rank.SENIOR                     → STRATEGIC (4)

    The three existing gates remain authoritative for specific permissions.
    This function provides a unified scalar for initiative decisions.

    Args:
        rank: Agent's current rank
        trust_score: Agent's current trust score (0.0-1.0)
        thresholds: Optional override thresholds from config. Keys:
            "responsive" (default 0.3), "contributory" (default 0.5),
            "proactive" (default 0.7).

    Note: Rank is already imported at module level from probos.crew_profile —
    do NOT re-import from probos.config.
    """
    t = thresholds if isinstance(thresholds, dict) else {}
    t_responsive = t.get("responsive", 0.3)
    t_contributory = t.get("contributory", 0.5)
    t_proactive = t.get("proactive", 0.7)

    rank_ordinal = {
        Rank.ENSIGN: 0,
        Rank.LIEUTENANT: 1,
        Rank.COMMANDER: 2,
        Rank.SENIOR: 3,
    }.get(rank, 0)

    if rank_ordinal >= 3:
        return InitiativeLevel.STRATEGIC

    if rank_ordinal >= 2:
        if trust_score >= t_proactive:
            return InitiativeLevel.PROACTIVE
        return InitiativeLevel.CONTRIBUTORY

    if rank_ordinal >= 1:
        if trust_score >= t_contributory:
            return InitiativeLevel.CONTRIBUTORY
        return InitiativeLevel.RESPONSIVE

    if trust_score >= t_responsive:
        return InitiativeLevel.RESPONSIVE
    return InitiativeLevel.DIRECTED


def calibrate_initiative(
    base_level: "InitiativeLevel",
    confidence: float,
    *,
    low_confidence_threshold: float = 0.4,
    critical_confidence_threshold: float = 0.2,
) -> "InitiativeLevel":
    """Calibrate initiative level by confidence (AD-675).

    When confidence is low, clamp initiative downward:
    - confidence >= low_threshold: no change
    - low_threshold > confidence >= critical_threshold: clamp down 1 level
    - confidence < critical_threshold: clamp down 2 levels (min DIRECTED)

    This prevents agents from acting autonomously when the information
    driving their decisions is uncertain.

    Args:
        base_level: Initiative level from resolve_initiative_level()
        confidence: 0.0-1.0 confidence in current observations
        low_confidence_threshold: Below this, clamp down 1 level
        critical_confidence_threshold: Below this, clamp down 2 levels
    """
    if confidence >= low_confidence_threshold:
        return base_level

    current = base_level.value

    if confidence < critical_confidence_threshold:
        clamped = max(0, current - 2)
    else:
        clamped = max(0, current - 1)

    return InitiativeLevel(clamped)


def can_respond_ambient(
    rank: Rank,
    *,
    is_captain_post: bool,
    same_department: bool,
) -> bool:
    """Can this agent respond WITHOUT being @mentioned?

    Core enforcement function. Returns True if the agent is permitted
    to respond to a post it was not explicitly mentioned in.
    """
    if rank == Rank.ENSIGN:
        return False  # Ensigns only respond when @mentioned

    if rank == Rank.LIEUTENANT:
        # Lieutenants respond to Captain posts in own department only
        return is_captain_post and same_department

    if rank == Rank.COMMANDER:
        # Commanders respond to any Captain post; agent posts in own dept
        return is_captain_post or same_department

    # Senior: unrestricted ambient response
    return True


def can_think_proactively(rank: Rank) -> bool:
    """Can this agent initiate proactive thought?

    Ensigns are reactive-only — they haven't earned the trust to
    self-initiate. Everyone else can think proactively.
    """
    return rank != Rank.ENSIGN


def can_perform_action(rank: Rank, action: str) -> bool:
    """Can this agent perform a specific Ward Room action?

    AD-437/AD-485: Action space gating by rank.
    - Ensign: dm only (configurable via communications.dm_min_rank)
    - Lieutenant: endorse + reply + dm
    - Commander: endorse + reply + dm
    - Senior: endorse + reply + dm + thread management (lock, pin)
    """
    _ACTION_TIERS: dict[str, Rank] = {
        "endorse": Rank.LIEUTENANT,
        "reply": Rank.LIEUTENANT,
        "dm": Rank.ENSIGN,  # AD-485: configurable via communications.dm_min_rank
        "lock": Rank.SENIOR,
        "pin": Rank.SENIOR,
    }

    min_rank = _ACTION_TIERS.get(action)
    if min_rank is None:
        return False  # Unknown action

    # Compare ordinals
    _RANK_ORDER = [Rank.ENSIGN, Rank.LIEUTENANT, Rank.COMMANDER, Rank.SENIOR]
    return _RANK_ORDER.index(rank) >= _RANK_ORDER.index(min_rank)
