"""
Access control for cross-zone operations with grant/revoke and violation logging.

This module provides:
- AccessGrant: Model for explicit cross-zone access permissions
- ViolationLog: Audit trail for trust zone violations
- ZoneAccessManager: Manager for grants, validation, and logging

Requirements: SEC-03 (grant/revoke access), SEC-04 (access validation with logging)
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, List, Optional, Set
import uuid
import logging

from pydantic import BaseModel, Field

from .trust_zones import TrustZone


class AccessGrant(BaseModel):
    """
    Explicit permission for cross-zone access between agents.

    Used when the default hierarchical access control is insufficient.
    Users can grant access from one agent to another, optionally
    with operation restrictions and expiration.

    Attributes:
        grant_id: Unique identifier for this grant.
        source_agent_id: Agent requesting access.
        target_agent_id: Agent being accessed.
        granted_by: User ID who granted access.
        granted_at: Timestamp of grant creation (UTC).
        expires_at: Optional expiration timestamp.
        allowed_operations: Set of allowed operations (default: {"read"}).
    """

    model_config = {"extra": "forbid"}  # Reject unknown fields

    grant_id: str = Field(..., min_length=1, description="Unique grant identifier")
    source_agent_id: str = Field(..., description="Agent requesting access")
    target_agent_id: str = Field(..., description="Agent being accessed")
    granted_by: str = Field(..., min_length=1, description="User who granted access")
    granted_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc),
        description="Grant creation timestamp (UTC)",
    )
    expires_at: Optional[datetime] = Field(
        default=None,
        description="Optional expiration timestamp",
    )
    allowed_operations: Set[str] = Field(
        default_factory=lambda: {"read"},
        description="Set of allowed operations",
    )

    def is_expired(self) -> bool:
        """
        Check if grant has expired.

        Returns:
            True if grant has expired, False otherwise.
            A grant without expiration (expires_at=None) never expires.
        """
        if self.expires_at is None:
            return False
        return datetime.now(timezone.utc) > self.expires_at

    def __repr__(self) -> str:
        """String representation for debugging."""
        return (
            f"AccessGrant("
            f"grant_id={self.grant_id!r}, "
            f"source={self.source_agent_id!r}, "
            f"target={self.target_agent_id!r}, "
            f"operations={self.allowed_operations})"
        )


@dataclass
class ViolationLog:
    """
    Audit record for trust zone access violations.

    Logs all denied access attempts with full context for
    security auditing and compliance.

    Attributes:
        timestamp: ISO format timestamp of the violation.
        source_agent_id: Agent that attempted access.
        target_agent_id: Agent that was accessed.
        operation: Operation that was attempted.
        source_zone: Trust zone of source agent.
        target_zone: Trust zone of target agent.
        reason: Explanation of why access was denied.
    """

    timestamp: str  # ISO format
    source_agent_id: str
    target_agent_id: str
    operation: str
    source_zone: TrustZone
    target_zone: TrustZone
    reason: str  # Why access was denied


class ZoneAccessManager:
    """
    Manages cross-zone access grants and validation (SEC-03, SEC-04).

    Provides:
    - Agent-to-zone assignment tracking
    - Explicit grant/revoke of cross-zone access
    - Hierarchical access validation with grant override
    - Violation logging for audit trail

    Access Validation Order:
    1. Same agent always has access
    2. Check hierarchical zone access (higher zones can access lower)
    3. Check explicit grants (not expired, operation allowed)
    4. If no match, deny and log violation
    """

    def __init__(self):
        """Initialize the access manager with empty state."""
        self._grants: Dict[str, AccessGrant] = {}
        self._agent_zones: Dict[str, TrustZone] = {}
        self._violations: List[ViolationLog] = []

    def assign_agent_to_zone(self, agent_id: str, zone: TrustZone) -> None:
        """
        Assign an agent to a trust zone.

        Args:
            agent_id: Unique identifier for the agent.
            zone: Trust zone to assign the agent to.
        """
        self._agent_zones[agent_id] = zone

    def get_agent_zone(self, agent_id: str) -> Optional[TrustZone]:
        """
        Get the trust zone for an agent.

        Args:
            agent_id: Agent identifier to look up.

        Returns:
            TrustZone if assigned, None otherwise.
        """
        return self._agent_zones.get(agent_id)

    def grant_access(
        self,
        source_agent_id: str,
        target_agent_id: str,
        granted_by: str,
        allowed_operations: Optional[Set[str]] = None,
        expires_at: Optional[datetime] = None,
    ) -> AccessGrant:
        """
        Grant cross-zone access (SEC-03).

        Creates an explicit permission for source agent to access target agent,
        overriding the default hierarchical access control.

        Args:
            source_agent_id: Agent requesting access.
            target_agent_id: Agent being accessed.
            granted_by: User ID who granted access.
            allowed_operations: Set of allowed operations (default: {"read"}).
            expires_at: Optional expiration timestamp.

        Returns:
            The created AccessGrant.
        """
        grant = AccessGrant(
            grant_id=str(uuid.uuid4()),
            source_agent_id=source_agent_id,
            target_agent_id=target_agent_id,
            granted_by=granted_by,
            allowed_operations=allowed_operations or {"read"},
            expires_at=expires_at,
        )
        self._grants[grant.grant_id] = grant
        return grant

    def revoke_access(self, grant_id: str) -> bool:
        """
        Revoke cross-zone access (SEC-03).

        Args:
            grant_id: ID of the grant to revoke.

        Returns:
            True if grant was found and revoked, False if not found.
        """
        if grant_id in self._grants:
            del self._grants[grant_id]
            return True
        return False

    def validate_access(
        self,
        source_agent_id: str,
        target_agent_id: str,
        operation: str = "read",
    ) -> bool:
        """
        Validate cross-zone access (SEC-04).

        Checks access in the following order:
        1. Same agent always has access
        2. Hierarchical zone access (higher zones access lower)
        3. Explicit grants (not expired, operation allowed)

        Args:
            source_agent_id: Agent requesting access.
            target_agent_id: Agent being accessed.
            operation: Operation being attempted (default: "read").

        Returns:
            True if access is allowed, False if denied.
        """
        # Same agent always has access
        if source_agent_id == target_agent_id:
            return True

        # Get zones
        source_zone = self._agent_zones.get(source_agent_id)
        target_zone = self._agent_zones.get(target_agent_id)

        if source_zone is None or target_zone is None:
            self._log_violation(
                source_agent_id,
                target_agent_id,
                operation,
                "Agent not assigned to zone",
            )
            return False

        # Check hierarchical access (higher zones can access lower)
        if target_zone.allows_access_from(source_zone):
            return True

        # Check explicit grants
        for grant in self._grants.values():
            if (
                grant.source_agent_id == source_agent_id
                and grant.target_agent_id == target_agent_id
                and not grant.is_expired()
                and operation in grant.allowed_operations
            ):
                return True

        # Access denied - log violation
        self._log_violation(
            source_agent_id,
            target_agent_id,
            operation,
            f"No grant found and {source_zone.value} cannot access {target_zone.value}",
        )
        return False

    def _log_violation(
        self,
        source_agent_id: str,
        target_agent_id: str,
        operation: str,
        reason: str,
    ) -> None:
        """
        Log trust zone violation for audit (SEC-04).

        Creates a ViolationLog entry and also logs to Python logging.

        Args:
            source_agent_id: Agent that attempted access.
            target_agent_id: Agent that was accessed.
            operation: Operation that was attempted.
            reason: Explanation of why access was denied.
        """
        source_zone = self._agent_zones.get(source_agent_id)
        target_zone = self._agent_zones.get(target_agent_id)

        violation = ViolationLog(
            timestamp=datetime.now(timezone.utc).isoformat(),
            source_agent_id=source_agent_id,
            target_agent_id=target_agent_id,
            operation=operation,
            source_zone=source_zone or TrustZone.PUBLIC,
            target_zone=target_zone or TrustZone.PUBLIC,
            reason=reason,
        )
        self._violations.append(violation)

        # Also log to Python logging
        logger = logging.getLogger("gsd_rlm.security")
        logger.warning(
            f"Trust zone violation: source={source_agent_id} "
            f"target={target_agent_id} operation={operation} reason={reason}"
        )

    def get_violations(self) -> List[ViolationLog]:
        """
        Get all logged violations.

        Returns:
            Copy of the violations list.
        """
        return self._violations.copy()

    def get_grants_for_agent(self, agent_id: str) -> List[AccessGrant]:
        """
        Get all grants where agent is source or target.

        Args:
            agent_id: Agent identifier to search for.

        Returns:
            List of relevant AccessGrant objects.
        """
        return [
            g
            for g in self._grants.values()
            if g.source_agent_id == agent_id or g.target_agent_id == agent_id
        ]
