from __future__ import annotations

import asyncio
import dataclasses
import os
import time
from dataclasses import asdict
from typing import Any

import httpx

from .exceptions import PreclinicalError
from .types import (
    Agent,
    CreateAgentParams,
    CreateTestParams,
    CreateWebhookParams,
    ListParams,
    PaginatedResponse,
    PollOptions,
    Scenario,
    ScenarioResult,
    ScenarioRun,
    TestRun,
    TestRunCreated,
    UpdateAgentParams,
    UpdateWebhookParams,
    ValidationResult,
    Webhook,
    WebhookDelivery,
    WebhookTestResult,
)

DEFAULT_BASE_URL = "https://preclinical-web.fly.dev"
SDK_VERSION = "0.1.0"


def _strip_none(d: dict[str, Any]) -> dict[str, Any]:
    return {k: v for k, v in d.items() if v is not None}


def _params_dict(params: Any) -> dict[str, Any]:
    return _strip_none(asdict(params))


def _from_dict(cls: type, data: dict[str, Any]) -> Any:
    """Construct a dataclass from a dict, ignoring unknown fields.

    This ensures forward-compatibility: if the API adds new fields,
    the SDK won't crash with ``TypeError: unexpected keyword argument``.
    """
    known = {f.name for f in dataclasses.fields(cls)}
    return cls(**{k: v for k, v in data.items() if k in known})


def _build_headers(api_key: str) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {api_key}",
        "User-Agent": f"preclinical-python/{SDK_VERSION}",
    }


def _handle_response(response: httpx.Response) -> Any:
    if response.status_code == 204:
        return None
    if not response.is_success:
        try:
            error_data = response.json()
        except Exception:
            error_data = {"error": response.reason_phrase}
        raise PreclinicalError(
            error_data.get("error", f"Request failed: {response.status_code}"),
            response.status_code,
            error_data.get("code"),
        )
    return response.json()


# ---------------------------------------------------------------------------
# Sync API resource classes
# ---------------------------------------------------------------------------


class _AgentsAPI:
    def __init__(self, client: Preclinical) -> None:
        self._client = client

    def list(self) -> list[Agent]:
        data = self._client._request("GET", "/agents")
        return [_from_dict(Agent, a) for a in data["agents"]]

    def get(self, agent_id: str) -> Agent:
        return _from_dict(Agent, self._client._request("GET", f"/agents/{agent_id}"))

    def create(self, params: CreateAgentParams) -> Agent:
        return _from_dict(Agent, self._client._request("POST", "/agents", json=_params_dict(params)))

    def update(self, agent_id: str, params: UpdateAgentParams) -> Agent:
        return _from_dict(Agent, self._client._request("PATCH", f"/agents/{agent_id}", json=_params_dict(params)))

    def delete(self, agent_id: str) -> None:
        self._client._request("DELETE", f"/agents/{agent_id}")

    def validate(self, agent_id: str) -> ValidationResult:
        return _from_dict(ValidationResult, self._client._request("POST", f"/agents/{agent_id}/validate"))


