"""Authentication helpers for Fyron."""

from __future__ import annotations

import getpass
import os
from datetime import datetime, timedelta, timezone
from typing import Any, Optional, Union

import requests
import jwt
from requests.auth import AuthBase, HTTPBasicAuth


def now_utc() -> datetime:
    """Return current UTC datetime."""
    return datetime.now(timezone.utc)


def _as_timedelta(value: Optional[Union[int, timedelta]]) -> Optional[timedelta]:
    """Normalize a minutes int or timedelta into a timedelta (or None)."""
    if value is None:
        return None
    if isinstance(value, timedelta):
        return value
    return timedelta(minutes=int(value))


class TokenAuth(AuthBase):
    """Very small token auth helper.

    Expects token endpoints that return JSON with `access_token` or `token`.
    """

    def __init__(
        self,
        username: str,
        password: str,
        auth_url: str,
        refresh_url: Optional[str] = None,
        token: Optional[str] = None,
        token_refresh_delta: Optional[Union[int, timedelta]] = None,
        max_login_attempts: int = 3,
        session: Optional[requests.Session] = None,
        token_field: str = "access_token",
        auth_method: str = "basic",
    ) -> None:
        """Create a token-based auth handler."""
        self._username = username
        self._password = password
        self._auth_url = auth_url
        self._refresh_url = refresh_url
        self._token_field = token_field
        self._session = session or requests.Session()
        self._token_session = requests.Session()
        self._session.hooks["response"].append(self._refresh_hook)
        self._max_login_attempts = max_login_attempts
        self._refresh_delta = _as_timedelta(token_refresh_delta)
        self._token: Optional[str] = None
        self._issued_at: Optional[datetime] = None
        self._auth_method = auth_method.lower()
        if token is None:
            self._authenticate()
        else:
            self._set_token(token)
        self.auth_time = now_utc()

    def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:  # type: ignore[override]
        """Attach a Bearer token to the request."""
        self._ensure_token()
        if self._token is None:
            raise RuntimeError("TokenAuth could not obtain a token.")
        self.token = self._token
        r.headers["Authorization"] = f"Bearer {self._token}"
        return r

    def _ensure_token(self) -> None:
        """Ensure a valid token is present, refreshing or logging in if needed."""
        if self._token is None:
            self._authenticate()
            return
        if self.is_refresh_required():
            self._refresh()

    def _refresh(self) -> None:
        """Refresh the token using refresh_url or fall back to login."""
        if self._refresh_url is None:
            self._authenticate()
            return
        response = self._token_session.get(self._refresh_url, timeout=30)
        if response.status_code == requests.codes.unauthorized:
            self._authenticate()
        else:
            response.raise_for_status()
            token = self._extract_token(response)
            self._set_token(token)
        self.auth_time = now_utc()

    def _authenticate(self) -> None:
        """Login to obtain a fresh token."""
        last_error: Optional[Exception] = None
        for _ in range(self._max_login_attempts):
            try:
                if self._auth_method == "basic":
                    response = self._token_session.get(
                        self._auth_url,
                        auth=(self._username, self._password),
                        timeout=30,
                    )
                else:
                    response = self._token_session.post(
                        self._auth_url,
                        json={"username": self._username, "password": self._password},
                        timeout=30,
                    )
                response.raise_for_status()
                token = self._extract_token(response)
                self._set_token(token)
                self.auth_time = now_utc()
                return
            except Exception as exc:  # pragma: no cover - last-resort fallthrough
                last_error = exc
        if last_error is not None:
            raise last_error

    def _extract_token(self, response: requests.Response) -> str:
        """Extract the token from a JSON response payload."""
        try:
            data = response.json()
            token = data.get(self._token_field) or data.get("token")
        except ValueError:
            token = response.text.strip()
        if not token:
            raise ValueError("Token response did not include an access token.")
        return token

    def _set_token(self, token: str) -> None:
        """Store the token and record issue time if refresh is enabled."""
        self._token = token
        self.token = token
        self._issued_at = datetime.now(timezone.utc)

    def is_refresh_required(self) -> bool:
        """Return True if token should be refreshed."""
        if self._token is None:
            return True
        try:
            decoded = jwt.decode(self._token, options={"verify_signature": False})
            if "exp" in decoded and "iat" in decoded:
                refresh_interval = (decoded["exp"] - decoded["iat"]) / 4
                return datetime.now(timezone.utc).timestamp() > (decoded["exp"] - refresh_interval)
            return False
        except jwt.exceptions.PyJWTError:
            return (
                self._refresh_delta is not None
                and (now_utc() - self.auth_time) > self._refresh_delta
            )

    def _refresh_hook(
        self, response: requests.Response, *args: Any, **kwargs: Any
    ) -> Optional[requests.Response]:
        """Refresh token on unauthorized or expiry."""
        if response.status_code == requests.codes.unauthorized or self.is_refresh_required():
            attempts = getattr(response.request, "login_reattempted_times", 0)
            attempts += 1
            if attempts >= self._max_login_attempts:
                response.raise_for_status()
            setattr(response.request, "login_reattempted_times", attempts)
            self._token = None
            self._refresh()
            return self._session.send(self(response.request), **kwargs)
        response.raise_for_status()
        return None


