"""Tutorial 202: register a spreadsheet, extract data twice, compare named_cells JSON locally."""

import json
import os
import sys
import time
from pathlib import Path

from istari_digital_client import Client, Configuration
from istari_digital_client.exceptions import ApiException, ServiceException
from urllib3.exceptions import MaxRetryError

CREDENTIALS_HELP = (
    "Put ISTARI_DIGITAL_API_URL and IDENTITY_SERVICE_SECRET_FILE in istari.env "
    "(or the environment). Developer Settings → Keys: copy the API URL and the path "
    "to the downloaded key file — not the key contents. See Python Client 201 or 202."
)


def _env(*names: str) -> str:
    for n in names:
        v = (os.environ.get(n) or "").strip()
        if v:
            return v
    return ""


def _load_istari_env(path: str = "istari.env") -> None:
    """Load KEY=VALUE lines without overwriting variables already set in the shell."""
    p = Path(path)
    if not p.is_file():
        return
    for line in p.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, _, v = line.partition("=")
        os.environ.setdefault(k.strip(), v.strip().strip("'").strip('"'))


def _on_uncaught(exc_type, exc, tb):
    if issubclass(exc_type, MaxRetryError):
        print("\nCannot reach the Istari Digital Platform.")
        print(CREDENTIALS_HELP)
        sys.exit(1)
    sys.__excepthook__(exc_type, exc, tb)


sys.excepthook = _on_uncaught


def wait_for_job(client, job, interval=5):
    """Poll a job until it reaches a terminal state, printing the status each loop.
    Returns the refreshed Job; raises if it ends in Failed or Canceled.
    Transient service errors (5xx) are retried silently."""
    while True:
        try:
            job = client.get_job(job.id)
        except ServiceException:
            time.sleep(interval)
            continue
        label = getattr(job.status.name, "value", job.status.name)
        print(f"  status: {label}")
        if label == "Completed":
            return job
        if label in ("Failed", "Canceled"):
            raise RuntimeError(f"Job {job.id} ended in state {label}.")
        time.sleep(interval)

baseline_path = Path("Group3-UAS-Requirements.xlsx")
revised_path = Path("Group3-UAS-Requirements-v2.xlsx")

assert baseline_path.is_file(), (
    f"Expected {baseline_path.name!r} in {str(Path.cwd())!r}. "
    "Download the sample from the Python Client 202 tutorial, save it next to this script, then run again."
)
assert revised_path.is_file(), (
    f"Expected {revised_path.name!r} in {str(Path.cwd())!r}. "
    "Download the v2 sample from the same tutorial, same folder, then run again."
)

_load_istari_env()

api_url = _env("ISTARI_DIGITAL_API_URL")
secret_file = _env("IDENTITY_SERVICE_SECRET_FILE")
if api_url and secret_file:
    assert Path(secret_file).is_file(), (
        f"IDENTITY_SERVICE_SECRET_FILE points at {secret_file}, which does not exist. "
        "Use the absolute path of the key file you downloaded from Developer Settings."
    )
    configuration = Configuration(
        digital_api_url=api_url,
        identity_service_secret_file=secret_file,
        identity_service_enabled=True,
    )
else:
    registry_url = _env("ISTARI_DIGITAL_REGISTRY_URL", "ISTARI_REGISTRY_URL")
    token = _env(
        "ISTARI_DIGITAL_REGISTRY_AUTH_TOKEN",
        "ISTARI_REGISTRY_AUTH_TOKEN",
        "ISTARI_PERSONAL_ACCESS_TOKEN",
    )
    assert api_url or registry_url, CREDENTIALS_HELP
    assert secret_file or token, CREDENTIALS_HELP
    configuration = Configuration(
        registry_url=registry_url,
        registry_auth_token=token,
    )

# 1. Connect — Key (current) or PAT (deprecated July 2026)
client = Client(configuration)

# 2. Health check — confirms the Istari Digital Platform is reachable and your credential is accepted.
try:
    report = client.readiness_check()  # or client.liveness_check() for a lighter probe
