"""Fitbit source adapter.

Handles OAuth 2.0 (PKCE), token management, API transport, and rate
limiting.  Returns raw API responses wrapped in provenance envelopes.
No domain logic — this module knows how to talk to Fitbit and nothing
else.
"""

import base64
import hashlib
import http.server
import json
import secrets
import threading
import webbrowser
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse

from byteb4rb1e.utils.http.client import get as http_get
from byteb4rb1e.utils.http.client import post as http_post

from byteb4rb1e.homeostat.envelope import Envelope

API_BASE = "https://api.fitbit.com"
AUTH_URL = "https://www.fitbit.com/oauth2/authorize"
TOKEN_URL = f"{API_BASE}/oauth2/token"
SCOPES = "activity heartrate sleep weight profile"


@dataclass
class TokenStore:
    """Manages Fitbit OAuth tokens on disk."""

    path: Path

    def load(self) -> dict[str, Any] | None:
        if not self.path.exists():
            return None
        return json.loads(self.path.read_text())

    def save(self, tokens: dict[str, Any]) -> None:
        self.path.write_text(json.dumps(tokens, indent=2))


class RateLimited(Exception):
    """Raised when the Fitbit API returns HTTP 429."""

    def __init__(self, reset_seconds: int) -> None:
        self.reset_seconds = reset_seconds
        super().__init__(
            f"Rate limited. Resets in {reset_seconds}s."
        )


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


