"""Policy engine for GreyMatter Solo.

Evaluates declarative rules against request context to decide whether
to allow, redact, block, or escalate MCP tool calls. Rules are loaded
from YAML config and share the same format as the SecureLLM Rust policy
engine — enabling seamless promotion from Solo to Enterprise.

Usage:
    engine = PolicyEngine.from_yaml(yaml_text)
    decision = engine.evaluate(EvalContext(tool="gm_observe", action="create", content="..."))
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any

import yaml


class PolicyOutcome(str, Enum):
    """What happens when a rule matches."""
    ALLOW = "allow"
    REWRITE = "rewrite"
    REDACT = "redact"
    ESCALATE = "escalate"
    REFUSE = "refuse"


@dataclass
class Condition:
    """A condition that must be true for a rule to match."""
    field: str           # e.g., "content", "tool", "action"
    operator: str        # contains, not_contains, matches, equals, starts_with, greater_than, less_than
    value: str

    def evaluate(self, field_value: str) -> bool:
        """Evaluate this condition against a field value."""
        match self.operator:
            case "contains":
                return self.value in field_value
            case "not_contains":
                return self.value not in field_value
            case "matches":
                try:
                    return bool(re.search(self.value, field_value))
                except re.error:
                    return False
            case "equals":
                return field_value == self.value
            case "starts_with":
                return field_value.startswith(self.value)
            case "greater_than":
                try:
                    return float(field_value) > float(self.value)
                except (ValueError, TypeError):
                    return False
            case "less_than":
                try:
                    return float(field_value) < float(self.value)
                except (ValueError, TypeError):
                    return False
            case _:
                return False


@dataclass
class PolicyRule:
    """A single policy rule."""
    rule_id: str
    description: str
    outcome: PolicyOutcome
    priority: int = 100
    conditions: list[Condition] = field(default_factory=list)
    projects: list[str] = field(default_factory=list)
    shadow: bool = False
    enabled: bool = True
    reason: str = ""
    modifications: dict[str, Any] = field(default_factory=dict)


@dataclass
class PolicyDecision:
    """Result of evaluating rules against a request."""
    outcome: PolicyOutcome
    rule_id: str
    reason: str
    shadow: bool = False
    modifications: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def default_allow(cls) -> PolicyDecision:
        return cls(
            outcome=PolicyOutcome.ALLOW,
            rule_id="_default",
            reason="No matching rules — default allow",
        )


@dataclass
class EvalContext:
    """Context provided to the policy engine for evaluation."""
    tool: str = ""           # MCP tool name (e.g., "gm_observe")
    action: str = ""         # Tool action (e.g., "create", "list")
    content: str = ""        # Content being processed
    project_id: str = ""     # Project identifier
    metadata: dict[str, str] = field(default_factory=dict)

    def field_value(self, name: str) -> str:
        """Get the value of a named field."""
        match name:
            case "tool":
                return self.tool
            case "action":
                return self.action
            case "content":
                return self.content
            case "project_id":
                return self.project_id
            case _:
                return self.metadata.get(name, "")


class PolicyEngine:
    """Evaluates rules against request context.

    Rules are sorted by priority (lower = higher priority).
    First matching non-shadow rule determines the outcome.
    Shadow rules are evaluated and logged but don't affect the decision.
    """

    def __init__(self, rules: list[PolicyRule] | None = None):
        self.rules = sorted(rules or [], key=lambda r: r.priority)

    def add_rule(self, rule: PolicyRule) -> None:
        self.rules.append(rule)
        self.rules.sort(key=lambda r: r.priority)

    def remove_rule(self, rule_id: str) -> bool:
        before = len(self.rules)
        self.rules = [r for r in self.rules if r.rule_id != rule_id]
        return len(self.rules) < before

    def evaluate(self, ctx: EvalContext) -> PolicyDecision:
        """Evaluate all rules. Returns first matching non-shadow decision."""
        shadow_hits: list[PolicyDecision] = []

        for rule in self.rules:
            if not rule.enabled:
                continue
            if rule.projects and ctx.project_id not in rule.projects:
                continue

            # All conditions must match (AND logic)
            matches = all(
                cond.evaluate(ctx.field_value(cond.field))
                for cond in rule.conditions
            )
            # Empty conditions = catch-all
            if not matches and rule.conditions:
                continue

            decision = PolicyDecision(
                outcome=rule.outcome,
                rule_id=rule.rule_id,
                reason=rule.reason,
                shadow=rule.shadow,
                modifications=rule.modifications,
            )

            if rule.shadow:
                shadow_hits.append(decision)
                continue

            return decision

        return PolicyDecision.default_allow()

    def evaluate_all(self, ctx: EvalContext) -> list[PolicyDecision]:
        """Evaluate and return ALL matching decisions (including shadow)."""
        decisions = []
        for rule in self.rules:
            if not rule.enabled:
                continue
            if rule.projects and ctx.project_id not in rule.projects:
                continue
            matches = all(
                cond.evaluate(ctx.field_value(cond.field))
                for cond in rule.conditions
            )
            if not matches and rule.conditions:
                continue
            decisions.append(PolicyDecision(
                outcome=rule.outcome,
                rule_id=rule.rule_id,
                reason=rule.reason,
                shadow=rule.shadow,
                modifications=rule.modifications,
            ))
        return decisions

    @classmethod
    def from_yaml(cls, yaml_text: str) -> PolicyEngine:
        """Load rules from YAML text."""
        data = yaml.safe_load(yaml_text) or {}
        raw_rules = data.get("rules", [])
        rules = []
        for r in raw_rules:
            conditions = [
                Condition(
                    field=c.get("field", ""),
                    operator=c.get("operator", "contains"),
                    value=c.get("value", ""),
                )
                for c in r.get("conditions", [])
            ]
            rules.append(PolicyRule(
                rule_id=r.get("rule_id", ""),
                description=r.get("description", ""),
                outcome=PolicyOutcome(r.get("outcome", "allow")),
                priority=r.get("priority", 100),
                conditions=conditions,
                projects=r.get("projects", []),
                shadow=r.get("shadow", False),
                enabled=r.get("enabled", True),
                reason=r.get("reason", ""),
                modifications=r.get("modifications", {}),
            ))
        return cls(rules)

    def to_yaml(self) -> str:
        """Export rules to YAML."""
        rules_data = []
        for r in self.rules:
            rule_dict: dict[str, Any] = {
                "rule_id": r.rule_id,
                "description": r.description,
                "outcome": r.outcome.value,
                "priority": r.priority,
                "reason": r.reason,
            }
            if r.conditions:
                rule_dict["conditions"] = [
                    {"field": c.field, "operator": c.operator, "value": c.value}
                    for c in r.conditions
                ]
            if r.projects:
                rule_dict["projects"] = r.projects
            if r.shadow:
                rule_dict["shadow"] = True
            if not r.enabled:
                rule_dict["enabled"] = False
            if r.modifications:
                rule_dict["modifications"] = r.modifications
            rules_data.append(rule_dict)
        return yaml.dump({"rules": rules_data}, default_flow_style=False)