class Auth:
    """Simplified authentication wrapper for Fyron sessions.

    Supports token-based auth and HTTP Basic auth.
    """

    def __init__(
        self,
        auth_url: Optional[str] = None,
        auth_type: str = "token",
        refresh_url: Optional[str] = None,
        username: Optional[str] = None,
        auth_method: str = "env",
        token_auth_method: Optional[str] = None,
        token: Optional[str] = None,
        max_login_attempts: int = 3,
        token_refresh_delta: Optional[Union[int, timedelta]] = None,
        session: Optional[requests.Session] = None,
    ) -> None:
        """Initialize an authenticated requests session."""
        self.auth_type = auth_type
        self.auth_method = auth_method
        self.auth_url = auth_url or os.environ.get("FHIR_AUTH_URL")
        self.refresh_url = refresh_url or os.environ.get("FHIR_REFRESH_URL")
        self.username = username
        self.token = token
        self.token_auth_method = (token_auth_method or "basic").lower()
        self.max_login_attempts = max_login_attempts
        self.token_refresh_delta = token_refresh_delta
        self._user_env_name = "FHIR_USER"
        self._pass_env_name = "FHIR_PASSWORD"
        self.session = session or requests.Session()
        self._authenticate()

    @classmethod
    def token_env(
        cls,
        *,
        auth_url: str,
        refresh_url: Optional[str] = None,
        token_auth_method: str = "basic",
        token: Optional[str] = None,
        max_login_attempts: int = 3,
        token_refresh_delta: Optional[Union[int, timedelta]] = None,
        session: Optional[requests.Session] = None,
    ) -> "Auth":
        """Create token-based auth using credentials from environment variables."""
        return cls(
            auth_url=auth_url,
            auth_type="token",
            refresh_url=refresh_url,
            auth_method="env",
            token_auth_method=token_auth_method,
            token=token,
            max_login_attempts=max_login_attempts,
            token_refresh_delta=token_refresh_delta,
            session=session,
        )

    def __enter__(self) -> "Auth":
        """Return self for context manager usage."""
        return self

    def close(self) -> None:
        """Close the underlying session."""
        self.session.close()

    def __exit__(self, exc_type, exc, exc_tb) -> None:
        """Close the session on context manager exit."""
        self.close()

    def _authenticate(self) -> None:
        """Authenticate the session using the configured method."""
        if self.auth_type is None:
            return
        auth_type = self.auth_type.lower()
        if auth_type not in {"token", "basic"}:
            raise ValueError(f"Unsupported auth_type: {self.auth_type}")
        if auth_type == "basic":
            username, password = self._resolve_credentials()
            self.session.auth = HTTPBasicAuth(username, password)
            return
        if self.auth_url is None and self.token is None:
            raise ValueError("auth_url is required for token auth unless token is provided.")
        username, password = self._resolve_credentials() if self.token is None else ("", "")
        self.session.auth = TokenAuth(
            username=username,
            password=password,
            auth_url=self.auth_url or "",
            refresh_url=self.refresh_url,
            token=self.token,
            token_refresh_delta=self.token_refresh_delta,
            max_login_attempts=self.max_login_attempts,
            session=self.session,
            auth_method=self.token_auth_method,
        )

    def _resolve_credentials(self) -> tuple[str, str]:
        """Resolve credentials via password prompt or environment."""
        method = self.auth_method.lower()
        if method == "password":
            if not self.username:
                raise ValueError("username is required for password auth.")
            return self.username, getpass.getpass()
        if method == "env":
            return os.environ[self._user_env_name], os.environ[self._pass_env_name]
        raise ValueError(f"Unsupported auth_method: {self.auth_method}")
