import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional, Set, Tuple


@dataclass
class RuleContext:
    payload: dict[str, Any]
    schema_failed: bool = False
    degrade_actions: Optional[Set[str]] = None
    governor_status: str = "APPROVED"
    reject_reason: Optional[str] = None
    clamp_changes: Optional[list[str]] = None


def _load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def load_rules(root: Path) -> dict[str, Any]:
    return {
        "data_quality": _load_json(root / "config/data_quality_rules.json"),
        "governor": _load_json(root / "config/risk_governor_rules.json"),
        "orchestration": _load_json(root / "config/agent_orchestration_rules.json"),
        "ai_schema": _load_json(root / "contracts/ai_trade_plan.schema.json"),
        "signal_schema": _load_json(root / "contracts/signal.schema.json"),
    }


def _required_keys_ok(obj: dict[str, Any], required: list[str]) -> bool:
    return all(k in obj for k in required)


def validate_ai_plan(ai_plan: dict[str, Any], ai_schema: dict[str, Any]) -> Tuple[bool, Optional[str]]:
    required = ai_schema.get("required", [])
    if not _required_keys_ok(ai_plan, required):
        return False, "MISSING_REQUIRED_FIELDS"
    if ai_plan.get("action") == "OPEN" and ai_plan.get("sl_price") is None:
        return False, "OPEN_WITHOUT_SL"
    if ai_plan.get("action") == "HOLD" and ai_plan.get("side") != "NONE":
        return False, "HOLD_SIDE_MUST_BE_NONE"
    return True, None


def _resolve_threshold_expr(expr: str, metrics: dict[str, Any]) -> float:
    parts = expr.split("*")
    base = parts[0].strip()
    multiplier = float(parts[1].strip()) if len(parts) > 1 else 1.0
    return float(metrics.get(base, 0)) * multiplier


def _compare(value: float, operator: str, threshold: float) -> bool:
    if operator == "<=":
        return value <= threshold
    if operator == ">":
        return value > threshold
    if operator == ">=":
        return value >= threshold
    if operator == "<":
        return value < threshold
    if operator == "==":
        return value == threshold
    return False


def apply_data_quality(ctx: RuleContext, rules: dict[str, Any]) -> None:
    metrics = ctx.payload.get("market_metrics", {})
    ai_plan = ctx.payload.get("ai_trade_plan", {})
    degrade = set()
    for check in rules.get("pre_trade_checks", []):
        metric_name = check.get("metric")
        operator = check.get("operator")
        value = float(metrics.get(metric_name, 0))
        threshold = check.get("threshold")
        if threshold is None and check.get("threshold_expr"):
            threshold = _resolve_threshold_expr(check["threshold_expr"], metrics)
        if threshold is None:
            continue
        ok = _compare(value, operator, float(threshold))
        if not ok:
            degrade.add(check.get("on_fail", "HOLD_ONLY"))
    if not degrade:
        ctx.degrade_actions = None
        return
    policies = rules.get("degrade_policy", {})
    allowed: set[str] = set()
    for mode in degrade:
        for action in policies.get(mode, []):
            allowed.add(action)
    ctx.degrade_actions = allowed
    if ai_plan.get("action") == "OPEN" and "OPEN" not in allowed:
        ai_plan["action"] = "HOLD"
        ai_plan["side"] = "NONE"
        ctx.payload["ai_trade_plan"] = ai_plan


def apply_orchestration(ctx: RuleContext, rules: dict[str, Any]) -> None:
    runtime = ctx.payload.get("agent_runtime", {})
    ai_plan = ctx.payload.get("ai_trade_plan", {})
    risk_output = ctx.payload.get("agent_outputs", {}).get("risk_agent", {})
    strat_output = ctx.payload.get("agent_outputs", {}).get("strategy_agent", {})

    for fr in rules.get("fallback_rules", []):
        when = fr.get("when", {})
        if "agent_timeout_ms_gt" in when and float(runtime.get("agent_timeout_ms", 0)) > float(when["agent_timeout_ms_gt"]):
            ai_plan["action"] = "HOLD"
            ai_plan["side"] = "NONE"
        if when.get("schema_validation_failed") and ctx.schema_failed:
            ai_plan["action"] = "HOLD"
            ai_plan["side"] = "NONE"

    if risk_output.get("risk_level") == "HIGH":
        ai_plan["action"] = "HOLD"
        ai_plan["side"] = "NONE"
    if strat_output.get("side_conflict_with_regime") is True:
        ai_plan["action"] = "HOLD"
        ai_plan["side"] = "NONE"
    evidence = ai_plan.get("evidence", [])
    if len(evidence) < 2:
        ai_plan["action"] = "HOLD"
        ai_plan["side"] = "NONE"
    ctx.payload["ai_trade_plan"] = ai_plan


