"""HarvestWave phone-number lookup example.

One process-wide httpx.Client (explicit connection limit, HTTP keep-alive).
Bearer tokens are reused until shortly before expires_in. No refresh token.

Depends on the httpx package.

Secrets are read from the environment only:

* HARVESTWAVE_CLIENT_ID
* HARVESTWAVE_CLIENT_SECRET
"""

from __future__ import annotations

import json
import os
import sys
import threading
import time
from dataclasses import dataclass

import httpx  # dependency: httpx

_TOKEN_URL = "https://api.harvestwave.com/oauth/token"
_LOOKUP_URL = "https://api.harvestwave.com/v1/lookups"
_DEFAULT_EXPIRES_IN_SECONDS = 28800
# Renew before expires_in so a lookup does not start on a dying token.
_RENEW_SKEW_SECONDS = 60
_CLIENT_ID_ENV = "HARVESTWAVE_CLIENT_ID"
_CLIENT_SECRET_ENV = "HARVESTWAVE_CLIENT_SECRET"

# Connectivity checks use DEMO test numbers (NPA-555, last four 0001-0199), not a credit purchase.

# Process-wide pool. Keep-alive is capped by keepalive_expiry. Not one client per call.
_HTTP = httpx.Client(
    limits=httpx.Limits(
        max_connections=10,
        max_keepalive_connections=10,
        keepalive_expiry=30.0,
    ),
    timeout=httpx.Timeout(30.0),
)
_TOKEN_LOCK = threading.Lock()
_access_token: str | None = None
_token_deadline_monotonic: float = 0.0


class HarvestWaveError(Exception):
    """Raised when a response body does not match the lookup contract."""


@dataclass(frozen=True, slots=True)
class Business:
    """One entry from the lookup ``businesses`` array."""

    name: str
    confidence: float


@dataclass(frozen=True, slots=True)
class LookupResult:
    """Lookup payload: ``isBusiness`` and ``businesses``."""

    is_business: bool
    businesses: tuple[Business, ...]


def _env(name: str) -> str:
    """Return a required environment variable, or raise if it is empty."""
    value = os.environ.get(name)
    if value is None or value.strip() == "":
        raise HarvestWaveError(f"Set environment variable {name}")
    return value


def _json_object(response: httpx.Response) -> dict[str, object]:
    """Parse a response body as a JSON object."""
    try:
        body: object = response.json()
    except ValueError as exc:
        raise HarvestWaveError("Response was not JSON") from exc
    if not isinstance(body, dict):
        raise HarvestWaveError("Response JSON was not an object")
    return body


def _request_access_token() -> tuple[str, int]:
    """POST client credentials and return ``(access_token, expires_in)``.

    The token endpoint accepts form-urlencoded or JSON. This client sends form.
    """
    response = _HTTP.post(
        _TOKEN_URL,
        data={
            "grant_type": "client_credentials",
            "client_id": _env(_CLIENT_ID_ENV),
            "client_secret": _env(_CLIENT_SECRET_ENV),
        },
        headers={"Accept": "application/json"},
    )
    response.raise_for_status()
    body = _json_object(response)
    access_token = body.get("access_token")
    token_type = body.get("token_type")
    if not isinstance(access_token, str) or access_token == "":
        raise HarvestWaveError("Token response missing access_token")
    if token_type != "Bearer":
        raise HarvestWaveError("Token response token_type was not Bearer")
    raw_expires = body.get("expires_in", _DEFAULT_EXPIRES_IN_SECONDS)
    if isinstance(raw_expires, bool) or not isinstance(raw_expires, (int, float)):
        raise HarvestWaveError("Token response expires_in was not a number")
    expires_in = int(raw_expires)
    if expires_in < 0:
        raise HarvestWaveError("Token response expires_in was negative")
    return access_token, expires_in


def _bearer_token() -> str:
    """Return the cached Bearer token, refreshing shortly before expiry."""
    global _access_token, _token_deadline_monotonic
    with _TOKEN_LOCK:
        now = time.monotonic()
        if _access_token is not None and now < _token_deadline_monotonic:
            return _access_token
        access_token, expires_in = _request_access_token()
        skew = min(float(_RENEW_SKEW_SECONDS), expires_in / 2.0)
        _access_token = access_token
        _token_deadline_monotonic = now + max(0.0, expires_in - skew)
        return access_token


def _parse_business(item: object) -> Business:
    """Read one business object's contracted ``name`` and ``confidence``."""
    if not isinstance(item, dict):
        raise HarvestWaveError("Lookup business entry was not an object")
    name = item.get("name")
    confidence = item.get("confidence")
    if not isinstance(name, str):
        raise HarvestWaveError("Lookup business missing name")
    if isinstance(confidence, bool) or not isinstance(confidence, (int, float)):
        raise HarvestWaveError("Lookup business missing confidence")
    return Business(name=name, confidence=float(confidence))


def _parse_lookup(body: dict[str, object]) -> LookupResult:
    """Read ``isBusiness`` and ``businesses`` from a lookup body."""
    is_business = body.get("isBusiness")
    raw_businesses = body.get("businesses")
    if not isinstance(is_business, bool):
        raise HarvestWaveError("Lookup response missing isBusiness")
    if not isinstance(raw_businesses, list):
        raise HarvestWaveError("Lookup response missing businesses")
    businesses = tuple(_parse_business(item) for item in raw_businesses)
    return LookupResult(is_business=is_business, businesses=businesses)


class HarvestWaveClient:
    """Lookup client. Every instance shares the process-wide HTTP pool."""

    def lookup(self, phone_number: str) -> LookupResult:
        """POST /v1/lookups with ``{"phoneNumber": phone_number}``.

        Args:
            phone_number: Number to look up, for example ``+15551234567``.

        Returns:
            The ``isBusiness`` flag and the ``businesses`` list.
        """
        response = _HTTP.post(
            _LOOKUP_URL,
            json={"phoneNumber": phone_number},
            headers={
                "Authorization": f"Bearer {_bearer_token()}",
                "Accept": "application/json",
            },
        )
        response.raise_for_status()
        return _parse_lookup(_json_object(response))

    def close(self) -> None:
        """Close the shared pool and drop the cached access token."""
        global _access_token, _token_deadline_monotonic
        with _TOKEN_LOCK:
            _access_token = None
            _token_deadline_monotonic = 0.0
            _HTTP.close()


def main(argv: list[str] | None = None) -> int:
    """Look up one number and print the contracted JSON object."""
    args = list(sys.argv[1:] if argv is None else argv)
    if len(args) != 1:
        print(
            "usage: python harvestwave_client.py <phoneNumber>",
            file=sys.stderr,
        )
        return 2
    result = HarvestWaveClient().lookup(args[0])
    payload = {
        "isBusiness": result.is_business,
        "businesses": [
            {"name": item.name, "confidence": item.confidence}
            for item in result.businesses
        ],
    }
    print(json.dumps(payload))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
