#!/usr/bin/env python3
"""
Dump (and optionally watch) a user's index-feed collections in Firestore:

    items/{uid}/items
    lists/{uid}/lists

Defaults to ericmigi@gmail.com. Auth uses Application Default Credentials
(`gcloud auth application-default login`). Project is read from
google-services.json next to this script.

Usage:
    ./dump_user_indexfeed.py
    ./dump_user_indexfeed.py --email someone@example.com
    ./dump_user_indexfeed.py --uid <firebase-uid>
    ./dump_user_indexfeed.py --watch 5      # poll every 5s and re-print on diff
    ./dump_user_indexfeed.py --json         # raw json instead of pretty
"""

import argparse
import hashlib
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

import firebase_admin
import google.auth
from firebase_admin import auth, credentials, firestore

DEFAULT_EMAIL = "ericmigi@gmail.com"
ROOT = Path(__file__).resolve().parent
GOOGLE_SERVICES_JSON = ROOT / "google-services.json"


def project_id_from_google_services() -> str:
    if not GOOGLE_SERVICES_JSON.exists():
        sys.exit(f"google-services.json not found at {GOOGLE_SERVICES_JSON}")
    data = json.loads(GOOGLE_SERVICES_JSON.read_text())
    pid = data.get("project_info", {}).get("project_id")
    if not pid:
        sys.exit("project_info.project_id missing in google-services.json")
    return pid


def init_app(project_id: str):
    if firebase_admin._apps:
        return firebase_admin.get_app()
    # Force the quota project to the Firebase project — gcloud ADC is often
    # anchored to a different GCP project (e.g. `eng-dash`), which breaks the
    # Identity Toolkit `x-goog-user-project` quota check otherwise.
    raw_creds, _ = google.auth.default()
    if hasattr(raw_creds, "with_quota_project"):
        raw_creds = raw_creds.with_quota_project(project_id)
    cred = credentials.ApplicationDefault()
    cred._g_credential = raw_creds  # use our quota-pinned creds for auth + firestore
    return firebase_admin.initialize_app(cred, {"projectId": project_id})


def resolve_uid(email: str | None, uid: str | None) -> str:
    if uid:
        return uid
    if not email:
        sys.exit("Either --email or --uid required")
    try:
        rec = auth.get_user_by_email(email)
    except auth.UserNotFoundError:
        sys.exit(f"No Firebase user found for {email}")
    return rec.uid


def fmt_ts(value) -> str:
    """Firestore Timestamp / int (epoch ms) / ISO string / gitlive {epochSeconds,
    nanosecondsOfSecond} dict → readable local time."""
    if value is None:
        return "—"
    if isinstance(value, (int, float)):
        # epoch ms heuristic
        seconds = value / 1000 if value > 1e11 else value
        return datetime.fromtimestamp(seconds, tz=timezone.utc).astimezone().isoformat(timespec="seconds")
    if isinstance(value, dict):
        # gitlive Firestore Timestamp serialized via kotlin.time.Instant ->
        # {"epochSeconds": ..., "nanosecondsOfSecond": ...}
        secs = value.get("epochSeconds")
        nanos = value.get("nanosecondsOfSecond", 0)
        if secs is not None:
            return datetime.fromtimestamp(secs + nanos / 1e9, tz=timezone.utc).astimezone().isoformat(timespec="seconds")
    if hasattr(value, "isoformat"):
        return value.astimezone().isoformat(timespec="seconds")
    return str(value)


def fetch_lists(db, uid: str) -> list[dict]:
    docs = db.collection("lists").document(uid).collection("lists").stream()
    out = []
    for d in docs:
        data = d.to_dict() or {}
        data["_id"] = d.id
        out.append(data)
    out.sort(key=lambda r: (r.get("seed") or "z", r.get("title") or ""))
    return out


def fetch_items(db, uid: str) -> list[dict]:
    docs = db.collection("items").document(uid).collection("items").stream()
    out = []
    for d in docs:
        data = d.to_dict() or {}
        data["_id"] = d.id
        out.append(data)
    out.sort(key=lambda r: r.get("createdAt") or 0, reverse=True)
    return out


