"""
Module: auth
Description: Authentication client and token storage

Implements:
    - docs/cli/reference.md#huitzo-login
    - docs/guides/developer-environment.md#authentication

See Also:
    - docs/reference/secrets.md (for secure storage patterns)
"""

from __future__ import annotations

import base64
import json
import logging
import os
import time
from pathlib import Path
from typing import Any

import httpx

from huitzo_cli.config import ConfigManager
from huitzo_cli.http_utils import parse_error

logger = logging.getLogger(__name__)


class TokenStore:
    """Manages authentication tokens securely."""

    def __init__(self) -> None:
        """Initialize token store."""
        self.token_path = self._get_token_path()
        self.refresh_token_path = self.token_path.parent / "refresh_token"

    @staticmethod
    def _get_token_path() -> Path:
        """Get platform-specific token file path."""
        if os.name == "nt":  # Windows
            base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
            return base / "huitzo" / "token"
        else:  # Unix-like
            return Path.home() / ".huitzo" / "token"

    def save(self, token: str) -> None:
        """Save token to secure storage.

        Args:
            token: Authentication token to save
        """
        self.token_path.parent.mkdir(parents=True, exist_ok=True)

        # Create file with restrictive permissions atomically (no TOCTOU window)
        if os.name != "nt":
            fd = os.open(str(self.token_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
            with os.fdopen(fd, "w") as f:
                f.write(token)
            self.token_path.parent.chmod(0o700)
        else:
            with open(self.token_path, "w") as f:
                f.write(token)

    def load(self) -> str | None:
        """Load token from secure storage.

        Returns:
            Token if it exists, None otherwise
        """
        if not self.token_path.exists():
            return None

        try:
            with open(self.token_path) as f:
                return f.read().strip()
        except OSError:
            return None

    def delete(self) -> None:
        """Delete stored token."""
        if self.token_path.exists():
            try:
                self.token_path.unlink()
            except OSError:
                pass

    def save_refresh(self, token: str) -> None:
        """Save refresh token to secure storage."""
        self.refresh_token_path.parent.mkdir(parents=True, exist_ok=True)
        if os.name != "nt":
            fd = os.open(str(self.refresh_token_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
            with os.fdopen(fd, "w") as f:
                f.write(token)
        else:
            with open(self.refresh_token_path, "w") as f:
                f.write(token)

    def load_refresh(self) -> str | None:
        """Load refresh token from secure storage."""
        if not self.refresh_token_path.exists():
            return None
        try:
            with open(self.refresh_token_path) as f:
                return f.read().strip()
        except OSError:
            return None

    def delete_refresh(self) -> None:
        """Delete stored refresh token."""
        if self.refresh_token_path.exists():
            try:
                self.refresh_token_path.unlink()
            except OSError:
                pass


class AuthClient:
    """Client for handling authentication with Huitzo backend."""

    def __init__(self, config_manager: ConfigManager) -> None:
        """Initialize auth client.

        Args:
            config_manager: Configuration manager instance
        """
        self.config = config_manager
        self.token_store = TokenStore()

    def login(
        self,
        email: str,
        password: str | None = None,
        url: str | None = None,
        token: str | None = None,
    ) -> dict[str, Any]:
        """Login to Huitzo backend.

        Token is stored exclusively in TokenStore (single source of truth).

        Args:
            email: User email address
            password: User password (required for credential-based login)
            url: Backend API URL (default from config)
            token: Optional pre-existing token (skips credential exchange)

        Returns:
            User info dict from the login response

        Raises:
            RuntimeError: If login fails
        """
        if token:
            # Direct token — no credential exchange needed
            self.token_store.save(token)
            return {"email": email}

        if not password:
            raise RuntimeError("Password is required for credential-based login")

        api_url = url or self.config.get("api_url")

        # Validate HTTPS before sending credentials
        if api_url and not str(api_url).startswith("https://"):
            localhost_prefixes = ("http://localhost", "http://127.0.0.1", "http://[::1]")
            if not str(api_url).startswith(localhost_prefixes):
                raise RuntimeError(
                    "HTTPS required for credential exchange. "
                    "Use an https:// URL or localhost for development."
                )

        # Only persist the URL when the user explicitly provided --url
        if url:
            self.config.set("api_url", api_url, scope="user")

        try:
            response = httpx.post(
                f"{api_url}/api/v1/auth/login",
                json={"email": email, "password": password},
                timeout=30,
            )
        except httpx.HTTPError as e:
            raise RuntimeError(f"Could not connect to backend at {api_url}: {e}")

        if response.status_code != 200:
            raise RuntimeError(parse_error(response))

        resp_body: dict[str, Any] = response.json()
        data: dict[str, Any] = resp_body.get("data", {})
        # Backend returns tokens nested: {"tokens": {"accessToken": "..."}}
        access_token = data.get("tokens", {}).get("accessToken")

        if not access_token:
            raise RuntimeError("No access token in response")

        self.token_store.save(access_token)

        refresh_token = data.get("tokens", {}).get("refreshToken")
        if refresh_token:
            self.token_store.save_refresh(refresh_token)

        return data

    def refresh_tokens(self) -> str:
        """Refresh access + refresh tokens using stored refresh token.

        Returns:
            New access token string.

        Raises:
            AuthenticationError: If refresh token is missing, expired, or invalid.
            NetworkError: If the backend is unreachable.
            ServerError: If the backend returns 5xx.
        """
        from huitzo_cli.errors import AuthenticationError, NetworkError, ServerError

        refresh_token = self.token_store.load_refresh()
        if not refresh_token:
            raise AuthenticationError("No refresh token stored. Run: huitzo login")

        api_url = self.get_api_url()
        try:
            response = httpx.post(
                f"{api_url}/api/v1/auth/refresh",
                json={"refreshToken": refresh_token},
                timeout=10,
            )
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            raise NetworkError(f"Could not reach backend: {e}") from e

        if response.status_code == 401:
            self.token_store.delete_refresh()
            raise AuthenticationError("Refresh token expired. Run: huitzo login")

        if response.status_code >= 500:
            raise ServerError(f"Backend returned {response.status_code}")

        if response.status_code != 200:
            raise AuthenticationError(f"Token refresh failed ({response.status_code})")

        resp_data = response.json().get("data", {})
        new_access: str | None = resp_data.get("accessToken")
        new_refresh: str | None = resp_data.get("refreshToken")

        if not new_access:
            raise AuthenticationError("No access token in refresh response")

        self.token_store.save(new_access)
        if new_refresh:
            self.token_store.save_refresh(new_refresh)

        return new_access

    def logout(self) -> None:
        """Logout and remove stored tokens."""
        self.token_store.delete()
        self.token_store.delete_refresh()

    def get_token(self) -> str | None:
        """Get current authentication token.

        Reads exclusively from TokenStore (single source of truth).
        Also checks the HUITZO_AUTH_TOKEN env var as a higher-priority override.

        Returns:
            Token if authenticated, None otherwise
        """
        # Env var takes priority (replaces the old --token CLI flag)
        env_token = os.environ.get("HUITZO_AUTH_TOKEN")
        if env_token:
            return env_token

        # Single source: token file
        token = self.token_store.load()
        if token:
            return token

        return None

    def get_api_url(self) -> str:
        """Get the configured API URL.

        Returns:
            API URL string
        """
        return str(self.config.get("api_url"))

    @staticmethod
    def _check_jwt_expiry(token: str) -> tuple[bool, str]:
        """Best-effort JWT expiry check without signature verification.

        Decodes the payload segment and checks the ``exp`` claim.
        Returns (True, message) if the token is expired, (False, "") otherwise.
        Non-JWT tokens or decode failures are treated as "not expired" so that
        backend validation can make the final decision.
        """
        parts = token.split(".")
        if len(parts) != 3:
            # Not a JWT — skip local check
            return False, ""

        try:
            # JWT base64url decode (add padding)
            payload_b64 = parts[1]
            padding = 4 - len(payload_b64) % 4
            if padding != 4:
                payload_b64 += "=" * padding
            payload = json.loads(base64.urlsafe_b64decode(payload_b64))

            exp = payload.get("exp")
            if exp is not None and float(exp) < time.time():
                return True, "Auth token has expired. Run: huitzo login"
        except (ValueError, KeyError, json.JSONDecodeError, UnicodeDecodeError):
            logger.debug("JWT expiry check failed (non-fatal)", exc_info=True)

        return False, ""

    def validate_token(self) -> tuple[bool, str]:
        """Validate the current token (local expiry check + backend verification).

        Returns:
            Tuple of (is_valid, error_message).
        """
        token = self.get_token()
        if not token:
            return False, "Not authenticated. Run: huitzo login"

        expired, msg = self._check_jwt_expiry(token)
        if expired:
            return False, msg

        user = self.get_current_user()
        if user is None:
            return False, "Auth token is invalid or expired. Run: huitzo login"

        return True, ""

    def is_authenticated(self) -> bool:
        """Check if user is authenticated.

        Returns:
            True if authenticated, False otherwise
        """
        return self.get_token() is not None

    def get_current_user(self) -> dict[str, Any] | None:
        """Get current authenticated user info from the backend.

        Returns:
            User info dict if authenticated, None otherwise
        """
        token = self.get_token()
        if not token:
            return None

        api_url = self.get_api_url()
        try:
            response = httpx.get(
                f"{api_url}/api/v1/auth/me",
                headers={"Authorization": f"Bearer {token}"},
                timeout=10,
            )
            if response.status_code == 200:
                body: dict[str, Any] = response.json()
                result: dict[str, Any] | None = body.get("data")
                return result
        except httpx.HTTPError:
            # Network/HTTP error: treat as unauthenticated
            pass

        return None
