"""
eaigins_sdk.policy_loader
──────────────────────────
Load governance policies from YAML or JSON config files.

This enables DevOps / compliance teams to define policies WITHOUT writing Python.
Policy files can be committed to git, reviewed in PRs, and loaded at runtime.

YAML policy format::

    # aml_policies.yaml
    policies:
      - id: POL-001
        name: High-Value Transaction Approval
        description: Transactions > $10K require compliance officer approval.
        category: OPERATIONAL
        requires_human_approval: true
        blocks_action: false
        rule:
          type: param_threshold
          param: amount
          operator: ">"
          value: 10000
          message: "Transaction ${amount} exceeds $10,000 — compliance approval required."

      - id: POL-003
        name: PII Access Requires Case ID
        category: DATA_GOVERNANCE
        blocks_action: true
        rule:
          type: tool_param_missing
          tools: [get_customer_full_profile]
          required_param: case_id
          message: "Full PII access requires an active case_id."

      - id: POL-CUSTOM
        name: Custom Expression Policy
        category: REGULATORY
        rule:
          type: expression
          expr: "tool_name not in ['file_sar', 'submit_regulatory_report']"
          message: "SAR filing requires human authorization."

Supported rule types:
    param_threshold       — tool_params[param] OP value
    tool_param_missing    — tool_name in tools AND param missing
    tool_match            — tool_name in [list]
    expression            — arbitrary Python expression (safe eval)
    always_pass           — policy always passes (placeholder/documentation)
    always_fail           — policy always fails (hard block everything)

Usage::

    from eaigins_sdk.policy_loader import load_policies_yaml, load_policies_json
    from eaigins_sdk import ControlPlane

    policies = load_policies_yaml("policies/aml.yaml")
    cp = ControlPlane(policies=policies)

    # Or combine with built-in policies
    from eaigins_sdk.policies import AML_POLICIES
    policies = AML_POLICIES + load_policies_yaml("policies/company_custom.yaml")
    cp = ControlPlane(policies=policies)
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Optional, Union

from .policy import Policy


# ── Safe expression evaluator ──────────────────────────────────────────────────
# Supports a restricted subset of Python for policy expressions.
# No imports, no builtins that could be exploited.

_SAFE_NAMES = {
    "True": True, "False": False, "None": None,
    "len": len, "abs": abs, "max": max, "min": min, "int": int,
    "float": float, "str": str, "bool": bool, "list": list,
}

def _safe_eval(expr: str, action_vars: dict) -> bool:
    """Evaluate a restricted policy expression string."""
    # Reject anything with imports, exec, eval, open, __
    forbidden = ["import", "exec", "eval", "open", "__", "globals", "locals",
                 "getattr", "setattr", "delattr", "compile", "input"]
    for f in forbidden:
        if f in expr:
            raise ValueError(f"Forbidden keyword in policy expression: {f!r}")
    try:
        namespace = {**_SAFE_NAMES, **action_vars}
        return bool(eval(expr, {"__builtins__": {}}, namespace))   # noqa: S307
    except Exception as e:
        raise ValueError(f"Policy expression evaluation failed: {e}\nExpression: {expr!r}")


# ── Rule compiler ──────────────────────────────────────────────────────────────

def _compile_rule(rule: dict) -> tuple:
    """
    Compile a rule dict into an (evaluate_fn, message_template) pair.
    Returns (callable, str).
    """
    rule_type = rule.get("type", "always_pass")
    msg_tmpl  = rule.get("message", "Policy rule triggered.")

    if rule_type == "param_threshold":
        param    = rule["param"]
        op       = rule["operator"]
        value    = rule["value"]
        op_map = {
            ">":  lambda a, b: a > b,
            ">=": lambda a, b: a >= b,
            "<":  lambda a, b: a < b,
            "<=": lambda a, b: a <= b,
            "==": lambda a, b: a == b,
            "!=": lambda a, b: a != b,
        }
        if op not in op_map:
            raise ValueError(f"Unsupported operator {op!r}. Use: {list(op_map)}")
        compare = op_map[op]
        def evaluate_threshold(action, _param=param, _compare=compare, _value=value, _msg=msg_tmpl):
            actual = action.tool_params.get(_param, 0)
            passed = not _compare(actual, _value)
            message = _msg.replace(f"${{{_param}}}", str(actual))
            return passed, message
        return evaluate_threshold

    elif rule_type == "tool_param_missing":
        tools         = set(rule.get("tools", []))
        required_param = rule["required_param"]
        def evaluate_missing(action, _tools=tools, _rp=required_param, _msg=msg_tmpl):
            if _tools and action.tool_name not in _tools:
                return True, _msg
            passed = bool(action.tool_params.get(_rp))
            return passed, _msg
        return evaluate_missing

    elif rule_type == "tool_match":
        tools = set(rule.get("tools", []))
        allow = rule.get("allow", False)  # True=allow list, False=block list
        def evaluate_tool(action, _tools=tools, _allow=allow, _msg=msg_tmpl):
            in_list = action.tool_name in _tools
            if _allow:
                return in_list, _msg         # block if NOT in allow-list
            else:
                return not in_list, _msg     # block if in block-list
        return evaluate_tool

    elif rule_type == "expression":
        expr = rule["expr"]
        def evaluate_expr(action, _expr=expr, _msg=msg_tmpl):
            vars_ = {
                "tool_name":       action.tool_name,
                "tool_params":     action.tool_params,
                "context_sources": action.context_sources,
                "agent_id":        action.agent_id,
                "prompt":          action.prompt,
                "metadata":        action.metadata,
            }
            try:
                passed = _safe_eval(_expr, vars_)
                return passed, _msg
            except ValueError as e:
                return True, f"[Expression eval error — skipping]: {e}"
        return evaluate_expr

    elif rule_type == "always_pass":
        return lambda action: (True, msg_tmpl)

    elif rule_type == "always_fail":
        return lambda action: (False, msg_tmpl)

    else:
        raise ValueError(f"Unknown rule type: {rule_type!r}. "
                         f"Valid: param_threshold, tool_param_missing, tool_match, "
                         f"expression, always_pass, always_fail")


# ── Public loaders ─────────────────────────────────────────────────────────────

def load_policies_dict(data: dict) -> list[Policy]:
    """
    Load policies from a parsed dict (already loaded from YAML or JSON).

    Expected format::

        {
          "policies": [
            {
              "id": "POL-001",
              "name": "...",
              "description": "...",
              "category": "OPERATIONAL",
              "requires_human_approval": true,
              "blocks_action": false,
              "rule": { "type": "param_threshold", ... }
            },
            ...
          ]
        }
    """
    raw_policies = data.get("policies", data) if isinstance(data, dict) else data
    if not isinstance(raw_policies, list):
        raise ValueError("Policy config must be a dict with 'policies' key, or a list.")

    result: list[Policy] = []
    for item in raw_policies:
        policy_id   = item.get("id") or item.get("policy_id", "UNKNOWN")
        name        = item.get("name", policy_id)
        description = item.get("description", "")
        category    = item.get("category", "OPERATIONAL").upper()
        req_human   = bool(item.get("requires_human_approval", False))
        blocks      = bool(item.get("blocks_action", False))

        rule        = item.get("rule")
        if rule is None:
            # No rule = always pass (documentation-only policy)
            evaluate_fn = lambda action: (True, f"Policy {policy_id}: no rule defined.")
        else:
            evaluate_fn = _compile_rule(rule)

        result.append(Policy(
            policy_id               = policy_id,
            name                    = name,
            description             = description,
            category                = category,
            requires_human_approval = req_human,
            blocks_action           = blocks,
            evaluate                = evaluate_fn,
        ))

    return result


def load_policies_json(path: Union[str, Path]) -> list[Policy]:
    """Load policies from a JSON file."""
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)
    return load_policies_dict(data)


def load_policies_yaml(path: Union[str, Path]) -> list[Policy]:
    """
    Load policies from a YAML file.
    Requires PyYAML: pip install pyyaml
    """
    try:
        import yaml
    except ImportError:
        raise ImportError(
            "PyYAML is required for YAML policy files: pip install pyyaml\n"
            "Or use load_policies_json() with a JSON policy file instead."
        )
    with open(path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    return load_policies_dict(data)


def load_policies_string(content: str, format: str = "yaml") -> list[Policy]:
    """
    Load policies from a string (YAML or JSON).
    Useful for loading from environment variables, remote config, or databases.

    Args:
        content: Policy config as a string.
        format:  "yaml" or "json".
    """
    if format == "json":
        data = json.loads(content)
    elif format == "yaml":
        try:
            import yaml
            data = yaml.safe_load(content)
        except ImportError:
            raise ImportError("pip install pyyaml required for YAML format")
    else:
        raise ValueError(f"Unsupported format: {format!r}. Use 'yaml' or 'json'.")
    return load_policies_dict(data)


# ── Schema exporter ────────────────────────────────────────────────────────────

def export_policies_json(policies: list[Policy], path: Optional[Union[str, Path]] = None) -> str:
    """
    Export a list of Policy objects to JSON string (or file if path given).
    Note: evaluate functions are exported as a description string only
    (Python callables can't be serialised to JSON).
    """
    records = []
    for p in policies:
        records.append({
            "id":                       p.policy_id,
            "name":                     p.name,
            "description":              p.description,
            "category":                 p.category,
            "requires_human_approval":  p.requires_human_approval,
            "blocks_action":            p.blocks_action,
            "rule":                     {"type": "expression",
                                         "expr": "<Python callable — see source>",
                                         "message": "See policy evaluate() function."}
        })
    result = json.dumps({"policies": records}, indent=2)
    if path:
        with open(path, "w", encoding="utf-8") as f:
            f.write(result)
    return result
