from __future__ import annotations

import time
from collections.abc import Callable

import httpx

from ..domain import build_create_payload
from ..errors import (
    ProxmoxApiError,
    ProxmoxAuthError,
    ProxmoxConnectError,
    ProxmoxError,
    ProxmoxPostCreateError,
    ProxmoxTlsError,
    ProxmoxTransportError,
    ProxmoxUnexpectedResponseError,
)
from ..models.workflow import VmConfig
from ..settings import AppSettings
from .base import (
    AuthenticatedSession,
    Bridge,
    IsoImage,
    Node,
    Pool,
    ProxmoxService,
    Realm,
    Storage,
    VmCreationResult,
)


def _looks_like_tls_error(message: str) -> bool:
    upper = message.upper()
    indicators = ("SSL", "TLS", "CERTIFICATE", "WRONG_VERSION", "EOF")
    return any(token in upper for token in indicators)


class ProxmoxApiClient:
    def __init__(
        self,
        settings: AppSettings,
        *,
        transport: httpx.BaseTransport | None = None,
        client_factory: Callable[..., httpx.Client] | None = None,
    ) -> None:
        settings.validate_live_endpoint_requirements()
        self._settings = settings
        factory = client_factory or httpx.Client
        self._client = factory(
            base_url=settings.api_url,
            verify=settings.proxmox_verify_tls,
            timeout=settings.request_timeout_seconds,
            follow_redirects=True,
            transport=transport,
        )
        self._ticket: str | None = None
        self._csrf_token: str | None = None

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

    def probe_transport(self) -> str:
        try:
            response = self._client.get(self._settings.proxmox_url or "/")
            return f"HTTP {response.status_code}"
        except httpx.ConnectError as exc:
            raise ProxmoxConnectError("Unable to connect to the Proxmox host.") from exc
        except httpx.TransportError as exc:
            message = str(exc)
            if _looks_like_tls_error(message):
                raise ProxmoxTlsError("TLS handshake or verification failed.") from exc
            raise ProxmoxTransportError("Transport error while probing Proxmox.") from exc

    def check_api_base(self) -> str:
        payload = self._request_json("GET", "/access/domains")
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("API base check did not return a realms list.")
        return "access/domains"

    def login(self, username: str, password: str, realm: str) -> AuthenticatedSession:
        full_username = username if "@" in username else f"{username}@{realm}"
        payload = self._request_json(
            "POST",
            "/access/ticket",
            data={"username": full_username, "password": password},
        )
        ticket = payload.get("ticket")
        csrf_token = payload.get("CSRFPreventionToken")
        if not isinstance(ticket, str) or not ticket:
            raise ProxmoxUnexpectedResponseError("Login response did not include a ticket.")
        self._ticket = ticket
        self._csrf_token = csrf_token if isinstance(csrf_token, str) else None
        self._client.cookies.set("PVEAuthCookie", ticket)
        return AuthenticatedSession(
            username=full_username,
            ticket=ticket,
            csrf_token=self._csrf_token,
        )

    def load_realms(self) -> list[Realm]:
        payload = self._request_json("GET", "/access/domains")
        realms: list[Realm] = []
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Realms payload was not a list.")
        for item in payload:
            if not isinstance(item, dict):
                continue
            realm = item.get("realm")
            title = item.get("comment") or item.get("commentary") or realm
            if isinstance(realm, str) and isinstance(title, str):
                realms.append(
                    Realm(
                        name=realm,
                        title=title,
                        default=bool(item.get("default")),
                    )
                )
        return realms

    def load_nodes(self) -> list[Node]:
        payload = self._request_json("GET", "/nodes", requires_auth=True)
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Nodes payload was not a list.")
        return [
            Node(name=item["node"], status=item.get("status"))
            for item in payload
            if isinstance(item, dict) and isinstance(item.get("node"), str)
        ]

    def load_next_vmid(self) -> int:
        payload = self._request_json("GET", "/cluster/nextid", requires_auth=True)
        if isinstance(payload, int):
            return payload
        if isinstance(payload, str) and payload.isdigit():
            return int(payload)
        raise ProxmoxUnexpectedResponseError("Next VM ID payload was not an integer.")

    def load_pools(self) -> list[Pool]:
        payload = self._request_json("GET", "/pools", requires_auth=True)
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Pools payload was not a list.")
        return [
            Pool(poolid=item["poolid"], comment=item.get("comment"))
            for item in payload
            if isinstance(item, dict) and isinstance(item.get("poolid"), str)
        ]

    def load_existing_tags(self) -> list[str]:
        payload = self._request_json(
            "GET",
            "/cluster/resources",
            params={"type": "vm"},
            requires_auth=True,
        )
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Cluster resource payload was not a list.")
        tags: set[str] = set()
        for item in payload:
            if not isinstance(item, dict):
                continue
            raw_tags = item.get("tags")
            if not isinstance(raw_tags, str):
                continue
            for tag in raw_tags.replace(",", ";").split(";"):
                normalized = tag.strip()
                if normalized:
                    tags.add(normalized)
        return sorted(tags)

    def load_bridges(self, node: str) -> list[Bridge]:
        payload = self._request_json("GET", f"/nodes/{node}/network", requires_auth=True)
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Network payload was not a list.")
        bridges = [
            Bridge(iface=item["iface"], active=bool(item.get("active", True)))
            for item in payload
            if isinstance(item, dict)
            and item.get("type") == "bridge"
            and isinstance(item.get("iface"), str)
        ]
        return sorted(bridges, key=lambda bridge: bridge.iface)

    def load_storages(self, node: str) -> list[Storage]:
        payload = self._request_json("GET", f"/nodes/{node}/storage", requires_auth=True)
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("Storage payload was not a list.")
        storages: list[Storage] = []
        for item in payload:
            if not isinstance(item, dict):
                continue
            storage = item.get("storage")
            content = tuple(
                part.strip()
                for part in str(item.get("content", "")).split(",")
                if part and isinstance(part, str)
            )
            if isinstance(storage, str):
                storages.append(Storage(storage=storage, node=node, content=content))
        return storages

    def load_isos(self, node: str, storage: str) -> list[IsoImage]:
        payload = self._request_json(
            "GET",
            f"/nodes/{node}/storage/{storage}/content",
            params={"content": "iso"},
            requires_auth=True,
        )
        if not isinstance(payload, list):
            raise ProxmoxUnexpectedResponseError("ISO payload was not a list.")
        return [
            IsoImage(volid=item["volid"], storage=storage, node=node)
            for item in payload
            if isinstance(item, dict) and isinstance(item.get("volid"), str)
        ]

    def create_vm(self, config: VmConfig, start_after_create: bool = False) -> VmCreationResult:
        payload = build_create_payload(config, self._settings)
        node = config.general.node
        vmid = config.general.vmid
        name = payload["name"]

        upid = self._request_json(
            "POST",
            f"/nodes/{node}/qemu",
            data={key: str(value) for key, value in payload.items()},
            requires_auth=True,
        )
        if isinstance(upid, str) and upid.startswith("UPID:"):
            self._wait_for_task(node, upid)

        try:
            serial_result = self._request_json(
                "PUT",
                f"/nodes/{node}/qemu/{vmid}/config",
                data={"serial0": "socket"},
                requires_auth=True,
            )
            if isinstance(serial_result, str) and serial_result.startswith("UPID:"):
                self._wait_for_task(node, serial_result)
        except ProxmoxError as exc:
            raise ProxmoxPostCreateError(
                node,
                vmid,
                "serial-console",
                f"VM was created but serial console configuration failed: {exc}",
            ) from exc

        if config.general.ha_enabled:
            try:
                ha_result = self._request_json(
                    "POST",
                    "/cluster/ha/resources",
                    data={
                        "sid": f"vm:{vmid}",
                        "state": "started" if start_after_create else "stopped",
                    },
                    requires_auth=True,
                )
                if isinstance(ha_result, str) and ha_result.startswith("UPID:"):
                    self._wait_for_task(node, ha_result)
            except ProxmoxError as exc:
                raise ProxmoxPostCreateError(
                    node,
                    vmid,
                    "high-availability",
                    f"VM was created but HA configuration failed: {exc}",
                ) from exc
        elif start_after_create:
            try:
                start_result = self._request_json(
                    "POST",
                    f"/nodes/{node}/qemu/{vmid}/status/start",
                    requires_auth=True,
                )
                if isinstance(start_result, str) and start_result.startswith("UPID:"):
                    self._wait_for_task(node, start_result)
            except ProxmoxError as exc:
                raise ProxmoxPostCreateError(
                    node,
                    vmid,
                    "start",
                    f"VM was created but automatic start failed: {exc}",
                ) from exc

        return VmCreationResult(
            node=node,
            vmid=vmid,
            name=str(name),
            serial_console_configured=True,
            ha_configured=config.general.ha_enabled,
        )

    def _wait_for_task(self, node: str, upid: str) -> None:
        deadline = time.time() + self._settings.request_timeout_seconds
        while time.time() < deadline:
            payload = self._request_json(
                "GET",
                f"/nodes/{node}/tasks/{upid}/status",
                requires_auth=True,
            )
            if not isinstance(payload, dict):
                raise ProxmoxUnexpectedResponseError("Task status payload was not an object.")
            if payload.get("status") == "stopped":
                if payload.get("exitstatus") != "OK":
                    raise ProxmoxApiError(f"Task {upid} failed with {payload.get('exitstatus')}.")
                return
            time.sleep(0.5)
        raise ProxmoxTransportError(f"Timed out while waiting for task {upid}.")

    def _request_json(
        self,
        method: str,
        path: str,
        *,
        requires_auth: bool = False,
        **kwargs: object,
    ) -> object:
        headers: dict[str, str] = {}
        if requires_auth:
            if not self._ticket:
                raise ProxmoxAuthError("Not authenticated with Proxmox.")
            if method.upper() not in {"GET", "HEAD", "OPTIONS"} and self._csrf_token:
                headers["CSRFPreventionToken"] = self._csrf_token

        try:
            response = self._client.request(method, path, headers=headers, **kwargs)
            response.raise_for_status()
        except httpx.ConnectError as exc:
            raise ProxmoxConnectError("Unable to connect to the Proxmox API.") from exc
        except httpx.TransportError as exc:
            message = str(exc)
            if _looks_like_tls_error(message):
                raise ProxmoxTlsError("TLS handshake or verification failed.") from exc
            raise ProxmoxTransportError("Transport error while calling the Proxmox API.") from exc
        except httpx.HTTPStatusError as exc:
            status_code = exc.response.status_code
            if status_code in {401, 403}:
                raise ProxmoxAuthError("Authentication was rejected by Proxmox.") from exc
            raise ProxmoxApiError(
                f"Proxmox API returned HTTP {status_code}.",
                status_code=status_code,
            ) from exc

        try:
            payload = response.json()
        except ValueError as exc:
            raise ProxmoxUnexpectedResponseError("Expected a JSON response from Proxmox.") from exc

        if not isinstance(payload, dict) or "data" not in payload:
            raise ProxmoxUnexpectedResponseError("Expected a top-level data field in the response.")
        return payload["data"]


