"""Policy Engine — Default Deny policy evaluation for CyberBrain26.

Core principle: DEFAULT DENY. Nothing sensitive is allowed just because an agent wants it.

Policy modes:
- SAFE: no risky automatic execution, sensitive operations require approval, limited tools
- FULL: higher automation, read-only operations may be automatic, risky write/execute require control
- RESEARCH: experimental models, detailed logging, sandbox, limited network
- RED_TEAM_LAB: isolated test environment, synthetic credentials, no production access
"""

from __future__ import annotations

import ast
import logging
import operator
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from cyberbrain26.security_plane.actions import (
    SENSITIVE_ACTIONS as CANONICAL_SENSITIVE_ACTIONS,
)
from cyberbrain26.security_plane.actions import (
    is_registered_action,
    normalize_action,
)
from cyberbrain26.security_plane.audit import AuditEventType, AuditLog
from cyberbrain26.security_plane.identity import AgentIdentity, TrustLevel

logger = logging.getLogger(__name__)


class PolicyDecision(str, Enum):
    """Outcome of a policy evaluation."""

    ALLOW = "allow"
    DENY = "deny"
    REQUIRE_APPROVAL = "require_approval"


class PolicyMode(str, Enum):
    """System-wide policy modes."""

    SAFE = "safe"
    FULL = "full"
    RESEARCH = "research"
    RED_TEAM_LAB = "red_team_lab"


@dataclass
class PolicyContext:
    """Context in which a policy decision is being made."""

    action: str
    resource: str | None = None
    environment: dict[str, Any] = field(default_factory=dict)
    risk_score: float = 0.0
    mode: PolicyMode = PolicyMode.SAFE
    timestamp: float | None = None


@dataclass
class PolicyRule:
    """A single policy rule."""

    name: str
    action_pattern: str
    decision: PolicyDecision
    condition: str | None = None
    description: str = ""
    reason: str = ""


@dataclass
class PolicyEvaluation:
    """Result of a policy evaluation."""

    decision: PolicyDecision
    rule: str | None = None
    reason: str = ""
    requires_approval: bool = False
    context: PolicyContext | None = None


class PolicyConditionError(ValueError):
    """Neplatný nebo nedovolený výraz policy podmínky."""


MAX_CONDITION_LENGTH = 256
MAX_CONDITION_NODES = 50
ALLOWED_CONDITION_NAMES = frozenset(
    {"action", "identity", "context", "risk_score", "mode",
     "is_sensitive", "SENSITIVE_ACTIONS"}
)
ALLOWED_CONDITION_CALLABLES = frozenset({"is_sensitive"})

_COMPARATORS: dict[type[ast.AST], Callable[[Any, Any], bool]] = {
    ast.Eq: operator.eq,
    ast.NotEq: operator.ne,
    ast.Lt: operator.lt,
    ast.LtE: operator.le,
    ast.Gt: operator.gt,
    ast.GtE: operator.ge,
    ast.In: lambda a, b: operator.contains(b, a),
    ast.NotIn: lambda a, b: not operator.contains(b, a),
    ast.Is: operator.is_,
    ast.IsNot: operator.is_not,
}

_ALLOWED_NODES = (
    ast.Expression, ast.BoolOp, ast.And, ast.Or, ast.UnaryOp, ast.Not,
    ast.Compare, ast.Constant, ast.Name, ast.Load, ast.Call,
) + tuple(_COMPARATORS)


def _validate_condition_ast(tree: ast.AST) -> None:
    """Allowlistová kontrola AST — cokoliv mimo povolené uzly je odmítnuto."""
    for node in ast.walk(tree):
        if not isinstance(node, _ALLOWED_NODES):
            raise PolicyConditionError(f"disallowed expression node: {type(node).__name__}")


def _eval_ast_node(node: ast.AST, env: dict[str, object]) -> object:
    if isinstance(node, ast.Constant):
        return node.value
    if isinstance(node, ast.Name):
        if node.id not in ALLOWED_CONDITION_NAMES or node.id not in env:
            raise PolicyConditionError(f"name not allowed: {node.id}")
        return env[node.id]
    if isinstance(node, ast.Call):
        if not isinstance(node.func, ast.Name) or node.func.id not in ALLOWED_CONDITION_CALLABLES:
            raise PolicyConditionError("call not allowed")
        if node.keywords or getattr(node, "starargs", None):
            raise PolicyConditionError("call arguments not allowed")
        target = env[node.func.id]
        if not callable(target):
            raise PolicyConditionError("callable not allowed")
        return target(*[_eval_ast_node(a, env) for a in node.args])
    if isinstance(node, ast.BoolOp):
        values = [_eval_ast_node(v, env) for v in node.values]
        return all(values) if isinstance(node.op, ast.And) else any(values)
    if isinstance(node, ast.UnaryOp):
        if not isinstance(node.op, ast.Not):
            raise PolicyConditionError("unary operator not allowed")
        return not _eval_ast_node(node.operand, env)
    if isinstance(node, ast.Compare):
        left = _eval_ast_node(node.left, env)
        for op, comparator in zip(node.ops, node.comparators):
            func = _COMPARATORS.get(type(op))
            if func is None:
                raise PolicyConditionError("comparison not allowed")
            right = _eval_ast_node(comparator, env)
            if not func(left, right):
                return False
            left = right
        return True
    raise PolicyConditionError(f"disallowed expression node: {type(node).__name__}")