class FitbitAdapter:
    """Low-level Fitbit API client with automatic token refresh."""

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        token_store: TokenStore,
    ) -> None:
        self._client_id = client_id
        self._client_secret = client_secret
        self._token_store = token_store
        self._tokens = token_store.load() or {}

    # -- OAuth ---------------------------------------------------------------

    def authorize(self, redirect_uri: str) -> dict[str, Any]:
        """Run the OAuth 2.0 Authorization Code Grant with PKCE."""
        verifier, challenge = _generate_pkce()

        params = {
            "client_id": self._client_id,
            "response_type": "code",
            "code_challenge": challenge,
            "code_challenge_method": "S256",
            "scope": SCOPES,
            "redirect_uri": redirect_uri,
        }
        auth_url = f"{AUTH_URL}?{urlencode(params)}"

        parsed = urlparse(redirect_uri)
        host = parsed.hostname or "localhost"
        port = parsed.port or 8080
        auth_code: dict[str, str | None] = {}

        class _Handler(http.server.BaseHTTPRequestHandler):
            def do_GET(self) -> None:
                qs = parse_qs(urlparse(self.path).query)
                auth_code["code"] = qs.get("code", [None])[0]
                self.send_response(200)
                self.send_header("Content-Type", "text/html")
                self.end_headers()
                self.wfile.write(
                    b"<h1>Authorization complete.</h1>"
                    b"<p>You can close this tab.</p>"
                )

            def log_message(self, *_args: Any) -> None:
                pass

        server = http.server.HTTPServer((host, port), _Handler)
        thread = threading.Thread(target=server.handle_request)
        thread.start()

        webbrowser.open(auth_url)
        thread.join(timeout=120)
        server.server_close()

        code = auth_code.get("code")
        if not code:
            raise RuntimeError("No authorization code received.")

        self._tokens = self._token_request({
            "client_id": self._client_id,
            "code": code,
            "code_verifier": verifier,
            "grant_type": "authorization_code",
            "redirect_uri": redirect_uri,
        })
        self._token_store.save(self._tokens)
        return self._tokens

    def _refresh(self) -> None:
        self._tokens = self._token_request({
            "grant_type": "refresh_token",
            "refresh_token": self._tokens["refresh_token"],
            "client_id": self._client_id,
        })
        self._token_store.save(self._tokens)

    def _token_request(
        self, data: dict[str, str]
    ) -> dict[str, Any]:
        body = urlencode(data).encode()
        resp = http_post(
            TOKEN_URL,
            data=body,
            headers={
                "Content-Type":
                    "application/x-www-form-urlencoded",
                "Authorization": _basic_auth_header(
                    self._client_id, self._client_secret
                ),
            },
        )
        if resp.status_code != 200:
            raise RuntimeError(
                f"Token request failed ({resp.status_code}):"
                f" {resp.text}"
            )
        return resp.json()

    # -- Transport -----------------------------------------------------------

    def get(self, path: str) -> Envelope:
        """GET an API endpoint; return an Envelope with the raw
        response.

        Handles 401 (token refresh + retry).  Rate limiting is
        handled by byteb4rb1e.utils.http.client.
        """
        fetched_at = datetime.now(timezone.utc).isoformat()
        url = f"{API_BASE}{path}"

        resp = http_get(url, headers=self._auth_header())

        if resp.status_code == 401:
            self._refresh()
            resp = http_get(url, headers=self._auth_header())

        if resp.status_code == 429:
            reset = int(
                resp.headers.get("Fitbit-Rate-Limit-Reset", "60")
            )
            raise RateLimited(reset)

        if resp.status_code >= 400:
            raise RuntimeError(
                f"Fitbit API {path} failed"
                f" ({resp.status_code}): {resp.text}"
            )

        return Envelope(
            source="fitbit",
            endpoint=path,
            fetched_at=fetched_at,
            payload=resp.json(),
        )

    def _auth_header(self) -> dict[str, str]:
        return {
            "Authorization":
                f"Bearer {self._tokens['access_token']}"
        }

    # -- Endpoint helpers ----------------------------------------------------
    #
    # These encode Fitbit endpoint paths.  They return Envelopes
    # containing raw API JSON — no interpretation.

    def heart_rate(self, date: str) -> Envelope:
        return self.get(
            f"/1/user/-/activities/heart/date/{date}/1d.json"
        )

    def heart_rate_intraday(
        self,
        date: str,
        start_time: str,
        end_time: str,
    ) -> Envelope:
        return self.get(
            f"/1/user/-/activities/heart/date/{date}/1d/1min"
            f"/time/{start_time}/{end_time}.json"
        )

    def heart_rate_range(
        self, start: str, end: str
    ) -> Envelope:
        return self.get(
            f"/1/user/-/activities/heart"
            f"/date/{start}/{end}.json"
        )

    def steps(self, date: str) -> Envelope:
        return self.get(
            f"/1/user/-/activities/steps/date/{date}/1d.json"
        )

    def steps_range(self, start: str, end: str) -> Envelope:
        return self.get(
            f"/1/user/-/activities/steps"
            f"/date/{start}/{end}.json"
        )

    def calories(self, date: str) -> Envelope:
        return self.get(
            f"/1/user/-/activities/calories"
            f"/date/{date}/1d.json"
        )

    def activities(self, date: str) -> Envelope:
        return self.get(
            f"/1/user/-/activities/date/{date}.json"
        )

    def sleep(self, date: str) -> Envelope:
        return self.get(
            f"/1.2/user/-/sleep/date/{date}.json"
        )

    def sleep_range(self, start: str, end: str) -> Envelope:
        return self.get(
            f"/1.2/user/-/sleep/date/{start}/{end}.json"
        )

    def body(self, date: str) -> Envelope:
        return self.get(
            f"/1/user/-/body/date/{date}.json"
        )

    def body_range(self, start: str, end: str) -> Envelope:
        return self.get(
            f"/1/user/-/body/date/{start}/{end}.json"
        )


# -- Helpers -----------------------------------------------------------------


def _generate_pkce() -> tuple[str, str]:
    verifier = secrets.token_urlsafe(64)[:128]
    challenge = (
        base64.urlsafe_b64encode(
            hashlib.sha256(verifier.encode()).digest()
        )
        .rstrip(b"=")
        .decode()
    )
    return verifier, challenge
