#!/usr/bin/env python3
"""Call the registry readiness endpoint and print the SDK pin that matches it.

A Key is preferred. When a Key file is present the script signs a client assertion,
exchanges it for a bearer token, and calls readiness through the API URL. Otherwise
a personal access token is sent directly as the readiness bearer. The token is not
printed. When both are set and the Key file exists, the Key is used.

Reads these names from the environment, or from --env FILE (KEY=value lines):

  ISTARI_DIGITAL_API_URL
    Service-router base URL, for example https://api.<your-instance>.istari.app
  ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE
    Path to the Access Key JSON file containing clientId, keyId, and key.
  ISTARI_REGISTRY_URL
    Registry URL from Developer Settings, for a personal access token.
  ISTARI_REGISTRY_AUTH_TOKEN
    The personal access token. Deprecated. Used only when no Key file is present.

Usage (macOS, Linux, and Windows):

  python -m pip install pyjwt
  python check-registry-readiness.py --env istari.env

Prints the answer first, one registry version and one SDK pin:

  registry_version=<x-istari-registry-version>
  sdk_version=<istari-digital-client pin>

Then skills=continue|stop, docs=<YYYY.MM> when skills=stop, then the readiness headers and JSON.

The signed assertion and bearer token are not printed.

sdk pin, from the newest listed registry version that is still <= the header:

  < 10.15.2                         no pin (exit 1)
  10.15.2 .. < 10.17.4              10.10.0   docs 2026.04
  10.17.4 .. < 10.21.2              10.11.1   docs 2026.05
  10.21.2 .. < 10.22.0              10.14.0   docs 2026.06
  10.22.0 .. < 11.2.0               11.0.0    docs 2026.07
  11.2.0                            13.0.0    skills=continue
  > 11.2.0 and < 11.5.0             13.1.9    skills=continue
  >= 11.5.0                         13.2.1    skills=continue

Requires Python 3. PyJWT is required for a Key (`python -m pip install pyjwt`) and
is not used for a personal access token. Do not install istari-digital-client
before this script prints sdk_version=.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request

AUGUST = (11, 2, 0)
SDK_13_2_1 = (11, 5, 0)
FLOOR = (10, 15, 2)
ENV_ALIASES = {
    "ISTARI_DIGITAL_REGISTRY_URL": "ISTARI_REGISTRY_URL",
    "ISTARI_DIGITAL_REGISTRY_AUTH_TOKEN": "ISTARI_REGISTRY_AUTH_TOKEN",
}
BANDS = (
    ((10, 22, 0), "11.0.0", "2026.07"),
    ((10, 21, 2), "10.14.0", "2026.06"),
    ((10, 17, 4), "10.11.1", "2026.05"),
    ((10, 15, 2), "10.10.0", "2026.04"),
)


def load_env_file(path: str) -> None:
    """Fill missing environment variables from a KEY=value file."""
    with open(path, encoding="utf-8") as env_file:
        for raw_line in env_file:
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            if line.startswith("export "):
                line = line[len("export ") :]
            if "=" not in line:
                continue
            key, value = line.split("=", 1)
            key = key.strip()
            value = value.strip()
            if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
                value = value[1:-1]
            os.environ.setdefault(key, value)
            alias = ENV_ALIASES.get(key)
            if alias:
                os.environ.setdefault(alias, value)


def env_value(name: str) -> str:
    return os.environ.get(name, "").strip()


def parse_version(raw: str) -> tuple[int, ...]:
    version_text = raw.strip()
    if "/" in version_text:
        version_text = version_text.rsplit("/", 1)[-1]
    if ":" in version_text:
        version_text = version_text.rsplit(":", 1)[-1]
    parts: list[int] = []
    for piece in version_text.split("."):
        digits: list[str] = []
        for char in piece:
            if char.isdigit():
                digits.append(char)
            else:
                break
        if not digits:
            raise ValueError(raw)
        parts.append(int("".join(digits)))
    if not parts:
        raise ValueError(raw)
    return tuple(parts)


def cmp_version(left: tuple[int, ...], right: tuple[int, ...]) -> int:
    width = max(len(left), len(right))
    left = left + (0,) * (width - len(left))
    right = right + (0,) * (width - len(right))
    return (left > right) - (left < right)


def sdk_lines(raw: str) -> list[str]:
    """Return the registry/sdk/skills lines for a readiness version header."""
    try:
        version = parse_version(raw)
    except ValueError:
        print(f"cannot parse registry version: {raw}", file=sys.stderr)
        sys.exit(1)
    if cmp_version(version, FLOOR) < 0:
        print(f"registry {raw} is older than 10.15.2; no SDK pin", file=sys.stderr)
        sys.exit(1)
    if cmp_version(version, SDK_13_2_1) >= 0:
        sdk, docs = "13.2.1", None
    elif cmp_version(version, AUGUST) == 0:
        sdk, docs = "13.0.0", None
    elif cmp_version(version, AUGUST) > 0:
        sdk, docs = "13.1.9", None
    else:
        sdk, docs = "", None
        for band_floor, band_sdk, band_docs in BANDS:
            if cmp_version(version, band_floor) >= 0:
                sdk, docs = band_sdk, band_docs
                break
    lines = [f"registry_version={raw}", f"sdk_version={sdk}"]
    if docs is None:
        lines.append("skills=continue")
    else:
        lines.extend(["skills=stop", f"docs={docs}"])
    return lines


def client_assertion(api_url: str, credentials_path: str) -> str:
    import jwt

    token_url = f"{api_url}/identity/oauth2/token"
    with open(credentials_path, encoding="utf-8") as credentials_file:
        credentials = json.load(credentials_file)
    now = int(time.time())
    return jwt.encode(
        {
            "iss": credentials["clientId"],
            "sub": credentials["clientId"],
            "aud": token_url,
            "iat": now,
            "exp": now + 300,
            "jti": str(uuid.uuid4()),
        },
        credentials["key"],
        algorithm="ES384",
        headers={"kid": credentials["keyId"]},
    )


def request(method: str, url: str, headers: dict[str, str], body: bytes | None) -> tuple[int, dict[str, str], bytes]:
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as response:
            return response.status, dict(response.headers.items()), response.read()
    except urllib.error.HTTPError as error:
        return error.code, dict(error.headers.items()), error.read()


def header_value(headers: dict[str, str], name: str) -> str:
    wanted = name.lower()
    for key, value in headers.items():
        if key.lower() == wanted:
            return value.strip()
    return ""


def access_token_from_key(api_url: str, credentials_path: str) -> str:
    """Exchange a signed Key assertion for a bearer token. The token is not printed."""
    token_url = f"{api_url}/identity/oauth2/token"
    try:
        assertion = client_assertion(api_url, credentials_path)
    except ModuleNotFoundError:
        print("PyJWT is required for a Key: python -m pip install pyjwt", file=sys.stderr)
        sys.exit(1)

    form = urllib.parse.urlencode(
        {
            "grant_type": "client_credentials",
            "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            "client_assertion": assertion,
        }
    ).encode()
    token_status, _token_headers, token_body = request(
        "POST",
        token_url,
        {"Content-Type": "application/x-www-form-urlencoded"},
        form,
    )
    if token_status != 200:
        print(f"token exchange failed: HTTP {token_status}", file=sys.stderr)
        sys.stderr.buffer.write(token_body)
        sys.stderr.write("\n")
        sys.exit(1)
    token = json.loads(token_body.decode()).get("access_token", "")
    if not token:
        print("token exchange returned no access_token", file=sys.stderr)
        sys.exit(1)
    return token


def bearer_token(raw: str) -> str:
    token = raw.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    return token


def main() -> None:
    parser = argparse.ArgumentParser(description="Read the registry version and print the matching SDK pin.")
    parser.add_argument("--env", help="KEY=value file to load before connecting")
    args = parser.parse_args()
    if args.env:
        load_env_file(args.env)

    api_url = env_value("ISTARI_DIGITAL_API_URL").rstrip("/")
    credentials_path = env_value("ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE")
    if credentials_path:
        credentials_path = os.path.expanduser(credentials_path)
    registry_url = env_value("ISTARI_REGISTRY_URL").rstrip("/")
    pat = bearer_token(env_value("ISTARI_REGISTRY_AUTH_TOKEN"))
    key_ready = bool(api_url and credentials_path and os.path.isfile(os.path.expanduser(credentials_path)))

    if key_ready:
        readiness_url = f"{api_url}/registry/api/v2/health/readiness"
        token = access_token_from_key(api_url, credentials_path)
    elif registry_url and pat:
        # The registry host serves /api/v2 directly. The API URL serves that same route under /registry.
        readiness_url = f"{registry_url}/api/v2/health/readiness"
        token = pat
    elif api_url and credentials_path:
        print(f"Key file not found: {credentials_path}", file=sys.stderr)
        sys.exit(1)
    else:
        print(
            "Set ISTARI_DIGITAL_API_URL and ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE, "
            "or ISTARI_REGISTRY_URL and ISTARI_REGISTRY_AUTH_TOKEN.",
            file=sys.stderr,
        )
        sys.exit(1)

    readiness_status, readiness_headers, readiness_body = request(
        "GET",
        readiness_url,
        {"Authorization": f"Bearer {token}"},
        None,
    )
    registry_version = header_value(readiness_headers, "x-istari-registry-version")
    if readiness_status == 200 and not registry_version:
        print("x-istari-registry-version header is missing", file=sys.stderr)
        sys.exit(1)
    if readiness_status == 200:
        for line in sdk_lines(registry_version):
            print(line)
        print()

    print(f"GET {readiness_url}")
    print(f"HTTP {readiness_status}")
    for key, value in readiness_headers.items():
        if key.lower().startswith("x-istari-"):
            print(f"{key}: {value}")
    print()
    sys.stdout.buffer.write(readiness_body)
    if not readiness_body.endswith(b"\n"):
        print()
    else:
        print()

    if readiness_status != 200:
        sys.exit(1)


if __name__ == "__main__":
    main()
