from __future__ import annotations

import hashlib
import json
import time
from typing import Any, cast

from app.core.llm.deepseek_client import create_llm_client, get_llm_model
from app.core.orchestration.agent.evaluator import evaluate_model_plan
from app.core.orchestration.agent.prompts import PROMPT_VERSION, build_planning_messages
from app.core.orchestration.agent.schemas import (
    MODEL_PLAN_SCHEMA,
    validate_planning_request,
)


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


def _hash(value: Any) -> str:
    return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()


def _opaque_hash(value: Any) -> str:
    try:
        source = _canonical(value)
    except (TypeError, ValueError):
        source = repr(type(value))
    return hashlib.sha256(source.encode("utf-8")).hexdigest()


def _safe_result(tool, result):
    allowed = {
        "candidate_id",
        "execution_id",
        "status",
        "sample_runs",
        "verified_by",
        "workflow_hash",
    }
    return {
        "tool": tool,
        "result": {
            key: value
            for key, value in dict(result or {}).items()
            if key in allowed and isinstance(value, (str, int, float, bool, type(None)))
        },
    }


class OpenAICompatiblePlanningModel:
    def __init__(
        self,
        *,
        client=None,
        provider="deepseek",
        model_name=None,
        max_tokens=4096,
    ):
        self.client = client or create_llm_client()
        self.provider = str(provider).strip() or "openai-compatible"
        self.model_name = str(model_name or get_llm_model()).strip()
        if not self.model_name:
            raise ValueError("planning model name is required")
        if (
            isinstance(max_tokens, bool)
            or not isinstance(max_tokens, int)
            or max_tokens < 256
            or max_tokens > 32768
        ):
            raise ValueError("planning model max_tokens must be between 256 and 32768")
        self.max_tokens = max_tokens

    def generate(self, *, messages, response_schema, timeout_seconds):
        schema_message = {
            "role": "user",
            "content": "OUTPUT_JSON_SCHEMA:\n" + _canonical(response_schema),
        }
        response = self.client.chat.completions.create(
            model=self.model_name,
            messages=cast(Any, [*list(messages), schema_message]),
            stream=False,
            temperature=0,
            max_tokens=self.max_tokens,
            timeout=timeout_seconds,
            response_format={"type": "json_object"},
        )
        choices = list(getattr(response, "choices", []) or [])
        content = (
            getattr(getattr(choices[0], "message", None), "content", None)
            if choices
            else None
        )
        if not isinstance(content, str) or not content.strip():
            raise ValueError("planning model returned empty content")
        return content