class LiveProxmoxService(ProxmoxService):
    mode = "live"

    def __init__(self, client: ProxmoxApiClient) -> None:
        self._client = client

    def check_connectivity(self) -> str:
        return self._client.probe_transport()

    def check_api_base(self) -> str:
        return self._client.check_api_base()

    def load_realms(self) -> list[Realm]:
        return self._client.load_realms()

    def login(self, username: str, password: str, realm: str) -> AuthenticatedSession:
        return self._client.login(username, password, realm)

    def load_nodes(self) -> list[Node]:
        return self._client.load_nodes()

    def load_next_vmid(self) -> int:
        return self._client.load_next_vmid()

    def load_pools(self) -> list[Pool]:
        return self._client.load_pools()

    def load_existing_tags(self) -> list[str]:
        return self._client.load_existing_tags()

    def load_bridges(self, node: str) -> list[Bridge]:
        return self._client.load_bridges(node)

    def load_storages(self, node: str) -> list[Storage]:
        return self._client.load_storages(node)

    def load_isos(self, node: str, storage: str) -> list[IsoImage]:
        return self._client.load_isos(node, storage)

    def create_vm(self, config: VmConfig, start_after_create: bool = False) -> VmCreationResult:
        return self._client.create_vm(config, start_after_create=start_after_create)

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