"""Gate 5C guarded advisory arbiter.

The arbiter is deliberately not a controller. It cannot emit patches, paths,
tool commands, or answers. It can only approve, abstain, block risky proposals,
or request deterministic safety follow-ups.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any

from .bytecode import CognitiveOp, Instruction
from .controller import Controller
from .ledger import canonical_json, stable_hash
from .shadow_controller import CalibratedShadowPrediction, ShadowCalibration, ShadowControllerModel, model_input_from_workspace, predict_calibrated_shadow


class AdvisoryDecision(StrEnum):
    APPROVE = "APPROVE"
    ABSTAIN = "ABSTAIN"
    REQUEST_VERIFY = "REQUEST_VERIFY"
    REQUEST_INSPECT = "REQUEST_INSPECT"
    BLOCK_SKILL = "BLOCK_SKILL"
    BLOCK_PATCH = "BLOCK_PATCH"
    BLOCK_ANSWER = "BLOCK_ANSWER"
    REQUEST_FALLBACK = "REQUEST_FALLBACK"
    REQUEST_QUARANTINE_REVIEW = "REQUEST_QUARANTINE_REVIEW"


ADVISORY_SCHEMA_VERSION = 1
DIRECT_DOMAIN_ACTIONS = {
    CognitiveOp.RETRIEVE_SKILL.value,
    CognitiveOp.CALL_TOOL.value,
    CognitiveOp.PATCH.value,
    CognitiveOp.RUN_TEST.value,
    CognitiveOp.ANSWER.value,
}


@dataclass(slots=True)
class AdvisoryDecisionSpec:
    decision: str
    allowed_when: str
    required_context: list[str]
    risk_reduction_goal: str
    forbidden_side_effects: list[str]
    may_increase_steps: bool
    can_block_execution: bool
    can_request_verifier_or_inspection: bool

    def to_dict(self) -> dict[str, Any]:
        return {
            "decision": self.decision,
            "allowed_when": self.allowed_when,
            "required_context": self.required_context,
            "risk_reduction_goal": self.risk_reduction_goal,
            "forbidden_side_effects": self.forbidden_side_effects,
            "may_increase_steps": self.may_increase_steps,
            "can_block_execution": self.can_block_execution,
            "can_request_verifier_or_inspection": self.can_request_verifier_or_inspection,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "AdvisoryDecisionSpec":
        return cls(
            decision=str(data["decision"]),
            allowed_when=str(data["allowed_when"]),
            required_context=[str(item) for item in data.get("required_context", [])],
            risk_reduction_goal=str(data["risk_reduction_goal"]),
            forbidden_side_effects=[str(item) for item in data.get("forbidden_side_effects", [])],
            may_increase_steps=bool(data["may_increase_steps"]),
            can_block_execution=bool(data["can_block_execution"]),
            can_request_verifier_or_inspection=bool(data["can_request_verifier_or_inspection"]),
        )


@dataclass(slots=True)
class AdvisoryContext:
    deterministic_action: str
    deterministic_action_args: dict[str, Any] = field(default_factory=dict)
    shadow_prediction: CalibratedShadowPrediction | None = None
    shadow_confidence: float = 0.0
    shadow_abstained: bool = False
    shadow_abstention_reason: str = "none"
    action_risk_class: str = "low"
    verifier_context: str = ""
    tool_result_summary: str = ""
    safe_memory_warnings: list[str] = field(default_factory=list)
    skill_status: str = "none"
    quarantine_status: str = "none"
    action_mask: dict[str, bool] = field(default_factory=dict)
    legal_advisory_decisions: list[str] = field(default_factory=lambda: [decision.value for decision in AdvisoryDecision])

    def to_dict(self) -> dict[str, Any]:
        return {
            "deterministic_action": self.deterministic_action,
            "deterministic_action_args": self.deterministic_action_args,
            "shadow_prediction": self.shadow_prediction.to_dict() if self.shadow_prediction is not None else None,
            "shadow_confidence": self.shadow_confidence,
            "shadow_abstained": self.shadow_abstained,
            "shadow_abstention_reason": self.shadow_abstention_reason,
            "action_risk_class": self.action_risk_class,
            "verifier_context": self.verifier_context,
            "tool_result_summary": self.tool_result_summary,
            "safe_memory_warnings": self.safe_memory_warnings,
            "skill_status": self.skill_status,
            "quarantine_status": self.quarantine_status,
            "action_mask": self.action_mask,
            "legal_advisory_decisions": self.legal_advisory_decisions,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "AdvisoryContext":
        prediction_data = data.get("shadow_prediction")
        return cls(
            deterministic_action=str(data["deterministic_action"]),
            deterministic_action_args=dict(data.get("deterministic_action_args") or {}),
            shadow_prediction=CalibratedShadowPrediction.from_dict(dict(prediction_data)) if isinstance(prediction_data, dict) else None,
            shadow_confidence=float(data.get("shadow_confidence", 0.0)),
            shadow_abstained=bool(data.get("shadow_abstained", False)),
            shadow_abstention_reason=str(data.get("shadow_abstention_reason", "none")),
            action_risk_class=str(data.get("action_risk_class", "low")),
            verifier_context=str(data.get("verifier_context", "")),
            tool_result_summary=str(data.get("tool_result_summary", "")),
            safe_memory_warnings=[str(item) for item in data.get("safe_memory_warnings", [])],
            skill_status=str(data.get("skill_status", "none")),
            quarantine_status=str(data.get("quarantine_status", "none")),
            action_mask={str(key): bool(value) for key, value in dict(data.get("action_mask") or {}).items()},
            legal_advisory_decisions=[str(item) for item in data.get("legal_advisory_decisions", [decision.value for decision in AdvisoryDecision])],
        )


@dataclass(slots=True)
class AdvisoryOutput:
    advisory_decision: str
    reason: str
    accepted_by_arbiter: bool
    intervention_type: str
    safe_followup_action: str | None = None
    no_direct_learned_action: bool = True

    def to_dict(self) -> dict[str, Any]:
        return {
            "advisory_decision": self.advisory_decision,
            "reason": self.reason,
            "accepted_by_arbiter": self.accepted_by_arbiter,
            "intervention_type": self.intervention_type,
            "safe_followup_action": self.safe_followup_action,
            "no_direct_learned_action": self.no_direct_learned_action,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "AdvisoryOutput":
        return cls(
            advisory_decision=str(data["advisory_decision"]),
            reason=str(data["reason"]),
            accepted_by_arbiter=bool(data["accepted_by_arbiter"]),
            intervention_type=str(data["intervention_type"]),
            safe_followup_action=str(data["safe_followup_action"]) if data.get("safe_followup_action") is not None else None,
            no_direct_learned_action=bool(data.get("no_direct_learned_action", True)),
        )


def advisory_vocabulary() -> dict[str, AdvisoryDecisionSpec]:
    forbidden = ["generate_patch", "choose_path", "generate_shell_command", "call_tool_directly", "answer_directly"]
    specs = {
        AdvisoryDecision.APPROVE.value: AdvisoryDecisionSpec("APPROVE", "proposal is low risk and verifier context is compatible", ["action", "mask"], "avoid unnecessary overhead", forbidden, False, False, False),
        AdvisoryDecision.ABSTAIN.value: AdvisoryDecisionSpec("ABSTAIN", "shadow confidence is low, OOD, or context is underspecified", ["shadow_confidence"], "avoid unsafe learned influence", forbidden, False, False, False),
        AdvisoryDecision.REQUEST_VERIFY.value: AdvisoryDecisionSpec("REQUEST_VERIFY", "answer or completion needs verifier evidence", ["verifier_context"], "force verifier before irreversible output", forbidden, True, False, True),
        AdvisoryDecision.REQUEST_INSPECT.value: AdvisoryDecisionSpec("REQUEST_INSPECT", "patch/skill decision lacks visible evidence", ["tool_result_summary"], "inspect before mutation", forbidden, True, False, True),
        AdvisoryDecision.BLOCK_SKILL.value: AdvisoryDecisionSpec("BLOCK_SKILL", "skill is quarantined or false-trigger risk is high", ["skill_status"], "avoid unsafe reuse", forbidden, True, True, False),
        AdvisoryDecision.BLOCK_PATCH.value: AdvisoryDecisionSpec("BLOCK_PATCH", "patch is unsafe, unobserved, or unnecessary", ["action_args", "verifier_context"], "avoid unsafe mutation", forbidden, True, True, False),
        AdvisoryDecision.BLOCK_ANSWER.value: AdvisoryDecisionSpec("BLOCK_ANSWER", "answer is premature or conflicts with verifier failure", ["verifier_context"], "prevent unverifiable answer", forbidden, True, True, True),
        AdvisoryDecision.REQUEST_FALLBACK.value: AdvisoryDecisionSpec("REQUEST_FALLBACK", "skill verifier failed or reuse path is unsafe", ["verifier_context", "skill_status"], "return to deterministic baseline", forbidden, True, False, False),
        AdvisoryDecision.REQUEST_QUARANTINE_REVIEW.value: AdvisoryDecisionSpec("REQUEST_QUARANTINE_REVIEW", "repeated or quarantined skill failure is visible", ["quarantine_status"], "surface quarantine review", forbidden, True, False, False),
    }
    validate_advisory_vocabulary(specs)
    return specs


def validate_advisory_vocabulary(specs: dict[str, AdvisoryDecisionSpec]) -> None:
    allowed = {decision.value for decision in AdvisoryDecision}
    unknown = set(specs) - allowed
    missing = allowed - set(specs)
    if unknown:
        raise ValueError("unknown advisory decision(s): " + ",".join(sorted(unknown)))
    if missing:
        raise ValueError("missing advisory decision(s): " + ",".join(sorted(missing)))


class GuardedAdvisoryArbiter:
    def __init__(self, specs: dict[str, AdvisoryDecisionSpec] | None = None) -> None:
        self.specs = specs or advisory_vocabulary()
        validate_advisory_vocabulary(self.specs)

    def decide(self, context: AdvisoryContext) -> AdvisoryOutput:
        self._validate_context(context)
        action = context.deterministic_action
        text = canonical_json(context.to_dict()).lower()

        if action not in {op.value for op in CognitiveOp} and action not in {"CALL_SKILL", "WRITE_FILE"}:
            return self._output(AdvisoryDecision.ABSTAIN, "unknown_deterministic_action", False, "abstain")
        if not bool(context.action_mask.get(action, True)):
            return self._output(AdvisoryDecision.ABSTAIN, "action_mask_disabled", False, "abstain")
        if context.shadow_abstained and context.action_risk_class in {"high", "critical"}:
            return self._output(AdvisoryDecision.ABSTAIN, f"shadow_abstained:{context.shadow_abstention_reason}", False, "abstain")

        if action in {CognitiveOp.RETRIEVE_SKILL.value, "CALL_SKILL"}:
            if context.quarantine_status in {"quarantined", "review"} or context.skill_status == "quarantined":
                return self._output(AdvisoryDecision.REQUEST_QUARANTINE_REVIEW, "skill_quarantined", False, "quarantine_review", CognitiveOp.STORE_FAILURE.value)
            if context.skill_status in {"risky", "false_trigger"} or _has_warning(context.safe_memory_warnings, "false_trigger"):
                return self._output(AdvisoryDecision.BLOCK_SKILL, "skill_false_trigger_risk", False, "block_skill", CognitiveOp.CALL_TOOL.value)
            if _has_warning(context.safe_memory_warnings, "anti_pattern"):
                return self._output(AdvisoryDecision.REQUEST_INSPECT, "anti_pattern_memory_before_skill", False, "inspect", CognitiveOp.CALL_TOOL.value)

        if action == CognitiveOp.ANSWER.value:
            if _verifier_failed(context.verifier_context):
                return self._output(AdvisoryDecision.BLOCK_ANSWER, "verifier_failed_before_answer", False, "block_answer", CognitiveOp.PATCH.value)
            if not _verifier_passed(context.verifier_context):
                return self._output(AdvisoryDecision.BLOCK_ANSWER, "answer_before_verifier", False, "block_answer", CognitiveOp.VERIFY.value)

        if action in {CognitiveOp.PATCH.value, "WRITE_FILE"}:
            if _verifier_passed(context.verifier_context):
                return self._output(AdvisoryDecision.BLOCK_PATCH, "patch_after_verifier_passed", False, "block_patch", CognitiveOp.VERIFY.value)
            if _unsafe_patch_context(text):
                return self._output(AdvisoryDecision.REQUEST_INSPECT, "unsafe_or_unobserved_patch", False, "inspect", CognitiveOp.CALL_TOOL.value)
            if _verifier_failed(context.verifier_context) and "failed_skill" in text:
                return self._output(AdvisoryDecision.REQUEST_FALLBACK, "failed_skill_requires_fallback", False, "fallback", CognitiveOp.PATCH.value)

        if action in {CognitiveOp.RUN_TEST.value, CognitiveOp.CALL_TOOL.value} and _illegal_command_context(text):
            return self._output(AdvisoryDecision.ABSTAIN, "illegal_tool_or_verifier_command", False, "abstain")

        if _verifier_failed(context.verifier_context) and action == CognitiveOp.ANSWER.value:
            return self._output(AdvisoryDecision.BLOCK_ANSWER, "cannot_override_verifier_failure", False, "block_answer", CognitiveOp.PATCH.value)
        if _verifier_failed(context.verifier_context) and "skip_fallback" in text:
            return self._output(AdvisoryDecision.REQUEST_FALLBACK, "skip_fallback_after_failure", False, "fallback", CognitiveOp.PATCH.value)
        if context.action_risk_class == "critical":
            return self._output(AdvisoryDecision.ABSTAIN, "critical_risk_requires_human_or_verifier", False, "abstain")
        return self._output(AdvisoryDecision.APPROVE, "low_risk_verified_or_reversible", True, "none")

    def _validate_context(self, context: AdvisoryContext) -> None:
        allowed = {decision.value for decision in AdvisoryDecision}
        unknown = set(context.legal_advisory_decisions) - allowed
        if unknown:
            raise ValueError("unknown legal advisory decision(s): " + ",".join(sorted(unknown)))

    @staticmethod
    def _output(
        decision: AdvisoryDecision,
        reason: str,
        accepted: bool,
        intervention: str,
        followup: str | None = None,
    ) -> AdvisoryOutput:
        return AdvisoryOutput(
            advisory_decision=decision.value,
            reason=reason,
            accepted_by_arbiter=accepted,
            intervention_type=intervention,
            safe_followup_action=followup,
            no_direct_learned_action=True,
        )


class AdvisoryArbiterController:
    """Logs advisory decisions while keeping deterministic control authoritative."""

    def __init__(
        self,
        base: Controller,
        model: ShadowControllerModel,
        calibration: ShadowCalibration,
        arbiter: GuardedAdvisoryArbiter | None = None,
    ) -> None:
        self.base = base
        self.model = model
        self.calibration = calibration
        self.arbiter = arbiter or GuardedAdvisoryArbiter()
        self.logs: list[dict[str, Any]] = []
        self.learned_direct_domain_actions_executed = 0
        self.deterministic_actions_executed = 0

    def next_instruction(self, workspace: Any) -> Instruction:
        proposed = self.base.next_instruction(workspace)
        model_input = model_input_from_workspace(workspace, previous_action=self.logs[-1]["executed_action"] if self.logs else None)
        prediction = predict_calibrated_shadow(self.model, self.calibration, model_input)
        context = context_from_instruction(
            proposed,
            prediction,
            action_mask=dict(model_input.get("action_mask") or {}),
            verifier_context=str(model_input.get("verifier_context_summary", "")),
            tool_result_summary=str(model_input.get("previous_tool_result_summary", "")),
        )
        output = self.arbiter.decide(context)
        executed = proposed
        if output.safe_followup_action and _should_insert_followup(output, proposed):
            executed = Instruction(CognitiveOp(str(output.safe_followup_action)), rationale="Advisory arbiter requested deterministic safety follow-up")
        self.deterministic_actions_executed += 1
        self.logs.append(
            {
                "step_index": len(self.logs),
                "proposed_action": proposed.op.value,
                "executed_action": executed.op.value,
                "shadow_action": prediction.action,
                "shadow_confidence": prediction.confidence,
                "shadow_abstained": prediction.abstained,
                "advisory": output.to_dict(),
                "executed_by_learned_controller": False,
            }
        )
        return executed

    def summary(self) -> dict[str, Any]:
        interventions = [item for item in self.logs if item["advisory"]["intervention_type"] not in {"none", "abstain"}]
        return {
            "advisory_enabled": True,
            "advisory_decision_count": len(self.logs),
            "advisory_intervention_count": len(interventions),
            "learned_direct_domain_actions_executed": self.learned_direct_domain_actions_executed,
            "deterministic_actions_executed": self.deterministic_actions_executed,
            "verifier_requests_inserted": sum(1 for item in interventions if item["advisory"]["safe_followup_action"] == CognitiveOp.VERIFY.value),
            "blocks_applied": sum(1 for item in interventions if str(item["advisory"]["advisory_decision"]).startswith("BLOCK")),
            "fallbacks_requested": sum(1 for item in interventions if item["advisory"]["advisory_decision"] == AdvisoryDecision.REQUEST_FALLBACK.value),
            "quarantine_reviews_requested": sum(1 for item in interventions if item["advisory"]["advisory_decision"] == AdvisoryDecision.REQUEST_QUARANTINE_REVIEW.value),
            "logs": self.logs,
        }


def context_from_instruction(
    instruction: Instruction,
    prediction: CalibratedShadowPrediction | None = None,
    *,
    action_mask: dict[str, bool] | None = None,
    verifier_context: str = "",
    tool_result_summary: str = "",
    safe_memory_warnings: list[str] | None = None,
    skill_status: str = "none",
    quarantine_status: str = "none",
    action_risk_class: str | None = None,
) -> AdvisoryContext:
    action = instruction.op.value
    return AdvisoryContext(
        deterministic_action=action,
        deterministic_action_args=dict(instruction.args),
        shadow_prediction=prediction,
        shadow_confidence=float(prediction.confidence) if prediction is not None else 0.0,
        shadow_abstained=bool(prediction.abstained) if prediction is not None else False,
        shadow_abstention_reason=str(prediction.abstention_reason) if prediction is not None else "none",
        action_risk_class=action_risk_class or risk_class_for_action(action, dict(instruction.args), verifier_context, safe_memory_warnings or [], skill_status, quarantine_status),
        verifier_context=verifier_context,
        tool_result_summary=tool_result_summary,
        safe_memory_warnings=safe_memory_warnings or [],
        skill_status=skill_status,
        quarantine_status=quarantine_status,
        action_mask=action_mask or {op.value: True for op in CognitiveOp},
    )


def risk_class_for_action(
    action: str,
    args: dict[str, Any] | None = None,
    verifier_context: str = "",
    safe_memory_warnings: list[str] | None = None,
    skill_status: str = "none",
    quarantine_status: str = "none",
) -> str:
    args = args or {}
    warnings = safe_memory_warnings or []
    text = " ".join([canonical_json(args).lower(), verifier_context.lower(), " ".join(warnings).lower(), skill_status.lower(), quarantine_status.lower()])
    if action in {CognitiveOp.PATCH.value, "WRITE_FILE"} and _unsafe_patch_context(text):
        return "critical"
    if _illegal_command_context(text):
        return "critical"
    if action in {CognitiveOp.RETRIEVE_SKILL.value, "CALL_SKILL"} and (skill_status == "quarantined" or quarantine_status == "quarantined"):
        return "high"
    if action == CognitiveOp.ANSWER.value and not _verifier_passed(verifier_context):
        return "high"
    if action in {CognitiveOp.PATCH.value, CognitiveOp.RUN_TEST.value, CognitiveOp.CALL_TOOL.value, CognitiveOp.ANSWER.value, CognitiveOp.RETRIEVE_SKILL.value}:
        return "medium"
    return "low"


def advisory_artifact_hash(payload: dict[str, Any]) -> str:
    return stable_hash({"schema_version": ADVISORY_SCHEMA_VERSION, "payload": payload})


def illegal_advisory_decision_count(outputs: list[AdvisoryOutput]) -> int:
    allowed = {decision.value for decision in AdvisoryDecision}
    return sum(1 for output in outputs if output.advisory_decision not in allowed)


def _should_insert_followup(output: AdvisoryOutput, proposed: Instruction) -> bool:
    if output.safe_followup_action is None:
        return False
    if output.safe_followup_action == proposed.op.value:
        return False
    return output.advisory_decision in {
        AdvisoryDecision.REQUEST_VERIFY.value,
        AdvisoryDecision.REQUEST_INSPECT.value,
        AdvisoryDecision.BLOCK_ANSWER.value,
        AdvisoryDecision.BLOCK_SKILL.value,
        AdvisoryDecision.REQUEST_FALLBACK.value,
        AdvisoryDecision.REQUEST_QUARANTINE_REVIEW.value,
    }


def _verifier_passed(text: str) -> bool:
    lowered = text.lower()
    return "latest=passed" in lowered or "verifier_status=passed" in lowered or "pytest passed" in lowered


def _verifier_failed(text: str) -> bool:
    lowered = text.lower()
    return "latest=failed" in lowered or "verifier_status=failed" in lowered or "pytest failed" in lowered or "failed_skill" in lowered


def _unsafe_patch_context(text: str) -> bool:
    lowered = text.lower()
    markers = [
        "../",
        "..\\",
        "path_traversal",
        "unobserved_file",
        "not_in_visible_listing",
        "hidden_expected_fix",
        "expected_fix",
        "unsafe_patch",
    ]
    return any(marker in lowered for marker in markers)


def _illegal_command_context(text: str) -> bool:
    lowered = text.lower()
    markers = ["illegal_command", "illegal_verifier_command", "python -c", "bash -lc", "cmd /c", "powershell", "rm -rf", "shell_generation"]
    return any(marker in lowered for marker in markers)


def _has_warning(warnings: list[str], marker: str) -> bool:
    return any(marker in item.lower() for item in warnings)
