#!/usr/bin/env python3
"""Download a pinned Oreline edition using Python's standard library.

ORELINE_API_KEY=... python3 oreline_sync.py
Optional: ORELINE_API_URL, ORELINE_EDITION, ORELINE_OUTPUT
Outputs a new edition directory. Existing downloads are never overwritten.
"""
import json
import os
from pathlib import Path
import time
from uuid import UUID
from urllib.error import HTTPError
from urllib.parse import urlencode, urlparse
from urllib.request import Request, HTTPRedirectHandler, build_opener

BASE = os.environ.get("ORELINE_API_URL", "https://oreline.sirca.io").rstrip("/")
KEY = os.environ["ORELINE_API_KEY"]
if urlparse(BASE).scheme != "https" and urlparse(BASE).hostname not in ("localhost", "127.0.0.1"):
    raise ValueError("Use HTTPS for API credentials.")
if urlparse(BASE).username or urlparse(BASE).query or urlparse(BASE).fragment or urlparse(BASE).path:
    raise ValueError("ORELINE_API_URL must be an origin, without a path or credentials.")


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise RuntimeError("Refusing to forward API credentials through a redirect.")


opener = build_opener(NoRedirect())


def get(path):
    if not path.startswith("/api/v1/") or "://" in path or "\\" in path:
        raise ValueError("Unexpected pagination path")
    for attempt in range(6):
        try:
            with opener.open(Request(BASE + path, headers={"Authorization": "Bearer " + KEY}), timeout=30) as response:
                result = json.load(response)
            time.sleep(1.1)  # Respect the shared 60/minute allowance.
            return result
        except HTTPError as exc:
            if exc.code not in (429, 502, 503, 504):
                raise RuntimeError("Oreline returned HTTP %s. Check access and key expiry." % exc.code) from None
            wait = int(exc.headers.get("Retry-After", 2 ** attempt))
            if wait > 120:
                raise RuntimeError("Monthly allowance reached. Resume after the usage reset.") from None
            time.sleep(max(1, wait))
    raise RuntimeError("API unavailable after retries. No complete edition was written.")


def main():
    edition = os.environ.get("ORELINE_EDITION")
    manifest = get("/api/v1/manifest" + ("?" + urlencode({"edition": edition}) if edition else ""))
    edition = manifest["edition_id"]
    UUID(edition)
    root = Path(os.environ.get("ORELINE_OUTPUT", "oreline-data"))
    target = root / edition
    resume = os.environ.get("ORELINE_RESUME") == "1"
    target.mkdir(parents=True, exist_ok=resume)
    manifest_path = target / "manifest.json"
    if manifest_path.exists():
        previous = json.loads(manifest_path.read_text(encoding="utf-8"))
        if previous["edition_id"] != edition or previous["data"] != manifest["data"]:
            raise RuntimeError("Existing manifest does not match this edition.")
    else:
        manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    for resource in manifest["data"]:
        name = resource["resource"]
        if not name.replace("-", "").isalnum():
            raise ValueError("Invalid resource name")
        path = "/api/v1/" + name + "?" + urlencode({"edition": edition, "limit": 100})
        count = 0
        complete = target / (name + ".jsonl")
        if resume and complete.exists():
            with complete.open(encoding="utf-8") as existing:
                records = [json.loads(line) for line in existing]
            if len(records) != resource["records"] or len({r["id"] for r in records}) != len(records):
                raise RuntimeError("Existing collection failed validation: " + name)
            print(name + ": already complete")
            continue
        partial = target / (name + ".jsonl.partial")
        with partial.open("w" if resume else "x", encoding="utf-8") as output:
            while path:
                page = get(path)
                if page["edition_id"] != edition:
                    raise RuntimeError("Edition changed during download")
                for record in page["data"]:
                    output.write(json.dumps(record, ensure_ascii=False) + "\n")
                    count += 1
                path = page["next"]
        if count != resource["records"]:
            raise RuntimeError("Record count mismatch for " + name)
        partial.rename(complete)
        print(name + ": " + str(count) + " records")
    (target / "COMPLETE").write_text(edition + "\n", encoding="utf-8")
    print("Complete edition: " + str(target))


if __name__ == "__main__":
    main()
