# retoor <retoor@molodetz.nl>

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Callable

import httpx

from devplacepy import stealth
from .errors import AuthRequiredError, NetworkError, ToolInputError, UpstreamError

logger = logging.getLogger("devii.http")

LOGIN_FAILURE_MARKERS = ("Invalid email or password", 'name="password"')
LOGIN_PATH = "/auth/login"


class PlatformClient:
    def __init__(
        self,
        base_url: str,
        timeout_seconds: float,
        api_key: str = "",
        key_resolver: Callable[[], str] | None = None,
    ) -> None:
        headers = {
            "User-Agent": "devii/0.1",
            "Accept": "application/json",
            "X-Devii-Agent": "1",
        }
        if api_key:
            headers["Authorization"] = f"Bearer {api_key}"
            headers["X-API-Key"] = api_key
        self._key_resolver = key_resolver
        self._client = stealth.stealth_async_client(
            base_url=base_url,
            timeout=timeout_seconds,
            follow_redirects=True,
            headers=headers,
        )
        self.authenticated: bool = bool(api_key)
        self.username: str | None = "api-key" if api_key else None

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

    def session_cookie(self) -> str:
        return self._client.cookies.get("session") or ""

    async def login(
        self, email: str, password: str, remember_me: bool = True
    ) -> dict[str, Any]:
        form = {
            "email": email,
            "password": password,
            "remember_me": "on" if remember_me else "",
            "next": "/feed",
        }
        response = await self._send("POST", LOGIN_PATH, data=form)
        has_session = len(self._client.cookies) > 0
        failed = response.status_code >= 400 or not has_session
        try:
            data = response.json()
            if isinstance(data, dict) and (
                data.get("ok") is False or data.get("error") or data.get("detail")
            ):
                failed = True
        except ValueError:
            if any(marker in response.text for marker in LOGIN_FAILURE_MARKERS):
                failed = True

        if failed:
            self.authenticated = False
            self.username = None
            logger.info("Login failed for %s", email)
            raise AuthRequiredError(
                "Login failed: invalid email or password.",
                email=email,
            )

        self.authenticated = True
        self.username = email
        logger.info("Login succeeded for %s", email)
        return {"status": "logged_in", "user": email}

    async def logout(self) -> dict[str, Any]:
        await self._send("GET", "/auth/logout")
        self._client.cookies.clear()
        self.authenticated = False
        previous = self.username
        self.username = None
        logger.info("Logged out %s", previous)
        return {"status": "logged_out", "previous_user": previous}

    async def call(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        file_field: tuple[str, str] | None = None,
        headers: dict[str, str] | None = None,
    ) -> httpx.Response:
        files = None
        if file_field is not None:
            field_name, raw_path = file_field
            files = {field_name: self._open_upload(raw_path)}

        response = await self._send(
            method, path, params=params, data=data, files=files, headers=headers
        )
        if self._auth_failed(path, response) and self._refresh_key():
            response = await self._send(
                method, path, params=params, data=data, files=files, headers=headers
            )

        if path != LOGIN_PATH and LOGIN_PATH in str(response.url):
            self.authenticated = False
            self.username = None
            logger.info("Session expired during %s %s", method, path)
            raise AuthRequiredError(
                "Session expired or not authenticated. Use the login tool again.",
                path=path,
            )

        if response.status_code == 401:
            self.authenticated = False
            logger.info("Unauthorized (401) during %s %s", method, path)
            raise AuthRequiredError(
                "Credentials are missing or invalid (401). Log in again or check the API key.",
                path=path,
            )

        if response.status_code >= 500:
            raise UpstreamError(
                f"Server returned {response.status_code}.",
                status=response.status_code,
                path=path,
            )
        return response

    @staticmethod
    def _auth_failed(path: str, response: httpx.Response) -> bool:
        if response.status_code == 401:
            return True
        return path != LOGIN_PATH and LOGIN_PATH in str(response.url)

    def _refresh_key(self) -> bool:
        if self._key_resolver is None:
            return False
        try:
            key = self._key_resolver()
        except Exception:  # noqa: BLE001 - refresh is best-effort; the original failure still surfaces
            logger.exception("Credential refresh failed after auth failure")
            return False
        if not key or key == self._client.headers.get("X-API-Key"):
            return False
        self._client.headers["Authorization"] = f"Bearer {key}"
        self._client.headers["X-API-Key"] = key
        self.authenticated = True
        self.username = self.username or "api-key"
        logger.info("Refreshed platform credentials after auth failure")
        return True

    def _open_upload(self, raw_path: str) -> tuple[str, bytes]:
        candidate = Path(raw_path).expanduser()
        if not candidate.is_file():
            raise ToolInputError(f"File not found: {raw_path}", path=raw_path)
        return candidate.name, candidate.read_bytes()

    async def _send(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        try:
            logger.debug("Request %s %s kwargs=%s", method, path, list(kwargs))
            response = await self._client.request(method, path, **kwargs)
            logger.debug("Response %s %s -> %s", method, path, response.status_code)
            return response
        except httpx.TimeoutException as exc:
            raise NetworkError(
                f"Request timed out: {method} {path}", path=path
            ) from exc
        except httpx.ConnectError as exc:
            raise NetworkError(
                f"Could not connect: {method} {path}", path=path
            ) from exc
        except httpx.HTTPError as exc:
            raise NetworkError(f"HTTP error: {exc}", path=path) from exc
