"""Local-development Strava OAuth and protected token-store boundary."""

from __future__ import annotations

import json
import os
import secrets
import tempfile
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Protocol

from mountain_twin.brand import brand
from urllib.parse import urlencode

AUTHORIZATION_ENDPOINT = "https://www.strava.com/oauth/authorize"
TOKEN_ENDPOINT = "https://www.strava.com/oauth/token"
# Strava recommends this endpoint as of June 2026; the legacy deauthorize endpoint remains transitional.
REVOKE_ENDPOINT = "https://www.strava.com/oauth/revoke"
READ_SCOPE = "activity:read"
READ_ALL_SCOPE = "activity:read_all"


class OAuthError(RuntimeError):
    pass


@dataclass(frozen=True)
class OAuthClientCredentials:
    client_id: str
    client_secret: str = field(repr=False)

    @classmethod
    def from_environment(cls) -> "OAuthClientCredentials":
        client_id = os.environ.get("STRAVA_CLIENT_ID")
        client_secret = os.environ.get("STRAVA_CLIENT_SECRET")
        if not client_id or not client_secret:
            raise OAuthError("STRAVA_APP_CREDENTIALS_REQUIRED")
        return cls(client_id, client_secret)


@dataclass(frozen=True)
class OAuthRequest:
    state: str = field(repr=False)
    requested_scopes: tuple[str, ...]
    redirect_uri: str


@dataclass(frozen=True)
class StravaToken:
    athlete_id: str
    access_token: str = field(repr=False)
    refresh_token: str = field(repr=False)
    expires_at: int
    granted_scopes: tuple[str, ...]

    def requires_refresh(self, now: int | None = None, leeway_seconds: int = 3600) -> bool:
        return self.expires_at <= int(time.time() if now is None else now) + leeway_seconds

    def metadata(self) -> dict[str, object]:
        return {
            "athlete_id": self.athlete_id,
            "expires_at": self.expires_at,
            "granted_scopes": list(self.granted_scopes),
        }


class FormPoster(Protocol):
    def __call__(self, url: str, values: dict[str, str]) -> dict: ...


class LocalTokenStore:
    """Atomic 0600 local storage; callers choose an ignored/outside-repository path."""

    def __init__(self, path: Path):
        self.path = path

    def load(self) -> StravaToken | None:
        if not self.path.exists():
            return None
        data = json.loads(self.path.read_text(encoding="utf-8"))
        return StravaToken(
            athlete_id=str(data["athlete_id"]),
            access_token=data["access_token"],
            refresh_token=data["refresh_token"],
            expires_at=int(data["expires_at"]),
            granted_scopes=tuple(data.get("granted_scopes", ())),
        )

    def save(self, token: StravaToken) -> None:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        fd, temporary_name = tempfile.mkstemp(prefix=".strava-token-", dir=self.path.parent)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as temporary:
                os.fchmod(temporary.fileno(), 0o600)
                json.dump({**token.metadata(), "access_token": token.access_token, "refresh_token": token.refresh_token}, temporary, sort_keys=True)
                temporary.write("\n")
            os.replace(temporary_name, self.path)
            os.chmod(self.path, 0o600)
        finally:
            if os.path.exists(temporary_name):
                os.unlink(temporary_name)

    def clear(self) -> None:
        if self.path.exists():
            self.path.unlink()


class StravaOAuth:
    def __init__(self, credentials: OAuthClientCredentials, poster: FormPoster):
        self.credentials = credentials
        self.poster = poster

    def begin(self, redirect_uri: str, *, include_private_activities: bool = False) -> tuple[OAuthRequest, str]:
        scopes = (READ_SCOPE, READ_ALL_SCOPE) if include_private_activities else (READ_SCOPE,)
        request = OAuthRequest(secrets.token_urlsafe(32), scopes, redirect_uri)
        parameters = {
            'client_id': self.credentials.client_id,
            'redirect_uri': redirect_uri,
            'response_type': 'code',
            'approval_prompt': 'auto',
            'scope': ','.join(scopes),
            'state': request.state,
        }
        return request, f"{AUTHORIZATION_ENDPOINT}?{urlencode(parameters)}"

    def complete(self, request: OAuthRequest, callback: dict[str, str]) -> StravaToken:
        if callback.get("state") != request.state:
            raise OAuthError("STRAVA_OAUTH_STATE_INVALID")
        if callback.get("error"):
            raise OAuthError("STRAVA_OAUTH_DENIED")
        code = callback.get("code")
        granted_scope = callback.get("scope")
        if not code or granted_scope is None:
            raise OAuthError("STRAVA_OAUTH_CALLBACK_INCOMPLETE")
        response = self.poster(TOKEN_ENDPOINT, {
            "client_id": self.credentials.client_id,
            "client_secret": self.credentials.client_secret,
            "code": code,
            "grant_type": "authorization_code",
        })
        return self._token_from_response(response, granted_scope)

    def refresh(self, token: StravaToken) -> StravaToken:
        response = self.poster(TOKEN_ENDPOINT, {
            "client_id": self.credentials.client_id,
            "client_secret": self.credentials.client_secret,
            "refresh_token": token.refresh_token,
            "grant_type": "refresh_token",
        })
        return self._token_from_response(response, " ".join(token.granted_scopes), athlete_id=token.athlete_id)

    @staticmethod
    def _token_from_response(response: dict, granted_scope: str, athlete_id: str | None = None) -> StravaToken:
        try:
            identity = athlete_id or str(response["athlete"]["id"])
            return StravaToken(identity, response["access_token"], response["refresh_token"], int(response["expires_at"]), tuple(sorted(set(granted_scope.replace(",", " ").split()))))
        except (KeyError, TypeError, ValueError) as error:
            raise OAuthError("STRAVA_TOKEN_RESPONSE_INVALID") from error


def complete_local_callback(oauth: StravaOAuth, request: OAuthRequest, store: LocalTokenStore) -> StravaToken:
    """Wait for one local callback; authorization codes are never printed or persisted."""
    captured: dict[str, str] = {}

    class CallbackHandler(BaseHTTPRequestHandler):
        def do_GET(self):  # noqa: N802
            from urllib.parse import parse_qs, urlsplit
            captured.update({key: values[0] for key, values in parse_qs(urlsplit(self.path).query).items()})
            self.send_response(200); self.end_headers(); self.wfile.write(f"{brand()['name']} Strava connection received. You can close this tab.".encode("utf-8"))
        def log_message(self, *_): pass

    server = HTTPServer(("127.0.0.1", 8765), CallbackHandler)
    try:
        server.handle_request()
    finally:
        server.server_close()
    token = oauth.complete(request, captured)
    store.save(token)
    return token