def evaluate_condition(condition: str, env: dict[str, object]) -> object:
    """Bezpečné vyhodnocení policy podmínky nad allowlistou AST."""
    if len(condition) > MAX_CONDITION_LENGTH:
        raise PolicyConditionError("condition too long")
    try:
        tree = ast.parse(condition, mode="eval")
    except SyntaxError as exc:
        raise PolicyConditionError("invalid condition syntax") from exc
    if sum(1 for _ in ast.walk(tree)) > MAX_CONDITION_NODES:
        raise PolicyConditionError("condition too complex")
    _validate_condition_ast(tree)
    return _eval_ast_node(tree.body, env)


class PolicyEngine:
    """Default Deny Policy Engine.

    Evaluates whether an agent identity is permitted to perform an action
    in a given context. Defaults to DENY unless an explicit rule allows it.

    Usage:
        engine = PolicyEngine()
        engine.add_rule(PolicyRule("allow_read", "read_file", PolicyDecision.ALLOW))
        result = engine.evaluate(identity, "read_file", "/etc/passwd")
    """

    # Canonical classification, owned by ``security_plane.actions`` so this
    # engine and the permission gateway classify one vocabulary.
    SENSITIVE_ACTIONS: frozenset[str] = CANONICAL_SENSITIVE_ACTIONS

    def __init__(self, mode: PolicyMode = PolicyMode.SAFE, audit_log: AuditLog | None = None) -> None:
        self.mode = mode
        self.audit_log = audit_log
        self._rules: list[PolicyRule] = []
        self._load_default_rules()

    def _load_default_rules(self) -> None:
        """Load the default rule set for the current mode."""
        self._rules = [
            PolicyRule(
                name="default_deny",
                action_pattern="*",
                decision=PolicyDecision.DENY,
                reason="No matching allow rule found — default deny",
            ),
        ]

        if self.mode == PolicyMode.SAFE:
            self._rules.insert(
                0,
                PolicyRule(
                    name="safe_mode_non_sensitive_allowed",
                    action_pattern="*",
                    decision=PolicyDecision.ALLOW,
                    condition="not is_sensitive(action)",
                    description="SAFE mode: non-sensitive actions are allowed by default",
                ),
            )
            self._rules.insert(
                0,
                PolicyRule(
                    name="safe_mode_sensitive_requires_approval",
                    action_pattern="*",
                    decision=PolicyDecision.REQUIRE_APPROVAL,
                    condition="is_sensitive(action)",
                    description="SAFE mode: all sensitive actions require human approval",
                ),
            )
        else:
            # FULL, RESEARCH, RED_TEAM_LAB: default deny, explicit allow rules only
            pass

    def add_rule(self, rule: PolicyRule) -> None:
        """Add a policy rule. Rules are evaluated in order; first match wins."""
        self._rules.insert(0, rule)

    def remove_rule(self, name: str) -> bool:
        """Remove a rule by name. Returns True if found."""
        for i, rule in enumerate(self._rules):
            if rule.name == name:
                self._rules.pop(i)
                return True
        return False

    def list_rules(self) -> list[PolicyRule]:
        """Return all registered rules."""
        return list(self._rules)

    def evaluate(
        self,
        identity: AgentIdentity,
        action: str,
        resource: str | None = None,
        context: PolicyContext | None = None,
    ) -> PolicyEvaluation:
        """Evaluate whether an identity can perform an action.

        Args:
            identity: The agent identity requesting the action.
            action: The action being requested (e.g. "read_file", "execute_shell").
            resource: Optional resource being accessed.
            context: Optional evaluation context (mode, risk_score, environment).

        Returns:
            PolicyEvaluation with decision, rule, reason, and approval flag.

        Security rationale: an unregistered action is denied before any rule is
        evaluated. Rules such as SAFE's ``not is_sensitive(action)`` and
        non-SAFE's describe the *known* vocabulary; applied to
        a name outside it they would turn an invented action into an allowed one.
        The action is canonicalized first so a cosmetic variant cannot select a
        different rule than the action it claims to be.
        """
        canonical_action = normalize_action(action)
        ctx = context or PolicyContext(action=canonical_action, resource=resource, mode=self.mode)

        # Unknown action names are denied before any rule can match them.
        if not is_registered_action(canonical_action):
            evaluation = PolicyEvaluation(
                decision=PolicyDecision.DENY,
                rule="unregistered_action",
                reason=(
                    f"Action '{canonical_action or '<empty>'}' is not a registered action "
                    "— denied by default"
                ),
                context=ctx,
            )
            self._audit_decision(identity, canonical_action, resource, evaluation)
            return evaluation

        # Check trust level first — untrusted agents are always denied
        if identity.trust_level == TrustLevel.UNTRUSTED:
            evaluation = PolicyEvaluation(
                decision=PolicyDecision.DENY,
                rule="trust_check",
                reason=f"Agent {identity.agent_id} is UNTRUSTED — denied by trust policy",
                context=ctx,
            )
            self._audit_decision(identity, canonical_action, resource, evaluation)
            return evaluation

        # Evaluate rules in order; first match wins
        for rule in self._rules:
            if self._rule_matches(rule, canonical_action, identity, ctx):
                requires_approval = rule.decision == PolicyDecision.REQUIRE_APPROVAL
                evaluation = PolicyEvaluation(
                    decision=rule.decision,
                    rule=rule.name,
                    reason=rule.reason or rule.description,
                    requires_approval=requires_approval,
                    context=ctx,
                )
                self._audit_decision(identity, canonical_action, resource, evaluation)
                return evaluation

        # Fallback: default deny
        evaluation = PolicyEvaluation(
            decision=PolicyDecision.DENY,
            rule="default_deny",
            reason="No matching rule — default deny applied",
            context=ctx,
        )
        self._audit_decision(identity, canonical_action, resource, evaluation)
        return evaluation

    def _audit_decision(
        self,
        identity: AgentIdentity,
        action: str,
        resource: str | None,
        evaluation: PolicyEvaluation,
    ) -> None:
        """Record a POLICY_DECISION audit event.

        RED_TEAM_LAB mode always logs every decision.
        Other modes log DENY and REQUIRE_APPROVAL decisions.
        """
        if self.audit_log is None:
            return

        should_log = self.mode == PolicyMode.RED_TEAM_LAB or evaluation.decision in (
            PolicyDecision.DENY,
            PolicyDecision.REQUIRE_APPROVAL,
        )
        if not should_log:
            return

        self.audit_log.record(
            AuditEventType.POLICY_DECISION,
            actor=identity.agent_id,
            agent_id=identity.agent_id,
            action=action,
            resource=resource or "",
            policy_decision=evaluation.decision.value,
            result=evaluation.rule or "",
        )

    def is_sensitive(self, action: str) -> bool:
        """Check whether an action is classified as sensitive.

        The name is canonicalized first, because this predicate is reachable
        from policy conditions; without normalization a whitespace or case
        variant would evaluate as non-sensitive and skip the approval gate.
        """
        return normalize_action(action) in self.SENSITIVE_ACTIONS

    def _rule_matches(
        self,
        rule: PolicyRule,
        action: str,
        identity: AgentIdentity,
        context: PolicyContext,
    ) -> bool:
        """Check whether a rule applies to the given action and context."""
        if rule.action_pattern not in ("*", action):
            return False

        # Check condition if present
        if rule.condition:
            try:
                cond_result = self._eval_condition(rule.condition, action, identity, context)
            except (SyntaxError, NameError, TypeError, ValueError, AttributeError, MemoryError, RecursionError) as exc:
                logger.warning("Condition %r evaluation failed: %s", rule.condition, exc)
                return False
            if not cond_result:
                return False

        return True

    def _eval_condition(
        self,
        condition: str,
        action: str,
        identity: AgentIdentity,
        context: PolicyContext,
    ) -> bool:
        """Evaluate a simple condition expression."""
        local_vars: dict[str, object] = {
            "action": action,
            "identity": identity,
            "context": context,
            "risk_score": context.risk_score,
            "mode": context.mode,
            "is_sensitive": self.is_sensitive,
            "SENSITIVE_ACTIONS": self.SENSITIVE_ACTIONS,
        }
        value = evaluate_condition(condition, local_vars)
        if not isinstance(value, bool):
            raise PolicyConditionError("condition must evaluate to bool")
        return value


__all__ = [
    "PolicyContext",
    "PolicyDecision",
    "PolicyEngine",
    "PolicyEvaluation",
    "PolicyMode",
    "PolicyRule",
]