from collections.abc import Mapping
from typing import Annotated, Protocol
from urllib.parse import quote, urlparse

import httpx
from fastapi import Depends
from pydantic import BaseModel, Field

from .config import Settings, get_settings
from .errors import AppError


class ComputeResult(BaseModel):
    provider_request_id: str = Field(min_length=1, max_length=160)
    provider_resource_id: str = Field(min_length=1, max_length=255)
    status: str = Field(min_length=1, max_length=24)
    ip_address: str | None = Field(default=None, max_length=64)
    control_endpoint: str | None = Field(default=None, max_length=255)
    browser_status: str | None = Field(default=None, max_length=40)
    metrics: dict = Field(default_factory=dict)


class SnapshotResult(BaseModel):
    provider_request_id: str = Field(min_length=1, max_length=160)
    provider_snapshot_id: str = Field(min_length=1, max_length=255)
    status: str = Field(min_length=1, max_length=24)


class ConsoleResult(BaseModel):
    provider_request_id: str = Field(min_length=1, max_length=160)
    console_path: str = Field(min_length=1, max_length=255)
    expires_in_seconds: int = Field(ge=1, le=300)


class ComputeProvider(Protocol):
    name: str
    configured: bool

    async def create(self, spec: Mapping, *, idempotency_key: str) -> ComputeResult: ...
    async def start(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def stop(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def restart(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def pause(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def resume(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def delete(self, resource_id: str, *, idempotency_key: str) -> ComputeResult: ...
    async def snapshot(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> SnapshotResult: ...
    async def restore(
        self, resource_id: str, snapshot_id: str, *, idempotency_key: str
    ) -> ComputeResult: ...
    async def resize(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> ComputeResult: ...
    async def get_status(self, resource_id: str) -> ComputeResult: ...
    async def get_metrics(self, resource_id: str) -> ComputeResult: ...
    async def get_console(self, resource_id: str) -> ConsoleResult: ...


class UnconfiguredComputeProvider:
    name = "unconfigured"
    configured = False

    @staticmethod
    def _raise():
        raise AppError(
            "COMPUTE_PROVIDER_UNCONFIGURED",
            "A real virtual-machine provider has not been configured.",
            503,
        )

    async def create(self, spec: Mapping, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def start(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def stop(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def restart(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def pause(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def resume(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def delete(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        self._raise()

    async def snapshot(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> SnapshotResult:
        self._raise()

    async def restore(
        self, resource_id: str, snapshot_id: str, *, idempotency_key: str
    ) -> ComputeResult:
        self._raise()

    async def resize(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> ComputeResult:
        self._raise()

    async def get_status(self, resource_id: str) -> ComputeResult:
        self._raise()

    async def get_metrics(self, resource_id: str) -> ComputeResult:
        self._raise()

    async def get_console(self, resource_id: str) -> ConsoleResult:
        self._raise()


class GatewayComputeProvider:
    configured = True

    def __init__(self, settings: Settings):
        self.name = settings.compute_provider
        self._url = (settings.compute_provider_url or "").rstrip("/")
        self._token = settings.read_compute_provider_token() or ""
        self._timeout = settings.compute_provider_timeout_seconds
        parsed = urlparse(self._url)
        if (
            not self._url
            or not self._token
            or not parsed.hostname
            or parsed.username
            or parsed.password
            or parsed.scheme not in ({"https"} if settings.app_env == "production" else {"http", "https"})
        ):
            raise AppError(
                "COMPUTE_PROVIDER_UNCONFIGURED",
                "The virtual-machine provider gateway is not safely configured.",
                503,
            )

    async def _request(
        self,
        method: str,
        path: str,
        *,
        payload: Mapping | None = None,
        idempotency_key: str | None = None,
        result_type: type[ComputeResult] | type[SnapshotResult] | type[ConsoleResult],
    ):
        headers = {"authorization": f"Bearer {self._token}"}
        if idempotency_key:
            headers["idempotency-key"] = idempotency_key
        try:
            async with httpx.AsyncClient(timeout=self._timeout) as client:
                response = await client.request(
                    method, f"{self._url}{path}", headers=headers, json=payload
                )
        except httpx.HTTPError as error:
            raise AppError(
                "COMPUTE_PROVIDER_UNAVAILABLE",
                "The virtual-machine provider could not be reached.",
                503,
            ) from error
        if response.status_code >= 400:
            raise AppError(
                "COMPUTE_PROVIDER_FAILED",
                "The virtual-machine provider rejected the operation.",
                502,
            )
        try:
            return result_type.model_validate(response.json())
        except (ValueError, TypeError) as error:
            raise AppError(
                "COMPUTE_PROVIDER_RESPONSE_INVALID",
                "The virtual-machine provider returned an invalid response.",
                502,
            ) from error

    async def create(self, spec: Mapping, *, idempotency_key: str) -> ComputeResult:
        return await self._request(
            "POST", "/v1/computers", payload=spec, idempotency_key=idempotency_key,
            result_type=ComputeResult,
        )

    async def _lifecycle(
        self, resource_id: str, action: str, idempotency_key: str
    ) -> ComputeResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "POST", f"/v1/computers/{resource_segment}/{action}", payload={},
            idempotency_key=idempotency_key, result_type=ComputeResult,
        )

    async def start(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "start", idempotency_key)

    async def stop(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "stop", idempotency_key)

    async def restart(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "restart", idempotency_key)

    async def pause(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "pause", idempotency_key)

    async def resume(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "resume", idempotency_key)

    async def delete(self, resource_id: str, *, idempotency_key: str) -> ComputeResult:
        return await self._lifecycle(resource_id, "delete", idempotency_key)

    async def snapshot(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> SnapshotResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "POST", f"/v1/computers/{resource_segment}/snapshots", payload=spec,
            idempotency_key=idempotency_key, result_type=SnapshotResult,
        )

    async def restore(
        self, resource_id: str, snapshot_id: str, *, idempotency_key: str
    ) -> ComputeResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "POST", f"/v1/computers/{resource_segment}/restore",
            payload={"snapshot_id": snapshot_id}, idempotency_key=idempotency_key,
            result_type=ComputeResult,
        )

    async def resize(
        self, resource_id: str, spec: Mapping, *, idempotency_key: str
    ) -> ComputeResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "POST", f"/v1/computers/{resource_segment}/resize", payload=spec,
            idempotency_key=idempotency_key, result_type=ComputeResult,
        )

    async def get_status(self, resource_id: str) -> ComputeResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "GET", f"/v1/computers/{resource_segment}", result_type=ComputeResult
        )

    async def get_metrics(self, resource_id: str) -> ComputeResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "GET", f"/v1/computers/{resource_segment}/metrics", result_type=ComputeResult
        )

    async def get_console(self, resource_id: str) -> ConsoleResult:
        resource_segment = quote(resource_id, safe="")
        return await self._request(
            "POST", f"/v1/computers/{resource_segment}/console", payload={},
            result_type=ConsoleResult,
        )


def get_compute_provider() -> ComputeProvider:
    settings = get_settings()
    if (
        settings.compute_provider == "unconfigured"
        or not settings.compute_provider_url
        or not settings.read_compute_provider_token()
    ):
        return UnconfiguredComputeProvider()
    try:
        return GatewayComputeProvider(settings)
    except AppError:
        return UnconfiguredComputeProvider()


ComputeProviderDep = Annotated[ComputeProvider, Depends(get_compute_provider)]