class SchedulingPlanner:
    def __init__(
        self,
        *,
        model,
        context_tools,
        scheduling_tools,
        canary_verifier,
        audit,
        model_timeout_seconds=30,
        canary_timeout_seconds=120,
        canary_poll_interval_seconds=0.5,
        sleep=time.sleep,
        monotonic=time.monotonic,
    ):
        if model_timeout_seconds < 1 or model_timeout_seconds > 300:
            raise ValueError("model timeout must be between 1 and 300 seconds")
        if canary_timeout_seconds < 1 or canary_timeout_seconds > 3600:
            raise ValueError("canary timeout must be between 1 and 3600 seconds")
        if canary_poll_interval_seconds <= 0 or canary_poll_interval_seconds > 30:
            raise ValueError("canary poll interval must be between 0 and 30 seconds")
        self.model = model
        self.context_tools = context_tools
        self.scheduling_tools = scheduling_tools
        self.canary_verifier = canary_verifier
        self.audit = audit
        self.model_timeout_seconds = model_timeout_seconds
        self.canary_timeout_seconds = canary_timeout_seconds
        self.canary_poll_interval_seconds = canary_poll_interval_seconds
        self.sleep = sleep
        self.monotonic = monotonic

    def _verify_canary(self, candidate_id, baseline_execution_id):
        deadline = self.monotonic() + self.canary_timeout_seconds
        while True:
            evidence = self.canary_verifier.verify(
                candidate_id,
                baseline_execution_id=baseline_execution_id,
            )
            if evidence.get("status") in {"passed", "failed"}:
                return evidence
            if self.monotonic() >= deadline:
                return {
                    "candidate_id": candidate_id,
                    "status": "timeout",
                    "sample_runs": 0,
                }
            self.sleep(self.canary_poll_interval_seconds)

    def _context(self, request):
        common = {
            "dataflow_uid": request["dataflow_uid"],
            "business_domain": request["business_domain"],
        }
        return {
            "sla": self.context_tools.call_tool(
                "get_sla_constraints",
                common,
            ),
            "datasources": self.context_tools.call_tool(
                "list_datasource_capabilities",
                {"business_domain": request["business_domain"]},
            ),
            "history": self.context_tools.call_tool(
                "get_execution_history",
                {**common, "limit": 20, "days": 7},
            ),
        }

    def _event(self, identity, request, *, decision, context_hash, detail):
        return {
            "subject": identity.subject,
            "roles": sorted(identity.roles),
            "business_domain": request["business_domain"],
            "environment": request["environment"],
            "correlation_id": identity.correlation_id,
            "action": "ai_planning_closed_loop",
            "decision": decision,
            "model_provider": str(getattr(self.model, "provider", "unknown"))[:80],
            "model_name": str(getattr(self.model, "model_name", "unknown"))[:120],
            "prompt_version": PROMPT_VERSION,
            "schema_version": "v54",
            "context_hash": context_hash,
            "detail": detail,
        }

    def _fallback(
        self,
        identity,
        request,
        *,
        reason_code,
        decision,
        context_hash,
        action_results=None,
        candidate_id=None,
        candidate_plan_hash=None,
        workflow_hash=None,
    ):
        result = {
            "status": "rejected" if decision == "rejected" else "degraded",
            "mode": "fixed_schedule",
            "reason_code": reason_code,
            "action": "continue_published_schedule",
            "formal_engine_changed": False,
            "correlation_id": identity.correlation_id,
        }
        if candidate_id:
            result["candidate_id"] = candidate_id
        detail = {
            "reason_code": reason_code,
            "action_results": list(action_results or []),
            "result": result,
        }
        if candidate_id:
            detail["candidate_id"] = candidate_id
        if candidate_plan_hash:
            detail["candidate_plan_hash"] = candidate_plan_hash
        if workflow_hash:
            detail["workflow_hash"] = workflow_hash
        self.audit.record(
            self._event(
                identity,
                request,
                decision=decision,
                context_hash=context_hash,
                detail=detail,
            )
        )
        return result

    def run(self, identity, request):
        normalized = validate_planning_request(request)
        identity.require_domain(normalized["business_domain"])
        identity.require_environment(normalized["environment"])
        if normalized["target_conflicts"]:
            return self._fallback(
                identity,
                normalized,
                reason_code="target_conflict",
                decision="rejected",
                context_hash=_hash(
                    {"target_conflict_count": len(normalized["target_conflicts"])}
                ),
            )
        try:
            context = self._context(normalized)
        except Exception:
            return self._fallback(
                identity,
                normalized,
                reason_code="tool_failure",
                decision="failed",
                context_hash=_hash({"context_status": "unavailable"}),
            )
        context_hash = _hash(context)
        messages = build_planning_messages(normalized, context)
        try:
            raw_plan = self.model.generate(
                messages=messages,
                response_schema=MODEL_PLAN_SCHEMA,
                timeout_seconds=self.model_timeout_seconds,
            )
        except Exception:
            return self._fallback(
                identity,
                normalized,
                reason_code="model_unavailable",
                decision="failed",
                context_hash=context_hash,
            )
        try:
            if isinstance(raw_plan, str):
                raw_plan = json.loads(raw_plan)
            plan = evaluate_model_plan(raw_plan, normalized)
        except (PermissionError, TypeError, ValueError, json.JSONDecodeError):
            return self._fallback(
                identity,
                normalized,
                reason_code="invalid_model_output",
                decision="rejected",
                context_hash=context_hash,
                candidate_plan_hash=_opaque_hash(raw_plan),
            )

        try:
            workflow_validation = self.context_tools.call_tool(
                "validate_workflow_spec",
                {"workflow_spec": plan["workflow_spec"]},
            )
            schedule_validation = self.context_tools.call_tool(
                "validate_schedule_plan",
                {"schedule_plan": plan["schedule_plan"]},
            )
            capacity = self.context_tools.call_tool(
                "estimate_schedule_capacity",
                {
                    "business_domain": normalized["business_domain"],
                    "schedule_plan": plan["schedule_plan"],
                },
            )
            simulation = self.context_tools.call_tool(
                "simulate_schedule",
                {
                    "business_domain": normalized["business_domain"],
                    "schedule_plan": plan["schedule_plan"],
                },
            )
        except Exception:
            return self._fallback(
                identity,
                normalized,
                reason_code="tool_failure",
                decision="failed",
                context_hash=context_hash,
                candidate_plan_hash=_hash(plan),
            )
        if not capacity.get("feasible"):
            return self._fallback(
                identity,
                normalized,
                reason_code="schedule_not_feasible",
                decision="rejected",
                context_hash=context_hash,
                candidate_plan_hash=_hash(plan),
            )

        scope = {
            "business_domain": normalized["business_domain"],
            "environment": normalized["environment"],
        }
        action_results = []
        candidate = {}
        candidate_id = None
        try:
            candidate = self.scheduling_tools.call_tool(
                "create_candidate_plan",
                {
                    **scope,
                    "workflow_spec": plan["workflow_spec"],
                    "schedule_plan": plan["schedule_plan"],
                },
            )
            action_results.append(_safe_result("create_candidate_plan", candidate))
            candidate_id = candidate["candidate_id"]
            deployed = self.scheduling_tools.call_tool(
                "deploy_disabled_version",
                {**scope, "candidate_id": candidate_id},
            )
            action_results.append(_safe_result("deploy_disabled_version", deployed))
            canary = self.scheduling_tools.call_tool(
                "run_canary",
                {
                    **scope,
                    "candidate_id": candidate_id,
                    "inputs": normalized["canary_inputs"],
                },
            )
            action_results.append(_safe_result("run_canary", canary))
            evidence = self._verify_canary(
                candidate_id,
                normalized.get("baseline_execution_id"),
            )
            action_results.append(_safe_result("verify_canary", evidence))
        except Exception:
            return self._fallback(
                identity,
                normalized,
                reason_code="tool_failure",
                decision="failed",
                context_hash=context_hash,
                action_results=action_results,
                candidate_id=candidate_id,
                candidate_plan_hash=_hash(plan),
                workflow_hash=candidate.get("workflow_hash"),
            )

        passed = evidence.get("status") == "passed"
        result = {
            "status": "canary_passed" if passed else "canary_not_passed",
            "candidate_id": candidate_id,
            "canary_execution_id": canary.get("execution_id"),
            "promotion_recommendation": "shadow_only" if passed else "hold",
            "formal_engine_changed": False,
            "correlation_id": identity.correlation_id,
        }
        self.audit.record(
            self._event(
                identity,
                normalized,
                decision="allowed" if passed else "rejected",
                context_hash=context_hash,
                detail={
                    "candidate_id": candidate_id,
                    "workflow_hash": candidate.get("workflow_hash"),
                    "prompt_hash": _hash(messages),
                    "candidate_plan": plan,
                    "validation": {
                        "workflow": workflow_validation,
                        "schedule": schedule_validation,
                        "capacity": capacity,
                        "simulation": simulation,
                    },
                    "action_results": action_results,
                    "result": result,
                },
            )
        )
        return result
