from __future__ import annotations

from datetime import datetime, timedelta, timezone
from urllib.parse import urlparse

import httpx
import jwt

from sitedrop.client.config import ClientConfig

DEFAULT_TIMEOUT = 30.0


class SitedropError(Exception):
    """User-facing error from the sitedrop client."""


def _validate_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise SitedropError(
            f"Invalid server URL: {url!r}. Must start with http:// or https://."
        )
    if not parsed.hostname:
        raise SitedropError(f"Invalid server URL: {url!r}. No hostname found.")
    return url.rstrip("/")


def _request(method: str, url: str, **kwargs):
    kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
    try:
        func = getattr(httpx, method)
        resp = func(url, **kwargs)
        resp.raise_for_status()
        return resp
    except httpx.ConnectError:
        raise SitedropError("Cannot connect to server. Is it running?")
    except httpx.TimeoutException:
        raise SitedropError(f"Request timed out after {kwargs.get('timeout')}s.")
    except httpx.HTTPStatusError as e:
        status = e.response.status_code
        try:
            detail = e.response.json().get("detail", "")
        except Exception:
            detail = e.response.text
        if status == 401:
            raise SitedropError(f"Authentication failed: {detail}")
        elif status == 413:
            raise SitedropError(f"File too large: {detail}")
        elif status == 412:
            raise SitedropError(f"Site already exists: {detail}")
        elif status == 404:
            raise SitedropError(f"Not found: {detail}")
        else:
            raise SitedropError(f"Server error ({status}): {detail}")
    except httpx.HTTPError:
        raise SitedropError("Network error. Check your connection and server URL.")


class SitedropClient:
    def __init__(self, config: ClientConfig):
        self.config = config
        self.base_url = config.server_url.rstrip("/") if config.server_url else ""

    def _token_expired(self) -> bool:
        if not self.config.token:
            return True
        try:
            payload = jwt.decode(self.config.token, options={"verify_signature": False})
            exp = datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
            return exp < datetime.now(timezone.utc) + timedelta(minutes=5)
        except (jwt.PyJWTError, KeyError):
            return True

    def _refresh_token(self) -> None:
        if not self.config.password:
            raise RuntimeError("No password saved. Run 'sitedrop login' first.")
        resp = _request(
            "post",
            f"{self.base_url}/api/auth",
            json={"password": self.config.password},
        )
        self.config.token = resp.json()["token"]
        self.config.save()

    def _ensure_token(self) -> None:
        if self._token_expired():
            self._refresh_token()

    def _headers(self) -> dict:
        self._ensure_token()
        return {"Authorization": f"Bearer {self.config.token}"}

    def login(self, server_url: str, password: str) -> str:
        self.base_url = _validate_url(server_url)
        resp = _request(
            "post",
            f"{self.base_url}/api/auth",
            json={"password": password},
        )
        token = resp.json()["token"]
        self.config.server_url = self.base_url
        self.config.password = password
        self.config.token = token
        self.config.save()
        return token

    def upload(self, name: str, content: str, force: bool = False) -> dict:
        headers = self._headers()
        if not force:
            headers["If-None-Match"] = "*"
        resp = _request(
            "put",
            f"{self.base_url}/api/sites/{name}",
            content=content.encode("utf-8"),
            headers=headers,
        )
        return resp.json()

    def list_sites(self) -> list[dict]:
        resp = _request(
            "get",
            f"{self.base_url}/api/sites",
            headers=self._headers(),
        )
        return resp.json()["sites"]

    def delete(self, name: str) -> str:
        resp = _request(
            "delete",
            f"{self.base_url}/api/sites/{name}",
            headers=self._headers(),
        )
        return resp.json()["message"]

    def change_password(self, current_password: str, new_password: str) -> dict:
        resp = _request(
            "post",
            f"{self.base_url}/api/password",
            json={
                "current_password": current_password,
                "new_password": new_password,
            },
            headers=self._headers(),
        )
        data = resp.json()
        if "token" in data:
            self.config.token = data["token"]
            self.config.save()
        return data
