from __future__ import annotations

import time
from typing import Any, Callable

from .models import AuthorizationError, PolicyError, State, canonical_hash
from .storage import DEFAULT_RUN, Store


READ_TOOLS = {"get_case", "get_policy", "get_result"}
WRITE_TOOLS = {"save_review_draft", "request_approval", "apply_mock_update"}
ALL_TOOLS = READ_TOOLS | WRITE_TOOLS
WRITE_ALLOWED_STATES = {
    "save_review_draft": {State.READY_FOR_REVIEW.value},
    "request_approval": {State.READY_FOR_REVIEW.value},
    "apply_mock_update": {State.APPROVED.value, State.RETRY_PENDING.value},
}


class PolicyGateway:
    def __init__(self, store: Store, version: str = "policy-v1", clock: Callable[[], float] | None = None) -> None:
        self.store = store
        self.version = version
        self.clock = clock or time.monotonic

    @property
    def now(self) -> float:
        return self.clock()

    def authorize(self, actor_case: str, target_case: str, tool: str, arguments: dict[str, Any], approval_id: str | None = None, run_id: str | None = None, enforce_state: bool = True) -> None:
        if tool not in ALL_TOOLS:
            raise PolicyError("forbidden tool")
        if actor_case != target_case:
            raise AuthorizationError("cross-case access blocked")
        run = run_id or DEFAULT_RUN
        if arguments.get("case_id") != target_case:
            raise AuthorizationError("tool case scope mismatch")
        if enforce_state and tool in WRITE_TOOLS and self.store.state(target_case, run) not in WRITE_ALLOWED_STATES[tool]:
            raise PolicyError("write forbidden in current workflow state")
        if tool == "apply_mock_update":
            self.validate_approval(approval_id, target_case, run, tool, arguments)

    def validate_approval(self, approval_id: str | None, case_id: str, run_id: str, tool: str, arguments: dict[str, Any], allow_used: bool = False) -> Any:
        if not approval_id:
            raise PolicyError("approval required")
        record = self.store.approval(approval_id, case_id, run_id)
        now = self.clock()
        if not record or record["case_id"] != case_id or record["run_id"] != run_id or record["tool"] != tool:
            raise PolicyError("approval scope mismatch")
        if record["args_hash"] != canonical_hash(arguments):
            raise PolicyError("approval arguments changed")
        if record["status"] != "APPROVED" or (record["used"] and not allow_used) or not record["actor"]:
            raise PolicyError("approval not active")
        if record["policy_version"] != self.version or record["decided_at"] is None or record["expires_at"] is None or not (record["decided_at"] <= now <= record["expires_at"]):
            raise PolicyError("approval stale")
        return record


class ExternalApprovalService:
    def __init__(self, store: Store, policy: PolicyGateway) -> None:
        self.store = store
        self.policy = policy

    def decide(self, approval_id: str, actor: str, decision: str = "APPROVED", ttl: float = 10.0) -> str:
        now = self.policy.clock()
        self.store.decide_approval(approval_id, decision, actor, now, now + ttl if decision == "APPROVED" else None)
        return approval_id


class HumanApprovalService(ExternalApprovalService):
    """Compatibility test helper; production runtime never constructs this service."""

    def grant(self, approval_id: str, case_id: str, tool: str, arguments: dict[str, Any], approver: str = "external-reviewer", run_id: str = DEFAULT_RUN) -> str:
        self.store.request_approval({"id": approval_id, "run_id": run_id, "case_id": case_id, "tool": tool, "args_hash": canonical_hash(arguments), "requested_at": self.policy.clock(), "policy_version": self.policy.version})
        return self.decide(approval_id, approver)
