"""eBay OAuth2 token management: exchange, refresh, and secure file-based storage.

Tokens are stored in a JSON file outside the repo (default: /var/lib/agent-samochodowy/ebay_tokens.json)
with 600 permissions so only the service user can read them.
"""

from __future__ import annotations

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

import httpx

logger = logging.getLogger(__name__)

EBAY_TOKEN_URL = "https://api.ebay.com/identity/v1/oauth2/token"

# Default scopes for Trading API seller read access
DEFAULT_SCOPES = (
    "https://api.ebay.com/oauth/api_scope "
    "https://api.ebay.com/oauth/api_scope/sell.inventory.readonly "
    "https://api.ebay.com/oauth/api_scope/sell.inventory "
    "https://api.ebay.com/oauth/api_scope/sell.account.readonly "
    "https://api.ebay.com/oauth/api_scope/sell.account"
)

DEFAULT_TOKEN_PATH = "/var/lib/agent-samochodowy/ebay_tokens.json"


def _basic_auth(client_id: str, client_secret: str) -> str:
    """Build HTTP Basic auth header value."""
    creds = f"{client_id}:{client_secret}"
    return "Basic " + base64.b64encode(creds.encode()).decode()


def exchange_code_for_tokens(
    code: str,
    client_id: str,
    client_secret: str,
    redirect_uri: str,
) -> dict:
    """Exchange an authorization code for access_token + refresh_token.

    Returns the raw eBay token response dict.
    """
    resp = httpx.post(
        EBAY_TOKEN_URL,
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": _basic_auth(client_id, client_secret),
        },
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": redirect_uri,
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


def refresh_access_token(
    refresh_token: str,
    client_id: str,
    client_secret: str,
    scopes: str = DEFAULT_SCOPES,
) -> dict:
    """Use a refresh_token to obtain a new access_token.

    Returns the raw eBay token response dict.
    """
    resp = httpx.post(
        EBAY_TOKEN_URL,
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": _basic_auth(client_id, client_secret),
        },
        data={
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "scope": scopes,
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()


# ---------------------------------------------------------------------------
# File-based token store (JSON, 0o600)
# ---------------------------------------------------------------------------

class EbayTokenStore:
    """Manages eBay OAuth tokens in a local JSON file with restricted permissions."""

    def __init__(self, path: str = DEFAULT_TOKEN_PATH) -> None:
        self.path = Path(path)

    def save(self, token_data: dict) -> None:
        """Persist token data to disk.

        Merges with existing data so that a refresh (which does NOT return a new
        refresh_token) does not overwrite the stored refresh_token.
        """
        existing = self.load()
        existing.update(token_data)
        existing["updated_at"] = time.time()

        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(json.dumps(existing, indent=2))
        os.chmod(self.path, 0o600)
        logger.info("eBay tokens saved to %s", self.path)

    def load(self) -> dict:
        """Load stored tokens. Returns empty dict if file missing."""
        if not self.path.exists():
            return {}
        try:
            return json.loads(self.path.read_text())
        except (json.JSONDecodeError, OSError) as e:
            logger.warning("Failed to read token file %s: %s", self.path, e)
            return {}

    @property
    def access_token(self) -> str | None:
        data = self.load()
        return data.get("access_token")

    @property
    def refresh_token(self) -> str | None:
        data = self.load()
        return data.get("refresh_token")

    @property
    def is_expired(self) -> bool:
        """Conservative check: treat token as expired 5 min before actual expiry."""
        data = self.load()
        updated = data.get("updated_at", 0)
        expires_in = data.get("expires_in", 0)
        if not updated or not expires_in:
            return True
        return time.time() > (updated + expires_in - 300)


def get_valid_access_token(
    client_id: str,
    client_secret: str,
    token_store: EbayTokenStore,
    scopes: str = DEFAULT_SCOPES,
) -> str:
    """Return a valid access token, refreshing if necessary.

    Raises RuntimeError if no refresh_token is available.
    """
    if not token_store.is_expired:
        token = token_store.access_token
        if token:
            return token

    refresh_tok = token_store.refresh_token
    if not refresh_tok:
        raise RuntimeError(
            "No refresh_token available. Run the OAuth consent flow first: "
            "python scripts/ebay_authorize.py"
        )

    logger.info("eBay access token expired, refreshing...")
    token_data = refresh_access_token(refresh_tok, client_id, client_secret, scopes)
    token_store.save(token_data)
    logger.info("eBay access token refreshed successfully")
    return token_data["access_token"]
