from __future__ import annotations

import json
import re
from typing import Any, Mapping, Optional

import requests

from .base import EngineCapability, WorkflowEngine


class KestraAdapter(WorkflowEngine):
    engine_name = "kestra"
    capabilities = frozenset(EngineCapability)

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
        tenant_id: str = "main",
        timeout: int = 30,
        session: Optional[Any] = None,
    ):
        normalized_url = (base_url or "").strip().rstrip("/")
        if not normalized_url:
            raise ValueError("Kestra base_url is required")
        if not username or not password:
            raise ValueError("Kestra basic-auth credentials are required")
        self.base_url = normalized_url
        self.username = username
        self.password = password
        self.tenant_id = tenant_id.strip() or "main"
        self.timeout = timeout
        self.session = session or requests.Session()

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        response = self.session.request(
            method,
            f"{self.base_url}/{self.tenant_id}/{path.lstrip('/')}",
            auth=(self.username, self.password),
            timeout=self.timeout,
            **kwargs,
        )
        response.raise_for_status()
        if not response.content:
            return {}
        return response.json()

    @staticmethod
    def _yaml_headers() -> Mapping[str, str]:
        return {
            "Accept": "application/json",
            "Content-Type": "application/x-yaml",
        }

    @staticmethod
    def _ensure_disabled(definition: Any) -> str:
        if not isinstance(definition, str) or not definition.strip():
            raise ValueError("Kestra flow definition must be YAML text")
        source = definition.strip() + "\n"
        if not re.search(r"(?m)^disabled: true\s*$", source):
            raise ValueError(
                "Kestra deploy_disabled requires flow-level disabled: true"
            )
        if "disabled: false" in source:
            raise ValueError("Kestra deploy_disabled refuses enabled triggers")
        if "triggers:" in source and "disabled: true" not in source:
            raise ValueError("Kestra deploy_disabled requires disabled triggers")
        return source

    def validate(self, definition: Any) -> Mapping[str, Any]:
        source = definition if isinstance(definition, str) else ""
        if not source.strip():
            raise ValueError("Kestra flow definition must be YAML text")
        return self._request(
            "POST",
            "flows/validate",
            headers=self._yaml_headers(),
            data=source,
        )

    def deploy_disabled(self, definition: Any) -> Mapping[str, Any]:
        source = self._ensure_disabled(definition)
        self.validate(source)
        return self._request(
            "POST",
            "flows",
            headers=self._yaml_headers(),
            data=source,
        )

    def activate(self, namespace: str, definition_id: str) -> Mapping[str, Any]:
        return self._request(
            "POST",
            "flows/enable/by-ids",
            json=[{"namespace": namespace, "id": definition_id}],
        )

    def deactivate(self, namespace: str, definition_id: str) -> Mapping[str, Any]:
        return self._request(
            "POST",
            "flows/disable/by-ids",
            json=[{"namespace": namespace, "id": definition_id}],
        )

    @staticmethod
    def _input_value(value: Any) -> str:
        if isinstance(value, str):
            return value
        if value is None:
            return ""
        return json.dumps(value, ensure_ascii=False, separators=(",", ":"))

    def execute(
        self,
        namespace: str,
        definition_id: str,
        inputs: Optional[Mapping[str, Any]] = None,
        **options: Any,
    ) -> Mapping[str, Any]:
        files = {
            key: (None, self._input_value(value))
            for key, value in sorted((inputs or {}).items())
        }
        return self._request(
            "POST",
            f"executions/{namespace}/{definition_id}",
            files=files,
        )

    def pause(self, execution_id: str) -> Mapping[str, Any]:
        return self._request("POST", f"executions/{execution_id}/pause")

    def kill(self, execution_id: str) -> Mapping[str, Any]:
        return self._request("DELETE", f"executions/{execution_id}/kill")

    def replay(self, execution_id: str) -> Mapping[str, Any]:
        return self._request("POST", f"executions/{execution_id}/replay")

    def backfill(
        self,
        namespace: str,
        definition_id: str,
        trigger_id: str,
        start: str,
        end: Optional[str],
    ) -> Mapping[str, Any]:
        return self._request(
            "PUT",
            "triggers",
            json={
                "namespace": namespace,
                "flowId": definition_id,
                "triggerId": trigger_id,
                "backfill": {"start": start, "end": end},
            },
        )

    def get_execution(self, execution_id: str) -> Mapping[str, Any]:
        return self._request("GET", f"executions/{execution_id}")

    def get_logs(self, execution_id: str) -> Any:
        return self._request("GET", f"logs/{execution_id}")
