"""HTTP client helpers for QRM services."""

from __future__ import annotations

import json
import os
import pprint
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from urllib.parse import urljoin

import requests
import urllib3
from pydantic import BaseModel, Field, HttpUrl


class LoginCredentials(BaseModel):
    """Payload accepted by the `/User/Login` endpoint."""

    username: str = Field(..., min_length=1)
    password: str = Field(..., min_length=1)


class LoginState(BaseModel):
    """Serialized login state saved on disk by the CLI."""

    token: str = Field(..., min_length=1)
    username: str = Field(..., min_length=1)
    base_url: HttpUrl
    verify_tls: bool = True
    stored_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class QrmClientError(RuntimeError):
    """Raised when the client cannot fulfill a request."""


@dataclass
class QrmClient:
    """Thin wrapper around the QRM HTTP API."""

    base_url: str
    verify_tls: bool = True
    timeout: float = 10.0
    session: Optional[requests.Session] = None
    debug: bool = False

    def export_sql(
        self,
        *,
        token: str,
        query: str,
        format: str = "json",
        parameters: Optional[Dict[str, Any]] = None,
    ) -> Any:
        """Execute a SELECT query via the `/Result/Export/Sql` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not query:
            raise ValueError("query must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {
            "query": query,
            "format": format,
        }
        if parameters:
            payload["parameters"] = parameters

        url = self._build_url("/Result/Export/Sql")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM export sql endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM export sql endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def __post_init__(self) -> None:
        self.debug = self.debug or os.getenv("QRM_DEBUG") == "1"
        if not self.base_url:
            raise ValueError("base_url must be provided")
        self.base_url = self.base_url.rstrip("/")
        self.session = self.session or requests.Session()
        self.session.verify = self.verify_tls
        if not self.verify_tls:
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    def login(self, username: str, password: str) -> str:
        """Authenticate against `/User/Login` and return the bearer token."""

        credentials = LoginCredentials(username=username, password=password)
        url = self._build_url("/User/Login")
        body = credentials.model_dump()
        self._debug_request(
            method="POST",
            url=url,
            headers=None,
            payload=body,
        )
        try:
            response = self.session.post(
                url,
                json=body,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM login endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM login endpoint",
        )

        token = self._extract_token(response)
        if not token:
            raise QrmClientError("QRM login response did not contain a token")
        return token

    def status(self, token: Optional[str] = None) -> Any:
        """Query the `/Result/Status` endpoint to verify service health."""

        headers = {"Authorization": f"Bearer {token}"} if token else None
        url = self._build_url("/Result/Status")
        self._debug_request(
            method="GET",
            url=url,
            headers=headers,
            payload=None,
        )
        try:
            response = self.session.get(
                url,
                headers=headers,
                timeout=self.timeout,
            )
            self._debug_response(method="GET", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="GET", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM status endpoint") from exc

        self._ensure_success(
            response=response,
            method="GET",
            url=url,
            error_message="Failed to contact QRM status endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def uut_status(
        self,
        *,
        token: str,
        serial_number: str,
        start_datetime: Optional[str] = None,
        stop_datetime: Optional[str] = None,
        max_results: Optional[int] = None,
    ) -> Any:
        """Query the `/Uut/Status` endpoint for unit test history."""

        if not serial_number:
            raise ValueError("serial_number must be provided")
        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {
            "serialNumber": serial_number,
        }
        if start_datetime:
            payload["startDateTime"] = start_datetime
        if stop_datetime:
            payload["stopDateTime"] = stop_datetime
        if max_results is not None:
            payload["maxNumberOfResults"] = max_results

        url = self._build_url("/Uut/Status")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM UUT status endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM UUT status endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_list(
        self,
        *,
        token: str,
        start_datetime: Optional[str] = None,
        stop_datetime: Optional[str] = None,
    ) -> Any:
        """Query the `/Box/List` endpoint for production boxes."""

        if not token:
            raise ValueError("token must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {}
        if start_datetime:
            payload["startDateTime"] = start_datetime
        if stop_datetime:
            payload["stopDateTime"] = stop_datetime

        body = payload or None
        url = self._build_url("/Box/List")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=body,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=body,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box list endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box list endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_add(
        self,
        *,
        token: str,
        box_identifier: str,
        started_datetime: Optional[str] = None,
        finished_datetime: Optional[str] = None,
        number_of_units: Optional[int] = None,
        shipped: bool = False,
    ) -> Any:
        """Add or update a box via the `/Box/Add` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {
            "boxIdentifier": box_identifier,
            "shipped": shipped,
        }
        if started_datetime:
            payload["startedDateTime"] = started_datetime
        if finished_datetime:
            payload["finishedDateTime"] = finished_datetime
        if number_of_units is not None:
            payload["numberOfUnits"] = number_of_units

        url = self._build_url("/Box/Add")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box add endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box add endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_remove(
        self,
        *,
        token: str,
        box_identifier: str,
    ) -> Any:
        """Remove a box via the `/Box/Remove/{boxIdentifier}` endpoint.

        Idempotent: does not fail if box does not exist.
        """

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        url = self._build_url(f"/Box/Remove/{box_identifier}")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=None,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box remove endpoint") from exc

        # Support idempotent deletion: treat 404 as success (box already deleted)
        if response.status_code == 404:
            return True

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box remove endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_uut_add(
        self,
        *,
        token: str,
        box_identifier: str,
        uut_serial_number: Optional[str] = None,
        uut_type_name: Optional[str] = None,
        identifier_tag: Optional[str] = None,
    ) -> Any:
        """Add a UUT to a box via the `/Box/Uut/Add/{boxIdentifier}` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {}
        if uut_serial_number:
            payload["uutSerialNumber"] = uut_serial_number
        if uut_type_name:
            payload["uutTypeName"] = uut_type_name
        if identifier_tag:
            payload["identifierTag"] = identifier_tag

        url = self._build_url(f"/Box/Uut/Add/{box_identifier}")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box UUT add endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box UUT add endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_uut_remove(
        self,
        *,
        token: str,
        box_identifier: str,
        uut_serial_number: Optional[str] = None,
        uut_type_name: Optional[str] = None,
        identifier_tag: Optional[str] = None,
    ) -> Any:
        """Remove a UUT from a box via the `/Box/Uut/Remove/{boxIdentifier}` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {}
        if uut_serial_number:
            payload["uutSerialNumber"] = uut_serial_number
        if uut_type_name:
            payload["uutTypeName"] = uut_type_name
        if identifier_tag:
            payload["identifierTag"] = identifier_tag

        url = self._build_url(f"/Box/Uut/Remove/{box_identifier}")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box UUT remove endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box UUT remove endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_get(
        self,
        *,
        token: str,
        box_identifier: str,
    ) -> Any:
        """Get a box by identifier via the `/Box/Get/{boxIdentifier}` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        url = self._build_url(f"/Box/Get/{box_identifier}")
        self._debug_request(
            method="GET",
            url=url,
            headers=headers,
            payload=None,
        )
        try:
            response = self.session.get(
                url,
                headers=headers,
                timeout=self.timeout,
            )
            self._debug_response(method="GET", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="GET", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box get endpoint") from exc

        self._ensure_success(
            response=response,
            method="GET",
            url=url,
            error_message="Failed to contact QRM box get endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_content(
        self,
        *,
        token: str,
        box_identifier: str,
    ) -> Any:
        """Get all UUTs in a box via the `/Box/Content/{boxIdentifier}` endpoint."""

        if not token:
            raise ValueError("token must be provided")
        if not box_identifier:
            raise ValueError("box_identifier must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        url = self._build_url(f"/Box/Content/{box_identifier}")
        self._debug_request(
            method="GET",
            url=url,
            headers=headers,
            payload=None,
        )
        try:
            response = self.session.get(
                url,
                headers=headers,
                timeout=self.timeout,
            )
            self._debug_response(method="GET", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="GET", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box content endpoint") from exc

        self._ensure_success(
            response=response,
            method="GET",
            url=url,
            error_message="Failed to contact QRM box content endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def box_find(
        self,
        *,
        token: str,
        uut_serial_number: Optional[str] = None,
        uut_type_name: Optional[str] = None,
        identifier_tag: Optional[str] = None,
    ) -> Any:
        """Search for the box that contains a specified UUT via the `/Box/Find` endpoint."""

        if not token:
            raise ValueError("token must be provided")

        headers = {"Authorization": f"Bearer {token}"}
        payload: Dict[str, Any] = {}
        if uut_serial_number:
            payload["uutSerialNumber"] = uut_serial_number
        if uut_type_name:
            payload["uutTypeName"] = uut_type_name
        if identifier_tag:
            payload["identifierTag"] = identifier_tag

        url = self._build_url("/Box/Find")
        self._debug_request(
            method="POST",
            url=url,
            headers=headers,
            payload=payload,
        )
        try:
            response = self.session.post(
                url,
                headers=headers,
                json=payload,
                timeout=self.timeout,
            )
            self._debug_response(method="POST", url=url, response=response)
        except requests.RequestException as exc:  # pragma: no cover - network failure
            self._debug_error(method="POST", url=url, error=exc)
            raise QrmClientError("Failed to contact QRM box find endpoint") from exc

        self._ensure_success(
            response=response,
            method="POST",
            url=url,
            error_message="Failed to contact QRM box find endpoint",
        )

        try:
            return response.json()
        except ValueError:
            return response.text.strip()

    def _ensure_success(
        self,
        *,
        response: requests.Response,
        method: str,
        url: str,
        error_message: str,
    ) -> None:
        if response.status_code == 401:
            message = (
                "Authentication failed (HTTP 401). Your credentials may be invalid or expired. "
                "Please run `qrm login` and try again."
            )
            self._debug_error(
                method=method,
                url=url,
                error=RuntimeError(message),
            )
            raise QrmClientError(message)

        try:
            response.raise_for_status()
        except requests.HTTPError as exc:
            self._debug_error(method=method, url=url, error=exc)
            raise QrmClientError(error_message) from exc

    def _build_url(self, path: str) -> str:
        return urljoin(f"{self.base_url}/", path.lstrip("/"))

    @staticmethod
    def _extract_token(response: requests.Response) -> Optional[str]:
        payload: Any
        try:
            payload = response.json()
        except ValueError:
            payload = None

        token = QrmClient._token_from_payload(payload)
        if token:
            return token

        fallback = response.text.strip()
        if fallback:
            token = QrmClient._token_from_payload(fallback)
            if token:
                return token
            return fallback
        return None

    @staticmethod
    def _token_from_payload(payload: Any) -> Optional[str]:
        if isinstance(payload, Dict):
            for key in (
                "token",
                "Token",
                "accessToken",
                "access_token",
                "bearerToken",
                "value",
            ):
                value = payload.get(key)
                if isinstance(value, str) and value.strip():
                    return value.strip()
        if isinstance(payload, str):
            trimmed = payload.strip()
            if not trimmed:
                return None
            if trimmed.startswith("{") and trimmed.endswith("}"):
                try:
                    return QrmClient._token_from_payload(json.loads(trimmed))
                except json.JSONDecodeError:
                    pass
            return trimmed
        return None

    def _debug_request(
        self,
        *,
        method: str,
        url: str,
        headers: Optional[Dict[str, Any]],
        payload: Any,
    ) -> None:
        if not self.debug:
            return
        data: Dict[str, Any] = {"method": method, "url": url}
        if headers is not None:
            data["headers"] = headers
        if payload is not None:
            data["body"] = payload
        self._debug_http("REQUEST", data)

    def _debug_response(
        self,
        *,
        method: str,
        url: str,
        response: requests.Response,
    ) -> None:
        if not self.debug:
            return
        headers = getattr(response, "headers", None)
        data: Dict[str, Any] = {
            "method": method,
            "url": url,
            "status_code": response.status_code,
            "body": response.text,
        }
        if headers is not None:
            data["headers"] = dict(headers)
        self._debug_http("RESPONSE", data)

    def _debug_error(self, *, method: str, url: str, error: Exception) -> None:
        if not self.debug:
            return
        self._debug_http(
            "ERROR",
            {
                "method": method,
                "url": url,
                "error": f"{error.__class__.__name__}: {error}",
            },
        )

    def _debug_http(self, stage: str, payload: Dict[str, Any]) -> None:
        if not self.debug:
            return
        formatted = pprint.pformat(payload)
        print(f"[QRM DEBUG] {stage}: {formatted}", file=sys.stderr)


__all__ = ["LoginCredentials", "LoginState", "QrmClient", "QrmClientError"]
