"""Python 3.11+: helpers for authenticated server routes or native clients."""

import base64
import hashlib
import json
import secrets
import time
from urllib.error import HTTPError
from urllib.parse import parse_qs, quote_plus, urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def begin_link(config, scope="profile:read"):
    transaction = {
        "state": secrets.token_urlsafe(32),
        "verifier": secrets.token_urlsafe(32),
        "redirect_uri": config["redirect_uri"],
        "created_at": time.time(),
    }
    challenge = base64.urlsafe_b64encode(
        hashlib.sha256(transaction["verifier"].encode("ascii")).digest()
    ).rstrip(b"=").decode("ascii")
    params = {
        "response_type": "code",
        "client_id": config["client_id"],
        "redirect_uri": transaction["redirect_uri"],
        "scope": scope,
        "state": transaction["state"],
        "code_challenge": challenge,
        "code_challenge_method": "S256",
    }
    return config["issuer"] + "/oauth/authorize?" + urlencode(params), transaction


def request(config, path, fields=None, access_token=None):
    headers = {}
    body = None
    if fields is not None:
        fields = dict(fields)
        secret = config.get("client_secret")
        if secret:
            # This sample uses a backend registered for client_secret_basic.
            credentials = quote_plus(config["client_id"]) + ":" + quote_plus(secret)
            headers["Authorization"] = "Basic " + base64.b64encode(
                credentials.encode("ascii")
            ).decode("ascii")
        else:
            fields["client_id"] = config["client_id"]
        headers["Content-Type"] = "application/x-www-form-urlencoded"
        body = urlencode(fields).encode("ascii")
    if access_token:
        headers["Authorization"] = "Bearer " + access_token
    req = Request(config["issuer"] + path, data=body, headers=headers)
    # No redirects or automatic retries; never log the payload or tokens.
    try:
        with build_opener(NoRedirect()).open(req, timeout=15) as response:
            payload = response.read()
            return json.loads(payload) if payload else None
    except HTTPError as error:
        status = error.code
        error.close()
        raise RuntimeError(f"R+D request failed ({status})") from None


def finish_link(config, callback_url, transaction):
    # Caller atomically consumes the transaction from the initiating user's
    # session before invoking this, including on denial or failure.
    callback = urlsplit(callback_url)
    expected = urlsplit(transaction["redirect_uri"])
    params = parse_qs(callback.query, keep_blank_values=True, max_num_fields=20)
    if (
        (callback.scheme, callback.netloc, callback.path)
        != (expected.scheme, expected.netloc, expected.path)
        or callback.fragment
        or any(len(value) != 1 for value in params.values())
        or time.time() - transaction["created_at"] > 600
        or params.get("state") != [transaction["state"]]
        or params.get("iss") != [config["issuer"]]
    ):
        raise ValueError("Invalid or expired authorization response")
    if "error" in params:
        raise ValueError("Account linking was not approved")
    code = params.get("code", [""])[0]
    if not code:
        raise ValueError("Authorization code missing")
    return request(config, "/oauth/token", {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": transaction["redirect_uri"],
        "code_verifier": transaction["verifier"],
    })


def refresh(config, refresh_token):
    # Serialize per connection; atomically replace both stored tokens.
    # After a timeout or ambiguous result, do not retry this refresh token.
    return request(config, "/oauth/token", {
        "grant_type": "refresh_token", "refresh_token": refresh_token,
    })


def revoke(config, token):
    return request(config, "/oauth/revoke", {"token": token})


def read_profile(config, tokens):
    if "profile:read" not in tokens["scope"].split():
        raise ValueError("Profile permission was not granted")
    return request(config, "/api/v1/users/me", access_token=tokens["access_token"])