except (ApiException, MaxRetryError) as e:
    print(f"\nCannot reach the Istari Digital Platform ({type(e).__name__}).")
    print(CREDENTIALS_HELP)
    sys.exit(1)
print(report)
if not report.healthy:
    raise RuntimeError(f"Platform reports unhealthy: {report}")

# 3. Register the first spreadsheet (same as uploading in Files)
model = client.add_model(path=baseline_path)
first_rev = model.file.revisions[0]
print(f"Model registered: {first_rev.name}")
print(f"  Model ID:     {model.id}")
print(f"  Revision ID:  {first_rev.id}")

# 4. First extraction — Open Spreadsheet → @istari:extract
job1 = client.add_job(
    model_id=model.id,
    function="@istari:extract",
    tool_name="open_spreadsheet",
)
print(f"\nJob: {job1.id}")
job1 = wait_for_job(client, job1)
print("First extraction done.")

# 5. Inspect what job 1 produced and capture the named_cells revision.
#    Each Product points to the exact FileRevision the agent wrote (race-safe).
products_1 = job1.revision.products or []
print(f"\nResources produced by job 1 ({len(products_1)} items):")
named_cells_rev1 = None
for p in products_1:
    rev = p.revision
    print(f"  - {rev.name}  (file={rev.file_id}  rev={rev.id})")
    if rev.name and "named_cells" in rev.name:
        named_cells_rev1 = rev

if named_cells_rev1 is None:
    raise RuntimeError("No named_cells.json was produced by the first extraction job.")

path_named_cells_1 = Path("named_cells_rev1.json")
path_named_cells_1.write_bytes(named_cells_rev1.read_bytes())
print(f"\nSaved {path_named_cells_1.resolve()}  (rev {named_cells_rev1.id})")

# 6. Add the second spreadsheet as a new version of the same file
model_with_2nd_rev = client.update_model(model_id=model.id, path=revised_path)
new_rev = model_with_2nd_rev.file.revisions[-1]
print("\nSecond file uploaded (new revision).")
print(f"  Model ID:     {model_with_2nd_rev.id}  (same as before)")
print(f"  Revision ID:  {new_rev.id}")

# 7. Second extraction on the current revision
job2 = client.add_job(
    model_id=model.id,
    function="@istari:extract",
    tool_name="open_spreadsheet",
)
print(f"\nJob: {job2.id}")
job2 = wait_for_job(client, job2)
print("Second extraction done.")

# 8. Same pattern for job 2
products_2 = job2.revision.products or []
print(f"\nResources produced by job 2 ({len(products_2)} items):")
named_cells_rev2 = None
for p in products_2:
    rev = p.revision
    print(f"  - {rev.name}  (file={rev.file_id}  rev={rev.id})")
    if rev.name and "named_cells" in rev.name:
        named_cells_rev2 = rev

if named_cells_rev2 is None:
    raise RuntimeError("No named_cells.json was produced by the second extraction job.")

path_named_cells_2 = Path("named_cells_rev2.json")
path_named_cells_2.write_bytes(named_cells_rev2.read_bytes())
print(f"\nSaved {path_named_cells_2.resolve()}  (rev {named_cells_rev2.id})")

# 9. Compare: list named-cell value changes between the two extractions
cells_1 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_1.read_text(encoding="utf-8"))}
cells_2 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_2.read_text(encoding="utf-8"))}
changes = sorted(
    (name, cells_1.get(name), cells_2.get(name))
    for name in set(cells_1) | set(cells_2)
    if cells_1.get(name) != cells_2.get(name)
)


print("\n--- Summary (file and revisions from the Istari Digital Platform) ---\n")
print(f"Named parameters (named_cells) for {first_rev.name!r} — model {model.id}")
print(f"  from: revision {first_rev.id} · {first_rev.created.isoformat(timespec='seconds')} · user {first_rev.created_by_id}")
print(f"  to:   revision {new_rev.id} · {new_rev.created.isoformat(timespec='seconds')} · user {new_rev.created_by_id}")

print(f"\nChange detail ({len(changes)} named cell(s) changed):")
if not changes:
    print("  (no value changes)")
for name, before, after in changes:
    print(f"  - {name}: {before} → {after}")