def _rule_match(when: dict[str, Any], plan: dict[str, Any], payload: dict[str, Any]) -> bool:
    account = payload.get("account_metrics", {})
    if "action" in when and plan.get("action") != when["action"]:
        return False
    if "sl_price_present" in when:
        present = plan.get("sl_price") is not None
        if bool(when["sl_price_present"]) != present:
            return False
    if "confidence_lt" in when and float(plan.get("confidence", 0)) >= float(when["confidence_lt"]):
        return False
    if "regime" in when and plan.get("regime") != when["regime"]:
        return False
    if "risk_per_trade_pct_gt" in when and float(plan.get("risk_per_trade_pct", 0)) <= float(when["risk_per_trade_pct_gt"]):
        return False
    if "daily_realized_loss_pct_gt" in when and float(account.get("daily_realized_loss_pct", 0)) <= float(when["daily_realized_loss_pct_gt"]):
        return False
    if "consecutive_losses_gte" in when and int(account.get("consecutive_losses", 0)) < int(when["consecutive_losses_gte"]):
        return False
    return True


def apply_governor(ctx: RuleContext, rules: dict[str, Any]) -> None:
    plan = ctx.payload.get("ai_trade_plan", {})
    ctx.clamp_changes = []
    for rule in rules.get("decision_rules", []):
        when = rule.get("when", {})
        if not _rule_match(when, plan, ctx.payload):
            continue
        result = rule.get("result")
        if result == "REJECTED":
            ctx.governor_status = "REJECTED"
            ctx.reject_reason = rule.get("reason")
            return
        if result == "CLAMPED":
            clamp = rule.get("clamp", {})
            if clamp.get("volume_lots") == "scale_to_risk_limit":
                current = float(plan.get("risk_per_trade_pct", 0))
                limit = float(rules.get("global_limits", {}).get("max_risk_per_trade_pct", 0.8))
                if current > 0 and float(plan.get("volume_lots", 0)) > 0:
                    ratio = limit / current
                    plan["volume_lots"] = round(float(plan["volume_lots"]) * ratio, 4)
                    ctx.clamp_changes.append(f"volume_lots_scaled:{ratio:.4f}")
            ctx.governor_status = "CLAMPED"
            ctx.reject_reason = rule.get("reason")
    ctx.payload["ai_trade_plan"] = plan


def build_signal(ctx: RuleContext) -> dict[str, Any]:
    plan = ctx.payload.get("ai_trade_plan", {})
    now_iso = datetime.now(timezone.utc).isoformat()
    signal = {
        "signal_id": plan.get("signal_id"),
        "timestamp": now_iso,
        "symbol": plan.get("symbol", "XAUUSD"),
        "action": plan.get("action", "HOLD"),
        "side": plan.get("side", "NONE"),
        "mode": plan.get("regime", "UNCERTAIN"),
        "governor_status": ctx.governor_status,
        "trace": {
            "confidence": float(plan.get("confidence", 0)),
            "evidence": plan.get("evidence", []),
            "risk_flags": plan.get("risk_flags", []),
        },
    }
    for key in ["volume_lots", "entry_price", "sl_price", "tp_price"]:
        if key in plan and plan.get(key) is not None:
            signal[key] = plan[key]
    if ctx.governor_status == "REJECTED":
        signal["reject_reason"] = ctx.reject_reason or "REJECTED_BY_GOVERNOR"
    if ctx.governor_status == "CLAMPED":
        signal["clamp_changes"] = ctx.clamp_changes or []
    return signal


def validate_signal(signal: dict[str, Any], signal_schema: dict[str, Any]) -> Tuple[bool, Optional[str]]:
    required = signal_schema.get("required", [])
    if not _required_keys_ok(signal, required):
        return False, "SIGNAL_MISSING_REQUIRED_FIELDS"
    return True, None


def run_engine(payload: dict[str, Any], root: Path) -> dict[str, Any]:
    rules = load_rules(root)
    ctx = RuleContext(payload=payload)
    ai_plan = payload.get("ai_trade_plan", {})
    ok, err = validate_ai_plan(ai_plan, rules["ai_schema"])
    if not ok:
        ctx.schema_failed = True
        ai_plan["action"] = "HOLD"
        ai_plan["side"] = "NONE"
        ai_plan.setdefault("evidence", []).append(err)
        payload["ai_trade_plan"] = ai_plan
    apply_data_quality(ctx, rules["data_quality"])
    apply_orchestration(ctx, rules["orchestration"])
    apply_governor(ctx, rules["governor"])
    signal = build_signal(ctx)
    sig_ok, sig_err = validate_signal(signal, rules["signal_schema"])
    if not sig_ok:
        raise ValueError(sig_err)
    return signal