class _TestsAPI:
    def __init__(self, client: Preclinical) -> None:
        self._client = client

    def create(self, params: CreateTestParams) -> TestRunCreated:
        return _from_dict(TestRunCreated, self._client._request("POST", "/tests", json=_params_dict(params)))

    def get(self, test_id: str) -> TestRun:
        return _from_dict(TestRun, self._client._request("GET", f"/tests/{test_id}"))

    def list(self, params: ListParams | None = None) -> PaginatedResponse[TestRun]:
        query = _params_dict(params) if params else {}
        data = self._client._request("GET", "/tests", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(TestRun, t) for t in data["tests"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    def cancel(self, test_id: str) -> None:
        self._client._request("POST", f"/tests/{test_id}/cancel")

    def results(self, test_id: str, params: ListParams | None = None) -> PaginatedResponse[ScenarioResult]:
        query = _params_dict(params) if params else {}
        data = self._client._request("GET", f"/tests/{test_id}/results", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(ScenarioResult, r) for r in data["results"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    def run_and_wait(self, params: CreateTestParams, options: PollOptions | None = None) -> TestRun:
        """Create a test and poll until it completes, fails, or is canceled."""
        opts = options or PollOptions()
        created = self.create(params)
        deadline = time.monotonic() + opts.timeout_seconds

        while time.monotonic() < deadline:
            time.sleep(opts.interval_seconds)
            test = self.get(created.id)
            if opts.on_progress:
                opts.on_progress(test)
            if test.status in ("completed", "failed", "canceled"):
                return test

        raise PreclinicalError("Test timed out", 408, "TIMEOUT")


class _ScenarioRunsAPI:
    def __init__(self, client: Preclinical) -> None:
        self._client = client

    def list(self, test_id: str, params: ListParams | None = None) -> PaginatedResponse[ScenarioResult]:
        query: dict[str, str] = {"test_id": test_id}
        if params:
            query.update({k: str(v) for k, v in _params_dict(params).items()})
        data = self._client._request("GET", "/scenario-runs", params=query)
        return PaginatedResponse(
            data=[_from_dict(ScenarioResult, r) for r in data["scenario_runs"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    def get(self, scenario_run_id: str) -> ScenarioRun:
        return _from_dict(ScenarioRun, self._client._request("GET", f"/scenario-runs/{scenario_run_id}"))


class _ScenariosAPI:
    def __init__(self, client: Preclinical) -> None:
        self._client = client

    def list(self, params: ListParams | None = None) -> PaginatedResponse[Scenario]:
        query = _params_dict(params) if params else {}
        data = self._client._request("GET", "/scenarios", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(Scenario, s) for s in data["scenarios"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    def get(self, scenario_id: str) -> Scenario:
        return _from_dict(Scenario, self._client._request("GET", f"/scenarios/{scenario_id}"))


class _WebhooksAPI:
    def __init__(self, client: Preclinical) -> None:
        self._client = client

    def list(self) -> list[Webhook]:
        data = self._client._request("GET", "/webhooks")
        return [_from_dict(Webhook, w) for w in data["webhooks"]]

    def create(self, params: CreateWebhookParams) -> Webhook:
        return _from_dict(Webhook, self._client._request("POST", "/webhooks", json=_params_dict(params)))

    def get(self, webhook_id: str) -> Webhook:
        return _from_dict(Webhook, self._client._request("GET", f"/webhooks/{webhook_id}"))

    def update(self, webhook_id: str, params: UpdateWebhookParams) -> Webhook:
        return _from_dict(Webhook, self._client._request("PATCH", f"/webhooks/{webhook_id}", json=_params_dict(params)))

    def delete(self, webhook_id: str) -> None:
        self._client._request("DELETE", f"/webhooks/{webhook_id}")

    def test(self, webhook_id: str) -> WebhookTestResult:
        return _from_dict(WebhookTestResult, self._client._request("POST", f"/webhooks/{webhook_id}/test"))

    def deliveries(self, webhook_id: str) -> list[WebhookDelivery]:
        data = self._client._request("GET", f"/webhooks/{webhook_id}/deliveries")
        return [_from_dict(WebhookDelivery, d) for d in data["deliveries"]]


# ---------------------------------------------------------------------------
# Sync client
# ---------------------------------------------------------------------------


class Preclinical:
    """Synchronous Python client for the Preclinical API."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ) -> None:
        api_key = api_key or os.environ.get("PRECLINICAL_API_KEY", "")
        if not api_key:
            raise ValueError(
                "api_key is required — pass it directly or set the PRECLINICAL_API_KEY environment variable"
            )

        self._base_url = base_url.rstrip("/")
        self._http = httpx.Client(
            base_url=f"{self._base_url}/api/v1",
            headers=_build_headers(api_key),
            timeout=timeout,
        )

        self.agents = _AgentsAPI(self)
        self.tests = _TestsAPI(self)
        self.scenario_runs = _ScenarioRunsAPI(self)
        self.scenarios = _ScenariosAPI(self)
        self.webhooks = _WebhooksAPI(self)

    def _request(
        self,
        method: str,
        path: str,
        json: dict[str, Any] | None = None,
        params: dict[str, str] | None = None,
    ) -> Any:
        headers = {"Content-Type": "application/json"} if json is not None else None
        response = self._http.request(method, path, json=json, params=params, headers=headers)
        return _handle_response(response)

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> Preclinical:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()


# ---------------------------------------------------------------------------
# Async API resource classes
# ---------------------------------------------------------------------------


class _AsyncAgentsAPI:
    def __init__(self, client: AsyncPreclinical) -> None:
        self._client = client

    async def list(self) -> list[Agent]:
        data = await self._client._request("GET", "/agents")
        return [_from_dict(Agent, a) for a in data["agents"]]

    async def get(self, agent_id: str) -> Agent:
        return _from_dict(Agent, await self._client._request("GET", f"/agents/{agent_id}"))

    async def create(self, params: CreateAgentParams) -> Agent:
        return _from_dict(Agent, await self._client._request("POST", "/agents", json=_params_dict(params)))

    async def update(self, agent_id: str, params: UpdateAgentParams) -> Agent:
        return _from_dict(Agent, await self._client._request("PATCH", f"/agents/{agent_id}", json=_params_dict(params)))

    async def delete(self, agent_id: str) -> None:
        await self._client._request("DELETE", f"/agents/{agent_id}")

    async def validate(self, agent_id: str) -> ValidationResult:
        return _from_dict(ValidationResult, await self._client._request("POST", f"/agents/{agent_id}/validate"))


class _AsyncTestsAPI:
    def __init__(self, client: AsyncPreclinical) -> None:
        self._client = client

    async def create(self, params: CreateTestParams) -> TestRunCreated:
        return _from_dict(TestRunCreated, await self._client._request("POST", "/tests", json=_params_dict(params)))

    async def get(self, test_id: str) -> TestRun:
        return _from_dict(TestRun, await self._client._request("GET", f"/tests/{test_id}"))

    async def list(self, params: ListParams | None = None) -> PaginatedResponse[TestRun]:
        query = _params_dict(params) if params else {}
        data = await self._client._request("GET", "/tests", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(TestRun, t) for t in data["tests"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    async def cancel(self, test_id: str) -> None:
        await self._client._request("POST", f"/tests/{test_id}/cancel")

    async def results(self, test_id: str, params: ListParams | None = None) -> PaginatedResponse[ScenarioResult]:
        query = _params_dict(params) if params else {}
        data = await self._client._request("GET", f"/tests/{test_id}/results", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(ScenarioResult, r) for r in data["results"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    async def run_and_wait(self, params: CreateTestParams, options: PollOptions | None = None) -> TestRun:
        """Create a test and poll until it completes, fails, or is canceled."""
        opts = options or PollOptions()
        created = await self.create(params)
        deadline = time.monotonic() + opts.timeout_seconds

        while time.monotonic() < deadline:
            await asyncio.sleep(opts.interval_seconds)
            test = await self.get(created.id)
            if opts.on_progress:
                opts.on_progress(test)
            if test.status in ("completed", "failed", "canceled"):
                return test

        raise PreclinicalError("Test timed out", 408, "TIMEOUT")


class _AsyncScenarioRunsAPI:
    def __init__(self, client: AsyncPreclinical) -> None:
        self._client = client

    async def list(self, test_id: str, params: ListParams | None = None) -> PaginatedResponse[ScenarioResult]:
        query: dict[str, str] = {"test_id": test_id}
        if params:
            query.update({k: str(v) for k, v in _params_dict(params).items()})
        data = await self._client._request("GET", "/scenario-runs", params=query)
        return PaginatedResponse(
            data=[_from_dict(ScenarioResult, r) for r in data["scenario_runs"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    async def get(self, scenario_run_id: str) -> ScenarioRun:
        return _from_dict(ScenarioRun, await self._client._request("GET", f"/scenario-runs/{scenario_run_id}"))


class _AsyncScenariosAPI:
    def __init__(self, client: AsyncPreclinical) -> None:
        self._client = client

    async def list(self, params: ListParams | None = None) -> PaginatedResponse[Scenario]:
        query = _params_dict(params) if params else {}
        data = await self._client._request("GET", "/scenarios", params={k: str(v) for k, v in query.items()})
        return PaginatedResponse(
            data=[_from_dict(Scenario, s) for s in data["scenarios"]],
            total=data["total"],
            limit=data["limit"],
            offset=data["offset"],
        )

    async def get(self, scenario_id: str) -> Scenario:
        return _from_dict(Scenario, await self._client._request("GET", f"/scenarios/{scenario_id}"))


class _AsyncWebhooksAPI:
    def __init__(self, client: AsyncPreclinical) -> None:
        self._client = client

    async def list(self) -> list[Webhook]:
        data = await self._client._request("GET", "/webhooks")
        return [_from_dict(Webhook, w) for w in data["webhooks"]]

    async def create(self, params: CreateWebhookParams) -> Webhook:
        return _from_dict(Webhook, await self._client._request("POST", "/webhooks", json=_params_dict(params)))

    async def get(self, webhook_id: str) -> Webhook:
        return _from_dict(Webhook, await self._client._request("GET", f"/webhooks/{webhook_id}"))

    async def update(self, webhook_id: str, params: UpdateWebhookParams) -> Webhook:
        return _from_dict(Webhook, await self._client._request("PATCH", f"/webhooks/{webhook_id}", json=_params_dict(params)))

    async def delete(self, webhook_id: str) -> None:
        await self._client._request("DELETE", f"/webhooks/{webhook_id}")

    async def test(self, webhook_id: str) -> WebhookTestResult:
        return _from_dict(WebhookTestResult, await self._client._request("POST", f"/webhooks/{webhook_id}/test"))

    async def deliveries(self, webhook_id: str) -> list[WebhookDelivery]:
        data = await self._client._request("GET", f"/webhooks/{webhook_id}/deliveries")
        return [_from_dict(WebhookDelivery, d) for d in data["deliveries"]]


# ---------------------------------------------------------------------------
# Async client
# ---------------------------------------------------------------------------


class AsyncPreclinical:
    """Async Python client for the Preclinical API."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ) -> None:
        api_key = api_key or os.environ.get("PRECLINICAL_API_KEY", "")
        if not api_key:
            raise ValueError(
                "api_key is required — pass it directly or set the PRECLINICAL_API_KEY environment variable"
            )

        self._base_url = base_url.rstrip("/")
        self._http = httpx.AsyncClient(
            base_url=f"{self._base_url}/api/v1",
            headers=_build_headers(api_key),
            timeout=timeout,
        )

        self.agents = _AsyncAgentsAPI(self)
        self.tests = _AsyncTestsAPI(self)
        self.scenario_runs = _AsyncScenarioRunsAPI(self)
        self.scenarios = _AsyncScenariosAPI(self)
        self.webhooks = _AsyncWebhooksAPI(self)

    async def _request(
        self,
        method: str,
        path: str,
        json: dict[str, Any] | None = None,
        params: dict[str, str] | None = None,
    ) -> Any:
        headers = {"Content-Type": "application/json"} if json is not None else None
        response = await self._http.request(method, path, json=json, params=params, headers=headers)
        return _handle_response(response)

    async def close(self) -> None:
        await self._http.aclose()

    async def __aenter__(self) -> AsyncPreclinical:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()