def render_pretty(uid: str, lists: list[dict], items: list[dict]) -> str:
    lines: list[str] = []
    lines.append(f"uid={uid}")
    lines.append("")
    lines.append(f"=== lists ({len(lists)}) ===")
    by_id = {}
    for l in lists:
        seed = l.get("seed") or "—"
        title = l.get("title") or "(untitled)"
        kind = l.get("listKind") or "?"
        deleted = " [deleted]" if l.get("deleted") else ""
        lines.append(f"  {l['_id']:18s}  seed={seed:11s}  kind={kind:9s}  {title!r}{deleted}")
        by_id[l["_id"]] = title

    lines.append("")
    lines.append(f"=== items ({len(items)}) ===")
    if not items:
        lines.append("  (none)")
    for it in items:
        kind = it.get("kind") or "?"
        title = (it.get("title") or "").replace("\n", " ")
        if len(title) > 60:
            title = title[:57] + "…"
        parents = it.get("parentListIds") or []
        parent_names = [by_id.get(p, p) for p in parents]
        parents_s = ",".join(parent_names) if parent_names else "(loose)"
        done = "✓" if it.get("done") else " "
        del_mark = "✗" if it.get("deleted") else " "
        due = it.get("dueAt")
        due_s = f"  due={fmt_ts(due)}" if due else ""
        body = (it.get("body") or "").replace("\n", " ")
        body_s = f"\n      body: {body[:120]}{'…' if len(body) > 120 else ''}" if body else ""
        fields_json = it.get("fieldsJson") or ""
        try:
            fields = json.loads(fields_json) if fields_json else {}
        except json.JSONDecodeError:
            fields = {"_raw": fields_json}
        fields_s = (
            f"\n      fields: {json.dumps(fields, default=str, ensure_ascii=False)}"
            if fields else ""
        )
        rec = it.get("sourceRecordingId") or "—"
        ts = fmt_ts(it.get("createdAt"))
        lines.append(
            f"  [{done}{del_mark}] {kind:11s} {title!r:62s}  parents=[{parents_s}]{due_s}\n"
            f"      created={ts}  recording={rec}  id={it['_id']}{body_s}{fields_s}"
        )
    return "\n".join(lines)


def render_json(uid: str, lists: list[dict], items: list[dict]) -> str:
    return json.dumps(
        {"uid": uid, "lists": lists, "items": items},
        default=str,
        indent=2,
        ensure_ascii=False,
    )


def fingerprint(lists: list[dict], items: list[dict]) -> str:
    blob = json.dumps(
        {"l": [(l["_id"], l.get("updatedAt"), l.get("deleted")) for l in lists],
         "i": [(i["_id"], i.get("updatedAt"), i.get("done"), i.get("deleted")) for i in items]},
        default=str, sort_keys=True,
    )
    return hashlib.sha1(blob.encode()).hexdigest()


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--email", help=f"User email (default: {DEFAULT_EMAIL})")
    p.add_argument("--uid", help="Firebase UID (overrides --email)")
    p.add_argument("--json", action="store_true", help="Emit raw JSON instead of pretty layout")
    p.add_argument("--watch", type=float, metavar="SECS", help="Poll every SECS and re-print when content changes")
    args = p.parse_args()

    email = args.email if (args.email or args.uid) else DEFAULT_EMAIL
    project = project_id_from_google_services()
    init_app(project)
    uid = resolve_uid(email, args.uid)
    db = firestore.client()

    last_fp = None
    while True:
        lists = fetch_lists(db, uid)
        items = fetch_items(db, uid)
        fp = fingerprint(lists, items)
        if fp != last_fp:
            print("\n" + "─" * 80)
            print(f"[{datetime.now().astimezone().isoformat(timespec='seconds')}] project={project}")
            if args.json:
                print(render_json(uid, lists, items))
            else:
                print(render_pretty(uid, lists, items))
            sys.stdout.flush()
            last_fp = fp
        if not args.watch:
            return 0
        time.sleep(max(args.watch, 1.0))


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(130)
