"""
Agenten-API FastAPI Router für Aufgabenverarbeitung.
"""

from __future__ import annotations

import logging
import os
import threading
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, TypedDict

import yaml
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class ExecutionSettings:
    """Konfiguration der Ausführungspipeline."""

    mode: str = "async"
    queue_ttl_seconds: int = 300
    heartbeat_interval_seconds: int = 10
    response_timeout_seconds: int = 30


@dataclass(frozen=True)
class LLMSettings:
    """Konfiguration des LLM-Clients."""

    provider: str = "local_stub"
    model: str = "local-agent"
    api_base_url: Optional[str] = None
    api_key: Optional[str] = None
    request_timeout_seconds: int = 15


@dataclass(frozen=True)
class AgentAPIConfig:
    """Gebündelte Agenten-API Konfiguration."""

    execution: ExecutionSettings
    llm: LLMSettings


class TaskRequest(BaseModel):
    """Eingangsmodell für eine Agenten-Aufgabe."""

    user_input: str = Field(..., description="Auslöser der Task in natürlicher Sprache.")
    context: Optional[Dict[str, Any]] = Field(default=None, description="Optionale Kontextdaten des Aufrufs.")
    metadata: Optional[Dict[str, Any]] = Field(default=None, description="Zusätzliche Metadaten zur Korrelation.")
    sync: Optional[bool] = Field(default=None, description="Erzwingt synchrone Verarbeitung, falls gesetzt.")


class TaskData(TypedDict):
    """Interne Darstellung eines Tasks im Speicher."""

    task_id: str
    status: str
    result: Optional[Dict[str, Any]]
    error: Optional[str]
    created_at: datetime
    updated_at: datetime
    expires_at: datetime
    last_heartbeat: datetime
    retry_after: int
    request: TaskRequest


def _utcnow() -> datetime:
    """Gibt die aktuelle UTC-Zeit zurück."""
    return datetime.now(timezone.utc)


def _to_iso(dt: datetime) -> str:
    """Formatiert Datumswerte als ISO8601-String mit Z-Suffix."""
    return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def _positive_int(value: Any, default: int) -> int:
    """Stellt sicher, dass ein Integer positiv ist, andernfalls gilt der Default."""
    try:
        candidate = int(value)
        if candidate > 0:
            return candidate
    except (TypeError, ValueError):
        pass
    return default


def _candidate_config_paths(module_path: Path) -> List[Path]:
    """Ermittelt potenzielle Konfigurationspfade gemäß Projektstandard."""
    candidates: List[Path] = []
    local_app_path = module_path.parent.parent
    candidates.append(local_app_path / "config" / "agent_api.yaml")
    for parent in module_path.parents:
        candidate = parent / "00_Globale_Richtlinien" / "Entworfener_Code" / "app" / "config" / "agent_api.yaml"
        if candidate not in candidates:
            candidates.append(candidate)
    return candidates


def _load_agent_config() -> AgentAPIConfig:
    """Lädt die Agenten-Konfiguration und führt sie mit Defaults zusammen."""
    module_path = Path(__file__).resolve()
    config_data: Dict[str, Any] = {}
    for path in _candidate_config_paths(module_path):
        if not path.exists():
            continue
        try:
            with path.open("r", encoding="utf-8") as handle:
                loaded = yaml.safe_load(handle) or {}
            config_data = loaded
            logger.debug("Agenten-API-Konfiguration aus %s geladen.", path)
            break
        except Exception as exc:
            logger.warning("Fehler beim Laden der Agenten-API-Konfiguration aus %s: %s", path, exc)
    execution_data = config_data.get("execution", {})
    llm_data = config_data.get("llm", {})
    mode = str(execution_data.get("mode", "async")).lower()
    if mode not in {"async", "sync"}:
        mode = "async"
    execution = ExecutionSettings(
        mode=mode,
        queue_ttl_seconds=_positive_int(execution_data.get("queue_ttl_seconds"), default=300),
        heartbeat_interval_seconds=_positive_int(execution_data.get("heartbeat_interval_seconds"), default=10),
        response_timeout_seconds=_positive_int(execution_data.get("response_timeout_seconds"), default=30),
    )
    env_api_key = os.getenv("AGENT_API_LLM_KEY")
    api_key = env_api_key if env_api_key else llm_data.get("api_key")
    llm = LLMSettings(
        provider=str(llm_data.get("provider", "local_stub")),
        model=str(llm_data.get("model", "local-agent")),
        api_base_url=llm_data.get("api_base_url"),
        api_key=str(api_key) if api_key else None,
        request_timeout_seconds=_positive_int(llm_data.get("request_timeout_seconds"), default=15),
    )
    return AgentAPIConfig(execution=execution, llm=llm)


