from __future__ import annotations

import copy
import json
from datetime import datetime
from typing import Any

from app.core.common.identifiers import ensure_governance_uid
from app.core.orchestration.spec import (
    SCHEDULE_PLAN_SCHEMA,
    WORKFLOW_SPEC_SCHEMA,
    validate_schedule_plan,
    validate_workflow_spec,
)

PLANNER_SCHEMA_VERSION = "1.0"
MODEL_PLAN_KEYS = {
    "schema_version",
    "workflow_spec",
    "schedule_plan",
    "decision_summary",
}
REQUEST_KEYS = {
    "business_goal",
    "dataflow_uid",
    "business_domain",
    "environment",
    "available_window",
    "authorized_resources",
    "canary_inputs",
    "baseline_execution_id",
    "target_conflicts",
}

MODEL_PLAN_SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://dataops.local/schemas/ai-schedule-plan-1.0.json",
    "title": "DataOps AI Schedule Candidate",
    "type": "object",
    "additionalProperties": False,
    "required": [
        "schema_version",
        "workflow_spec",
        "schedule_plan",
        "decision_summary",
    ],
    "properties": {
        "schema_version": {"const": PLANNER_SCHEMA_VERSION},
        "workflow_spec": WORKFLOW_SPEC_SCHEMA,
        "schedule_plan": SCHEDULE_PLAN_SCHEMA,
        "decision_summary": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
        },
    },
}


def _required_string(value: Any, label: str, maximum: int = 1000) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{label} is required")
    normalized = value.strip()
    if len(normalized) > maximum:
        raise ValueError(f"{label} exceeds {maximum} characters")
    return normalized


def _uid(value: Any, label: str) -> str:
    try:
        return ensure_governance_uid({"uid": str(value)})
    except ValueError as exc:
        raise ValueError(f"{label} must be a valid UUIDv7") from exc


def _timestamp(value: Any, label: str) -> datetime:
    source = _required_string(value, label, 100)
    if source.endswith("Z"):
        source = f"{source[:-1]}+00:00"
    try:
        parsed = datetime.fromisoformat(source)
    except ValueError as exc:
        raise ValueError(f"{label} must be an ISO-8601 timestamp") from exc
    if parsed.tzinfo is None:
        raise ValueError(f"{label} must include a timezone")
    return parsed


def validate_planning_request(value: Any) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError("planning request must be an object")
    unknown = sorted(set(value) - REQUEST_KEYS)
    if unknown:
        raise ValueError(
            f"planning request contains unsupported fields: {', '.join(unknown)}"
        )
    request = copy.deepcopy(value)
    request["business_goal"] = _required_string(
        request.get("business_goal"), "business goal", 2000
    )
    request["dataflow_uid"] = _uid(request.get("dataflow_uid"), "dataflow_uid")
    request["business_domain"] = _required_string(
        request.get("business_domain"), "business domain", 200
    )
    request["environment"] = _required_string(
        request.get("environment"), "environment", 20
    )
    if request["environment"] not in {"development", "test", "production"}:
        raise ValueError("unsupported environment")
    window = request.get("available_window")
    if not isinstance(window, dict) or set(window) != {"start", "end"}:
        raise ValueError("available window must contain only start and end")
    start = _timestamp(window["start"], "available window start")
    end = _timestamp(window["end"], "available window end")
    if end <= start:
        raise ValueError("available window end must be after start")
    resources = request.get("authorized_resources")
    if not isinstance(resources, list) or not resources or len(resources) > 100:
        raise ValueError("authorized resources must be a non-empty bounded array")
    request["authorized_resources"] = sorted(
        {_uid(item, "authorized resource") for item in resources}
    )
    canary_inputs = request.get("canary_inputs", {})
    if not isinstance(canary_inputs, dict) or len(canary_inputs) > 100:
        raise ValueError("canary inputs must be a bounded object")
    try:
        encoded_inputs = json.dumps(canary_inputs, ensure_ascii=False)
    except (TypeError, ValueError) as exc:
        raise ValueError("canary inputs must be JSON serializable") from exc
    if len(encoded_inputs.encode("utf-8")) > 65536:
        raise ValueError("canary inputs exceed size limit")
    request["canary_inputs"] = canary_inputs
    if "baseline_execution_id" in request:
        request["baseline_execution_id"] = _required_string(
            request["baseline_execution_id"], "baseline execution id", 255
        )
    conflicts = request.get("target_conflicts", [])
    if not isinstance(conflicts, list) or len(conflicts) > 20:
        raise ValueError("target conflicts must be a bounded array")
    request["target_conflicts"] = [
        _required_string(item, "target conflict", 500) for item in conflicts
    ]
    return request


def validate_model_plan(
    value: Any,
    *,
    dataflow_uid: str,
    authorized_resources: list[str],
) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError("model plan must be an object")
    unknown = sorted(set(value) - MODEL_PLAN_KEYS)
    if unknown:
        raise ValueError(
            f"model plan contains unsupported fields: {', '.join(unknown)}"
        )
    if set(value) != MODEL_PLAN_KEYS:
        missing = sorted(MODEL_PLAN_KEYS - set(value))
        raise ValueError(f"model plan is missing fields: {', '.join(missing)}")
    if value.get("schema_version") != PLANNER_SCHEMA_VERSION:
        raise ValueError(f"model plan schema_version must be {PLANNER_SCHEMA_VERSION}")
    workflow = validate_workflow_spec(value.get("workflow_spec"))
    schedule = validate_schedule_plan(value.get("schedule_plan"))
    if workflow["dataflow_uid"] != dataflow_uid:
        raise ValueError("model plan dataflow_uid does not match request")
    allowed = set(authorized_resources)
    for node in workflow["nodes"]:
        if node.get("purpose") == "write" or node["type"] == "sql.execute":
            raise ValueError("V54 canary plans must be read-only")
        source_uid = node.get("data_source_uid")
        if source_uid and source_uid not in allowed:
            raise PermissionError("model plan references an unauthorized resource")
    return {
        "schema_version": PLANNER_SCHEMA_VERSION,
        "workflow_spec": workflow,
        "schedule_plan": schedule,
        "decision_summary": _required_string(
            value.get("decision_summary"), "decision summary", 1000
        ),
    }
