from __future__ import annotations

import hashlib
import json
from typing import Any, Mapping, Sequence

from kernite.core import evaluate_policy_rules


COMBINING_ALGORITHM_DENY_OVERRIDES = "deny_overrides"


def _is_sequence(value: Any) -> bool:
    return isinstance(value, Sequence) and not isinstance(
        value, (str, bytes, bytearray)
    )


def _safe_int(value: Any, *, default: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _canonical_json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def _trace_hash(payload: Mapping[str, Any], *, domain: str) -> str:
    canonical = _canonical_json(payload)
    digest = hashlib.sha256(f"{domain}\n{canonical}".encode("utf-8")).hexdigest()
    return f"sha256:{digest}"


def _normalize_request(
    request_body: Mapping[str, Any],
    *,
    idempotency_key: str | None,
) -> dict[str, Any]:
    workspace_id = str(request_body.get("workspace_id") or "").strip()
    object_type = str(request_body.get("object_type") or "").strip().lower()
    operation = str(request_body.get("operation") or "").strip().lower()
    principal = request_body.get("principal")
    payload = request_body.get("payload") or {}

    if not isinstance(principal, Mapping):
        principal = {}
    if not isinstance(payload, Mapping):
        payload = {}

    return {
        "workspace_id": workspace_id,
        "principal": {
            "type": str(principal.get("type") or "").strip().lower(),
            "id": str(principal.get("id") or "").strip(),
        },
        "object_type": object_type,
        "operation": operation,
        "payload": dict(payload),
        "idempotency_key": idempotency_key,
    }


def _normalize_reasons(
    raw_reasons: Sequence[Mapping[str, Any]],
) -> list[dict[str, Any]]:
    reasons: list[dict[str, Any]] = []
    for reason in raw_reasons:
        normalized = {
            "code": str(reason.get("code") or "policy_denied"),
            "message": str(reason.get("message") or "Policy denied the request."),
            "rule_key": reason.get("rule_key"),
            "field_path": reason.get("field_path"),
            "details": dict(reason.get("details") or {}),
        }
        reasons.append(normalized)
    return reasons


def _normalize_rule_events(
    raw_events: Sequence[Mapping[str, Any]],
) -> list[dict[str, Any]]:
    events: list[dict[str, Any]] = []
    for event in raw_events:
        events.append(
            {
                "rule_key": event.get("rule_key"),
                "rule_type": event.get("rule_type"),
                "status": str(event.get("status") or "info"),
                "message": str(event.get("message") or ""),
                "reason_code": event.get("reason_code"),
                "field_path": event.get("field_path"),
                "details": dict(event.get("details") or {}),
            }
        )
    return events


def _normalize_rules(raw_rules: Any) -> list[dict[str, Any]]:
    if not _is_sequence(raw_rules):
        return []

    normalized: list[dict[str, Any]] = []
    for rule in raw_rules:
        if isinstance(rule, Mapping):
            normalized.append(dict(rule))
    return normalized


def _normalize_policy(policy: Mapping[str, Any]) -> dict[str, Any]:
    policy_key = str(policy.get("policy_key") or "policy").strip() or "policy"
    policy_version = _safe_int(policy.get("policy_version") or policy.get("version"))
    effect = str(policy.get("effect") or "allow").strip().lower()
    if effect not in {"allow", "deny"}:
        effect = "allow"

    normalized: dict[str, Any] = {
        "policy_key": policy_key,
        "policy_version": policy_version,
        "version": _safe_int(policy.get("version"), default=policy_version),
        "effect": effect,
        "priority": _safe_int(policy.get("priority"), default=0),
        "policy_name": str(policy.get("policy_name") or policy_key),
        "rules": _normalize_rules(policy.get("rules")),
    }

    for key in ("policy_id", "policy_version_id"):
        value = str(policy.get(key) or "").strip()
        if value:
            normalized[key] = value

    return normalized


def _normalize_selected_policies(raw_policies: Any) -> list[dict[str, Any]]:
    if not _is_sequence(raw_policies):
        return []

    normalized: list[dict[str, Any]] = []
    for policy in raw_policies:
        if isinstance(policy, Mapping):
            normalized.append(_normalize_policy(policy))
    return normalized


def _scope_is_governed(
    governed_scopes: Any,
    *,
    object_type: str,
    operation: str,
) -> bool:
    if not _is_sequence(governed_scopes):
        return False

    for scope in governed_scopes:
        if not isinstance(scope, Mapping):
            continue
        scope_object_type = str(scope.get("object_type") or "").strip().lower()
        scope_operation = str(scope.get("operation") or "").strip().lower()
        if scope_object_type == object_type and scope_operation == operation:
            return True

    return False


def _normalize_policy_context(
    policy_context: Any,
    *,
    object_type: str,
    operation: str,
) -> dict[str, Any]:
    default_context = {
        "selected_policies": [],
        "policy_selection_reason_code": "out_of_scope_phase1",
        "default_policy": None,
        "guardrail_result": None,
        "validation_rules": [],
        "combining_algorithm": COMBINING_ALGORITHM_DENY_OVERRIDES,
        "previous_trace_hash": None,
        "trace_domain": "kernite.execute.v1",
        "no_policy_decision": "approved",
        "no_policy_reason_code": "out_of_scope_phase1",
        "no_policy_message": "Scope is outside governed policy context.",
    }

    if not isinstance(policy_context, Mapping):
        return default_context

    selected_policies = _normalize_selected_policies(
        policy_context.get("selected_policies")
    )

    governed = False
    governed_raw = policy_context.get("governed")
    if isinstance(governed_raw, bool):
        governed = governed_raw
    elif _scope_is_governed(
        policy_context.get("governed_scopes"),
        object_type=object_type,
        operation=operation,
    ):
        governed = True
    elif selected_policies:
        governed = True

    policy_selection_reason_code = str(
        policy_context.get("policy_selection_reason_code") or ""
    ).strip()
    if not policy_selection_reason_code:
        if selected_policies:
            policy_selection_reason_code = "policy_selected_workspace_default"
        elif governed:
            policy_selection_reason_code = "no_policy_found"
        else:
            policy_selection_reason_code = "out_of_scope_phase1"

    default_policy = policy_context.get("default_policy")
    normalized_default_policy = (
        dict(default_policy) if isinstance(default_policy, Mapping) else None
    )

    guardrail_result = policy_context.get("guardrail_result")
    normalized_guardrail = (
        dict(guardrail_result) if isinstance(guardrail_result, Mapping) else None
    )

    previous_trace_hash = (
        str(policy_context.get("previous_trace_hash") or "").strip() or None
    )

    combining_algorithm = (
        str(
            policy_context.get("combining_algorithm")
            or COMBINING_ALGORITHM_DENY_OVERRIDES
        ).strip()
        or COMBINING_ALGORITHM_DENY_OVERRIDES
    )

    trace_domain = str(
        policy_context.get("trace_domain") or "kernite.execute.v1"
    ).strip()
    if not trace_domain:
        trace_domain = "kernite.execute.v1"

    no_policy_decision = "denied" if governed else "approved"
    no_policy_reason_code = "no_matching_policy" if governed else "out_of_scope_phase1"
    no_policy_message = (
        "No active policy matched this governed request."
        if governed
        else "Scope is outside governed policy context."
    )

    no_policy_decision_override = (
        str(policy_context.get("no_policy_decision") or "").strip().lower()
    )
    if no_policy_decision_override in {"approved", "denied"}:
        no_policy_decision = no_policy_decision_override

    no_policy_reason_code_override = str(
        policy_context.get("no_policy_reason_code") or ""
    ).strip()
    if no_policy_reason_code_override:
        no_policy_reason_code = no_policy_reason_code_override

    no_policy_message_override = str(
        policy_context.get("no_policy_message") or ""
    ).strip()
    if no_policy_message_override:
        no_policy_message = no_policy_message_override

    return {
        "selected_policies": selected_policies,
        "policy_selection_reason_code": policy_selection_reason_code,
        "default_policy": normalized_default_policy,
        "guardrail_result": normalized_guardrail,
        "validation_rules": _normalize_rules(policy_context.get("validation_rules")),
        "combining_algorithm": combining_algorithm,
        "previous_trace_hash": previous_trace_hash,
        "trace_domain": trace_domain,
        "no_policy_decision": no_policy_decision,
        "no_policy_reason_code": no_policy_reason_code,
        "no_policy_message": no_policy_message,
    }


def _policy_response(
    selected_policies: Sequence[Mapping[str, Any]],
    default_policy: Mapping[str, Any] | None,
) -> dict[str, Any]:
    if default_policy:
        return dict(default_policy)

    if selected_policies:
        first = selected_policies[0]
        response: dict[str, Any] = {}
        if first.get("policy_key"):
            response["policy_key"] = str(first["policy_key"])
        if first.get("policy_version") is not None:
            response["policy_version"] = first["policy_version"]
        elif first.get("version") is not None:
            response["policy_version"] = first["version"]
        if response:
            return response

    return {}


def _evaluate_selected_policies(
    *,
    payload: Mapping[str, Any],
    selected_policies: Sequence[Mapping[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
    reasons: list[dict[str, Any]] = []
    rule_events: list[dict[str, Any]] = []
    matched_policies: list[dict[str, Any]] = []

    for policy in selected_policies:
        policy_id = str(policy.get("policy_id") or "") or None
        policy_version_id = str(policy.get("policy_version_id") or "") or None
        policy_key = str(policy.get("policy_key") or "") or "policy"
        policy_name = str(policy.get("policy_name") or policy_key)
        policy_effect = str(policy.get("effect") or "allow").strip().lower()
        if policy_effect not in {"allow", "deny"}:
            policy_effect = "allow"

        rules = list(policy.get("rules") or [])

        matched_entry: dict[str, Any] = {
            "policy_version_id": policy_version_id,
            "policy_id": policy_id,
            "policy_key": policy_key,
            "policy_name": policy_name,
            "effect": policy_effect,
            "priority": int(policy.get("priority") or 0),
            "version": int(policy.get("version") or policy.get("policy_version") or 0),
            "rule_count": len(rules),
            "decision": "approved",
            "reason_codes": [],
        }

        if not rules:
            if policy_effect == "deny":
                deny_reason = {
                    "code": "policy_effect_deny",
                    "message": f"Policy {policy_key} denied the request.",
                    "rule_key": None,
                    "field_path": None,
                    "details": {
                        "policy_key": policy_key,
                        "policy_id": policy_id,
                        "policy_version_id": policy_version_id,
                        "effect": policy_effect,
                        "mode": "unconditional",
                    },
                }
                reasons.append(deny_reason)
                matched_entry["decision"] = "denied"
                matched_entry["reason_codes"] = [deny_reason["code"]]
                rule_events.append(
                    {
                        "rule_key": None,
                        "rule_type": "policy_effect",
                        "status": "failed",
                        "message": "Policy effect deny (no rules).",
                        "reason_code": "policy_effect_deny",
                        "field_path": None,
                        "details": {
                            "policy_key": policy_key,
                            "policy_id": policy_id,
                            "policy_version_id": policy_version_id,
                            "effect": policy_effect,
                            "mode": "unconditional",
                        },
                    }
                )

            matched_policies.append(matched_entry)
            continue

        _, current_reasons, current_events = evaluate_policy_rules(rules, payload)
        for event in current_events:
            event_details = dict(event.get("details") or {})
            event_details.update(
                {
                    "policy_key": policy_key,
                    "policy_id": policy_id,
                    "policy_version_id": policy_version_id,
                    "effect": policy_effect,
                }
            )
            normalized_event = {
                "rule_key": event.get("rule_key"),
                "rule_type": event.get("rule_type"),
                "status": event.get("status"),
                "message": event.get("message"),
                "reason_code": event.get("reason_code"),
                "field_path": event.get("field_path"),
                "details": event_details,
            }
            rule_events.append(normalized_event)

        if current_reasons:
            matched_entry["decision"] = "denied"
            matched_entry["reason_codes"] = [
                str(reason.get("code") or "policy_denied") for reason in current_reasons
            ]
            for reason in current_reasons:
                reason_details = dict(reason.get("details") or {})
                reason_details.update(
                    {
                        "policy_key": policy_key,
                        "policy_id": policy_id,
                        "policy_version_id": policy_version_id,
                        "effect": policy_effect,
                    }
                )
                reasons.append(
                    {
                        "code": str(reason.get("code") or "policy_denied"),
                        "message": str(
                            reason.get("message") or "Policy denied the request."
                        ),
                        "rule_key": reason.get("rule_key"),
                        "field_path": reason.get("field_path"),
                        "details": reason_details,
                    }
                )

        matched_policies.append(matched_entry)

    return reasons, rule_events, matched_policies


def _build_no_policy_reasons(*, code: str, message: str) -> list[dict[str, Any]]:
    return [
        {
            "code": code,
            "message": message,
            "rule_key": None,
            "field_path": None,
            "details": {},
        }
    ]


def evaluate_execute_with_context(
    request_body: Mapping[str, Any],
    *,
    idempotency_key: str | None = None,
    policy_selection_reason_code: str | None = None,
    selected_policies: Sequence[Mapping[str, Any]] | None = None,
    guardrail_result: Mapping[str, Any] | None = None,
    validation_rules: Sequence[Mapping[str, Any]] | None = None,
    combining_algorithm: str = COMBINING_ALGORITHM_DENY_OVERRIDES,
    default_policy: Mapping[str, Any] | None = None,
    previous_trace_hash: str | None = None,
    trace_domain: str = "kernite.execute.v1",
    no_policy_decision: str = "approved",
    no_policy_reason_code: str = "no_matching_policy",
    no_policy_message: str = "No active policy matched this request.",
) -> dict[str, Any]:
    normalized = _normalize_request(request_body, idempotency_key=idempotency_key)
    payload = normalized["payload"]
    effective_policies = list(selected_policies or [])

    if not policy_selection_reason_code:
        policy_selection_reason_code = (
            "policy_selected_workspace_default"
            if effective_policies
            else "no_policy_found"
        )

    normalized_no_policy_decision = (
        str(no_policy_decision or "approved").strip().lower()
    )
    if normalized_no_policy_decision not in {"approved", "denied"}:
        normalized_no_policy_decision = "approved"

    normalized_no_policy_reason_code = (
        str(no_policy_reason_code or "no_matching_policy").strip()
        or "no_matching_policy"
    )
    normalized_no_policy_message = (
        str(no_policy_message or "No active policy matched this request.").strip()
        or "No active policy matched this request."
    )

    reasons: list[dict[str, Any]] = []
    rule_events: list[dict[str, Any]] = []
    matched_policies: list[dict[str, Any]] = []

    guardrail = dict(guardrail_result or {})
    guardrail_decision = str(guardrail.get("decision") or "approved").strip().lower()
    guardrail_reasons = _normalize_reasons(guardrail.get("reasons") or [])

    if guardrail_decision == "denied":
        decision = "denied"
        reasons = guardrail_reasons
    else:
        normalized_validation_rules = list(validation_rules or [])
        if normalized_validation_rules:
            validation_decision, validation_reasons, validation_events = (
                evaluate_policy_rules(normalized_validation_rules, payload)
            )
            rule_events.extend(_normalize_rule_events(validation_events))
            if validation_decision == "denied":
                decision = "denied"
                reasons = _normalize_reasons(validation_reasons)
            else:
                policy_reasons, policy_rule_events, matched_policies = (
                    _evaluate_selected_policies(
                        payload=payload,
                        selected_policies=effective_policies,
                    )
                )
                reasons = policy_reasons
                rule_events.extend(policy_rule_events)
                if policy_reasons:
                    decision = "denied"
                elif effective_policies:
                    decision = "approved"
                else:
                    decision = normalized_no_policy_decision
                    reasons = _build_no_policy_reasons(
                        code=normalized_no_policy_reason_code,
                        message=normalized_no_policy_message,
                    )
        else:
            policy_reasons, policy_rule_events, matched_policies = (
                _evaluate_selected_policies(
                    payload=payload,
                    selected_policies=effective_policies,
                )
            )
            reasons = policy_reasons
            rule_events.extend(policy_rule_events)
            if policy_reasons:
                decision = "denied"
            elif effective_policies:
                decision = "approved"
            else:
                decision = normalized_no_policy_decision
                reasons = _build_no_policy_reasons(
                    code=normalized_no_policy_reason_code,
                    message=normalized_no_policy_message,
                )

    reason_codes = [
        str(reason.get("code") or "") for reason in reasons if reason.get("code")
    ]
    trace_hash = _trace_hash(
        {
            "input": normalized,
            "decision": decision,
            "reason_codes": reason_codes,
            "policy_selection_reason_code": policy_selection_reason_code,
            "matched_policies": matched_policies,
            "combining_algorithm": combining_algorithm,
            "previous_trace_hash": previous_trace_hash,
        },
        domain=trace_domain,
    )

    ctx_id = f"ctx_{trace_hash.replace('sha256:', '')[:12]}"
    message = (
        "Denied by governance policy."
        if decision == "denied"
        else "Approved by governance policy."
    )

    return {
        "ctx_id": ctx_id,
        "message": message,
        "data": {
            "decision": decision,
            "reason_codes": reason_codes,
            "reasons": reasons,
            "rule_events": rule_events,
            "matched_policies": matched_policies,
            "combining_algorithm": combining_algorithm,
            "policy_selection_reason_code": policy_selection_reason_code,
            "policy": _policy_response(effective_policies, default_policy),
            "trace_hash": trace_hash,
            "previous_trace_hash": previous_trace_hash,
            "idempotency_key": idempotency_key,
        },
    }


def evaluate_execute(
    request_body: Mapping[str, Any],
    *,
    idempotency_key: str | None = None,
) -> dict[str, Any]:
    normalized = _normalize_request(request_body, idempotency_key=idempotency_key)
    policy_context = _normalize_policy_context(
        request_body.get("policy_context"),
        object_type=normalized["object_type"],
        operation=normalized["operation"],
    )

    return evaluate_execute_with_context(
        normalized,
        idempotency_key=idempotency_key,
        policy_selection_reason_code=policy_context["policy_selection_reason_code"],
        selected_policies=policy_context["selected_policies"],
        guardrail_result=policy_context["guardrail_result"],
        validation_rules=policy_context["validation_rules"],
        combining_algorithm=policy_context["combining_algorithm"],
        default_policy=policy_context["default_policy"],
        previous_trace_hash=policy_context["previous_trace_hash"],
        trace_domain=policy_context["trace_domain"],
        no_policy_decision=policy_context["no_policy_decision"],
        no_policy_reason_code=policy_context["no_policy_reason_code"],
        no_policy_message=policy_context["no_policy_message"],
    )