CONFIG = _load_agent_config()


class TaskRegistry:
    """Thread-sichere Verwaltung der In-Memory-Tasks."""

    def __init__(self) -> None:
        self._tasks: Dict[str, TaskData] = {}
        self._lock = threading.Lock()

    def create(self, request: TaskRequest, ttl_seconds: int, retry_after: int) -> TaskData:
        """Erzeugt eine neue Task-Struktur und speichert sie."""
        now = _utcnow()
        task: TaskData = {
            "task_id": str(uuid.uuid4()),
            "status": "processing",
            "result": None,
            "error": None,
            "created_at": now,
            "updated_at": now,
            "expires_at": now + timedelta(seconds=ttl_seconds),
            "last_heartbeat": now,
            "retry_after": max(retry_after, 1),
            "request": request,
        }
        with self._lock:
            self._tasks[task["task_id"]] = task
            return task.copy()

    def get(self, task_id: str) -> Optional[TaskData]:
        """Liefert eine Kopie der Task und aktualisiert Expirationen."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None:
                return None
            self._expire_task_locked(task)
            return task.copy()

    def get_request(self, task_id: str) -> Optional[TaskRequest]:
        """Liefert das ursprüngliche Request-Objekt."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None:
                return None
            return task["request"]

    def heartbeat(self, task_id: str) -> None:
        """Aktualisiert den Heartbeat einer laufenden Task."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None or task["status"] != "processing":
                return
            now = _utcnow()
            task["last_heartbeat"] = now
            task["updated_at"] = now

    def mark_succeeded(self, task_id: str, result: Dict[str, Any], ttl_seconds: int) -> Optional[TaskData]:
        """Markiert eine Task als abgeschlossen."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None:
                return None
            if task["status"] in {"cancelled", "expired"}:
                return task.copy()
            now = _utcnow()
            task["status"] = "succeeded"
            task["result"] = result
            task["error"] = None
            task["updated_at"] = now
            task["last_heartbeat"] = now
            task["expires_at"] = now + timedelta(seconds=ttl_seconds)
            return task.copy()

    def mark_failed(self, task_id: str, error: str, ttl_seconds: int) -> Optional[TaskData]:
        """Markiert eine Task als fehlgeschlagen."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None:
                return None
            now = _utcnow()
            task["status"] = "failed"
            task["result"] = {"error": error}
            task["error"] = error
            task["updated_at"] = now
            task["last_heartbeat"] = now
            task["expires_at"] = now + timedelta(seconds=ttl_seconds)
            return task.copy()

    def cancel(self, task_id: str) -> Optional[TaskData]:
        """Bricht eine Task ab und markiert sie als cancelled."""
        with self._lock:
            task = self._tasks.get(task_id)
            if task is None:
                return None
            now = _utcnow()
            if task["status"] in {"succeeded", "failed", "expired", "cancelled"}:
                self._expire_task_locked(task)
                return task.copy()
            task["status"] = "cancelled"
            task["error"] = "cancelled by client"
            task["result"] = None
            task["updated_at"] = now
            task["last_heartbeat"] = now
            task["expires_at"] = now
            return task.copy()

    def _expire_task_locked(self, task: TaskData) -> None:
        """Setzt den Status auf expired, falls das Ablaufdatum erreicht ist."""
        now = _utcnow()
        if now < task["expires_at"] or task["status"] == "expired":
            return
        task["status"] = "expired"
        task["error"] = "expired"
        task["updated_at"] = now


REGISTRY = TaskRegistry()


def _local_stub_result(request: TaskRequest) -> Dict[str, Any]:
    """Lokale Fallback-Implementierung ohne externe LLM-Abhängigkeit."""
    return {
        "output": f"{request.user_input} [local_stub]",
        "provider": "local_stub",
        "metadata": {
            "mode": CONFIG.execution.mode,
            "context_present": bool(request.context),
            "metadata_present": bool(request.metadata),
        },
    }


def _invoke_llm(request: TaskRequest) -> Dict[str, Any]:
    """Ruft das konfigurierte LLM auf oder fällt auf die Stub-Implementierung zurück."""
    api_key = CONFIG.llm.api_key
    base_url = CONFIG.llm.api_base_url
    if not api_key or not base_url:
        return _local_stub_result(request)
    try:
        import httpx  # type: ignore
    except ModuleNotFoundError:
        logger.warning("httpx nicht verfügbar, fallback auf lokalen Stub.")
        return _local_stub_result(request)
    headers: Dict[str, str] = {}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"
    payload = {
        "model": CONFIG.llm.model,
        "input": request.user_input,
        "context": request.context or {},
        "metadata": request.metadata or {},
    }
    timeout = CONFIG.llm.request_timeout_seconds
    try:
        with httpx.Client(timeout=timeout) as client:
            response = client.post(str(base_url), json=payload, headers=headers)
            response.raise_for_status()
            data = response.json()
    except Exception as exc:
        logger.warning("LLM-Request fehlgeschlagen (%s), fallback auf Stub.", exc)
        return _local_stub_result(request)
    return {
        "output": data.get("output") or data.get("result") or data,
        "provider": CONFIG.llm.provider,
        "metadata": {
            "source": "llm",
            "model": CONFIG.llm.model,
        },
    }


def _execute_request(request: TaskRequest) -> Dict[str, Any]:
    """Führt Vorverarbeitung aus und delegiert an das LLM."""
    # Simulierter Pre-Processing-Schritt sowie Heartbeat-Update
    time.sleep(0.01)
    return _invoke_llm(request)


def _process_task(task_id: str) -> None:
    """Hintergrundverarbeitung einer Task."""
    request = REGISTRY.get_request(task_id)
    if request is None:
        return
    try:
        REGISTRY.heartbeat(task_id)
        result = _execute_request(request)
        REGISTRY.heartbeat(task_id)
        REGISTRY.mark_succeeded(task_id, result, CONFIG.execution.queue_ttl_seconds)
    except Exception as exc:
        logger.exception("Fehler bei der Bearbeitung von Task %s", task_id)
        REGISTRY.mark_failed(task_id, str(exc), CONFIG.execution.queue_ttl_seconds)


def _serialize_task(task: TaskData, include_result: bool = True, include_detail: bool = True) -> Dict[str, Any]:
    """Konvertiert den internen Task-Status in eine API-Antwort."""
    payload: Dict[str, Any] = {
        "task_id": task["task_id"],
        "status": task["status"],
        "expires_at": _to_iso(task["expires_at"]),
    }
    if task["status"] == "processing":
        payload["retry_after"] = task["retry_after"]
    if include_result and task["result"] is not None and task["status"] in {"succeeded", "failed"}:
        payload["result"] = task["result"]
    if include_detail and task.get("error"):
        payload["detail"] = task["error"]
    if include_detail and task["status"] == "cancelled" and "detail" not in payload:
        payload["detail"] = "cancelled"
    return payload


agent_router = APIRouter(prefix="/agent/v1", tags=["agent-api"])


@agent_router.post("/tasks")
def submit_task(request: TaskRequest) -> JSONResponse:
    """Erstellt eine neue Task und verarbeitet sie je nach Modus synchron oder asynchron."""
    effective_mode = CONFIG.execution.mode
    if request.sync is not None:
        effective_mode = "sync" if request.sync else "async"
    ttl = CONFIG.execution.queue_ttl_seconds
    retry_after = CONFIG.execution.heartbeat_interval_seconds
    task = REGISTRY.create(request, ttl_seconds=ttl, retry_after=retry_after)
    task_id = task["task_id"]
    worker = threading.Thread(
        target=_process_task,
        args=(task_id,),
        name=f"agent-task-{task_id}",
        daemon=True,
    )
    worker.start()
    if effective_mode == "sync":
        worker.join(timeout=CONFIG.execution.response_timeout_seconds)
        final_task = REGISTRY.get(task_id)
        if final_task is None:
            raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="task_not_found")
        if final_task["status"] == "processing":
            content = {
                "task_id": task_id,
                "status": "processing",
                "detail": "still_running",
                "expires_at": _to_iso(final_task["expires_at"]),
                "retry_after": final_task["retry_after"],
            }
            return JSONResponse(status_code=status.HTTP_408_REQUEST_TIMEOUT, content=content)
        return JSONResponse(status_code=status.HTTP_200_OK, content=_serialize_task(final_task))
    return JSONResponse(status_code=status.HTTP_202_ACCEPTED, content=_serialize_task(task, include_result=False, include_detail=False))


@agent_router.get("/tasks/{task_id}")
def get_task_status(task_id: str) -> Dict[str, Any]:
    """Liefert den Status einer bestehenden Task."""
    task = REGISTRY.get(task_id)
    if task is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="task_not_found")
    return _serialize_task(task)


@agent_router.post("/tasks/{task_id}/cancel")
def cancel_task(task_id: str) -> Dict[str, Any]:
    """Bricht eine laufende Task ab."""
    task = REGISTRY.cancel(task_id)
    if task is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="task_not_found")
    return _serialize_task(task)


__all__ = ["agent_router"]