"""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.sdk import (
    APIConnectionError,
    Configuration,
    InternalServerError,
    Istari,
    IstariError,
)
from urllib3.exceptions import MaxRetryError

CREDENTIALS_HELP = (
    "Put ISTARI_DIGITAL_API_URL and ISTARI_CLIENT_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."
)
# The name istari-digital-client itself reads for the Key. The unprefixed
# IDENTITY_SERVICE_SECRET_FILE is still accepted so older istari.env files keep working.
SECRET_FILE_NAMES = ("ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE", "IDENTITY_SERVICE_SECRET_FILE")


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, (APIConnectionError, 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.
    Server errors (5xx) and dropped connections are retried silently; the client does not retry them."""
    while True:
        try:
            job = client.jobs.get(job.id)
        except (InternalServerError, APIConnectionError):
            time.sleep(interval)
            continue
        label = 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)


def outputs_of(client, revision_id):
    """The revisions that jobs run on `revision_id` wrote, as (resource_id, revision) pairs.
    Each job output is tied to the revision it ran on by a `produces` relationship."""
    outputs = []
    for edge in client.resources.relationships.list(revision_id):
        written = edge.right_revision
        if (
            edge.relationship_type_name != "produces"
            or edge.left_revision.file_revision_id != revision_id
            or written.owning_entity_type == "job"
        ):
            continue
        resource_id = written.owning_entity_id
        outputs.append((resource_id, client.resources.revisions.get(resource_id, written.file_revision_id)))
    return outputs

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(*SECRET_FILE_NAMES)
if api_url and secret_file:
    assert Path(secret_file).is_file(), (
        f"{SECRET_FILE_NAMES[0]} 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 = Istari(configuration)

# 2. Connection check — one small authenticated read confirms the Istari Digital Platform
#    is reachable and your credential is accepted.
try:
    client.resources.list(size=1).first()
except (IstariError, MaxRetryError) as e:
    print(f"\nCannot reach the Istari Digital Platform ({type(e).__name__}).")
    print(CREDENTIALS_HELP)
    sys.exit(1)
print("Connected.")

# 3. Register the first spreadsheet (same as uploading on Resources)
resource = client.resources.create(baseline_path, "model")
print(f"Resource registered: {resource.name}")
print(f"  Resource ID:  {resource.resource_id}")
print(f"  Revision ID:  {resource.file_revision_id}")

# 4. First extraction — Open Spreadsheet → @istari:extract
job1 = client.jobs.create(
    resource_id=resource.resource_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.
#    Outputs hang off the revision the job ran on, so this list is job 1's alone.
outputs_1 = outputs_of(client, resource.file_revision_id)
print(f"\nResources produced by job 1 ({len(outputs_1)} items):")
named_cells_rev1 = None
for resource_id, rev in outputs_1:
    print(f"  - {rev.name}  (resource={resource_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 revision of the same resource
new_rev = client.resources.revisions.create(resource.resource_id, revised_path)
print("\nSecond file uploaded (new revision).")
print(f"  Resource ID:  {resource.resource_id}  (same as before)")
print(f"  Revision ID:  {new_rev.id}")

# 7. Second extraction on the current revision
job2 = client.jobs.create(
    resource_id=resource.resource_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, on the new revision
outputs_2 = outputs_of(client, new_rev.id)
print(f"\nResources produced by job 2 ({len(outputs_2)} items):")
named_cells_rev2 = None
for resource_id, rev in outputs_2:
    print(f"  - {rev.name}  (resource={resource_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 (resource and revisions from the Istari Digital Platform) ---\n")
print(f"Named parameters (named_cells) for {resource.name!r} — resource {resource.resource_id}")
print(f"  from: revision {resource.file_revision_id} · {resource.created.isoformat(timespec='seconds')} · user {resource.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}")
