#!/usr/bin/env python3
# uncompressed-markers.py — pull your client's review notes from uncompressed.io
# straight ONTO the current DaVinci Resolve timeline as markers. Run it again
# any time: it refreshes in place (old uncompressed markers are replaced, your
# own markers are never touched).
#
# WHAT IT DOES
#   1. Reads the CURRENT timeline's name and matches it to your uncompressed.io
#      clip (falls back to the Resolve project name; override below).
#   2. Fetches every review note — web room and Mac app alike.
#   3. Places one marker per note at its exact frame:
#        RED   = still to do        GREEN = applied (done)
#      Marker name = who said it · marker note = what they said.
#
# SETUP (once — same as the upload script)
#   1. Put this file in Resolve's Scripts folder:
#        macOS:   ~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility
#        Windows: %APPDATA%\Blackmagic Design\DaVinci Resolve\Support\Fusion\Scripts\Utility
#   2. Create a file called .uncompressed in your home folder containing:
#        api_key=uc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#      Your key lives in the uncompressed.io dashboard, SETTINGS → API key.
#      Optional extra line to pin a timeline to a clip when names don't match:
#        clip_title=Exact clip title on uncompressed.io
#   3. Workspace → Scripts → Utility → uncompressed-markers. Progress shows in
#      the Console (Workspace → Console). Click it again after each new round.
#
# Pure Python 3 standard library — Resolve's bundled Python needs no pip.

import json
import os
import re
import sys
import urllib.error
import urllib.request

BASE_URL = os.environ.get("UNCOMPRESSED_BASE_URL", "https://uncompressed.io")
TIMEOUT = 60
TAG = "uncompressed.io"          # customData tag — how we recognize our own markers
TODO_COLOR = "Red"
DONE_COLOR = "Green"


# ————— API key (same rules as the upload script) —————————————————————————————
def _parse_config(path):
    values = {}
    try:
        with open(path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if "=" in line and not line.startswith("#"):
                    k, _, v = line.partition("=")
                    values[k.strip()] = v.strip()
    except OSError:
        pass
    return values


def load_config():
    here = os.path.dirname(os.path.abspath(__file__))
    cfg = {}
    cfg.update(_parse_config(os.path.join(os.path.expanduser("~"), ".uncompressed")))
    cfg.update(_parse_config(os.path.join(here, ".uncompressed")))
    if os.environ.get("UNCOMPRESSED_API_KEY"):
        cfg.setdefault("api_key", os.environ["UNCOMPRESSED_API_KEY"])
    return cfg


def api(path, payload, key):
    req = urllib.request.Request(
        BASE_URL + path,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json", "Authorization": "Bearer " + key},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
        return json.loads(r.read().decode("utf-8"))


# ————— Title matching ————————————————————————————————————————————————————————
def norm(s):
    s = re.sub(r"\.(mov|mp4|m4v|mxf|mkv|webm|braw|r3d)$", "", (s or "").lower())
    return re.sub(r"[^a-z0-9]+", " ", s).strip()


def pick_clip(clips, timeline_name, project_name, pinned_title):
    if pinned_title:
        for c in clips:
            if norm(c["title"]) == norm(pinned_title):
                return c, "pinned in .uncompressed"
        print("!! clip_title=%r matches nothing on your account." % pinned_title)
    for label, name in (("timeline name", timeline_name), ("project name", project_name)):
        n = norm(name)
        if not n:
            continue
        exact = [c for c in clips if norm(c["title"]) == n]
        if len(exact) == 1:
            return exact[0], label
        contains = [c for c in clips if n in norm(c["title"]) or norm(c["title"]) in n]
        if len(contains) == 1:
            return contains[0], label + " (fuzzy)"
    return None, None


# ————— Resolve ———————————————————————————————————————————————————————————————
def get_resolve():
    r = globals().get("resolve")
    if r:
        return r
    try:
        import DaVinciResolveScript as dvr  # type: ignore
        return dvr.scriptapp("Resolve")
    except Exception:
        return None


def main():
    cfg = load_config()
    key = cfg.get("api_key", "")
    if not key.startswith("uc_"):
        print("No API key found. Put  api_key=uc_…  in ~/.uncompressed")
        print("(your key is in the uncompressed.io dashboard, SETTINGS → API key)")
        return 1

    r = get_resolve()
    if not r:
        print("Couldn't reach Resolve's scripting API — run from Workspace → Scripts.")
        return 1
    project = r.GetProjectManager().GetCurrentProject()
    timeline = project.GetCurrentTimeline() if project else None
    if not timeline:
        print("Open the timeline you want the markers on, then run this again.")
        return 1

    tl_name = timeline.GetName()
    fps = float(timeline.GetSetting("timelineFrameRate") or project.GetSetting("timelineFrameRate") or 24)
    # Resolve reports drop-frame rates as 29.97/59.94 already; frames = t × fps.

    print("Timeline: %s  (%.3f fps)" % (tl_name, fps))
    print("Fetching your review clips…")
    try:
        listing = api("/api/tool/reviews", {}, key)
    except urllib.error.HTTPError as e:
        print("uncompressed.io said HTTP %d — check your API key in SETTINGS." % e.code)
        return 1
    clips = listing.get("clips") or []
    if not clips:
        print("No clips with review notes yet — send a review link first.")
        return 0

    clip, how = pick_clip(clips, tl_name, project.GetName(), cfg.get("clip_title"))
    if not clip:
        print("Couldn't match this timeline to a clip. Your reviewed clips:")
        for c in clips:
            print("   · %s  (%d note%s)" % (c["title"], c["notes"], "" if c["notes"] == 1 else "s"))
        print("Rename the timeline to match, or add  clip_title=…  to ~/.uncompressed")
        return 1
    print("Matched by %s → %s" % (how, clip["title"]))

    notes = api("/api/tool/comments", {"videoId": clip["videoId"]}, key).get("notes") or []
    if not notes:
        print("No notes on that clip yet.")
        return 0

    # Replace our previous markers — never touch the editor's own.
    removed = 0
    for frame, m in sorted((timeline.GetMarkers() or {}).items()):
        if str(m.get("customData", "")).startswith(TAG):
            if timeline.DeleteMarkerAtFrame(int(frame)):
                removed += 1

    added = 0
    for n in notes:
        frame = int(round(float(n.get("t") or 0) * fps))
        color = DONE_COLOR if n.get("done") else TODO_COLOR
        name = (n.get("author") or "note").strip()[:60]
        text = (n.get("text") or "").strip()
        if n.get("done"):
            text = "[applied] " + text
        if n.get("has_drawing"):
            text += "  (has a drawing — grab the package from the workspace)"
        # A second note on the same frame would collide; nudge one frame right.
        placed = False
        for f in range(frame, frame + 6):
            if timeline.AddMarker(f, color, name, text, 1, TAG + ":" + str(n.get("id"))):
                placed = True
                break
        if placed:
            added += 1
    print("Done — %d marker%s placed (%d replaced). RED = to do, GREEN = applied."
          % (added, "" if added == 1 else "s", removed))
    print("New round of notes? Just run this script again.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
