"""
PictureFlow Connect: a small helper for publish tools.

    pip install requests
    export PICTUREFLOW_KEY=pf_live_...   # Settings > API keys, with "Change Tracker shows"

    from pictureflow import PictureFlow
    pf = PictureFlow()
    v = pf.publish_version(
        shot_id="cm3shot0001",
        step_id="comp",
        source_url="https://media.mystudio.example/NB101_010/comp/v012/NB101_010_comp_v012.mov",
        review_file="/renders/NB101_010_comp_v012_h264.mp4",   # optional, hosted for review
        thumbnail_file="/renders/NB101_010_comp_v012.jpg",      # optional
        comment="Sky warmed on the last 20 frames",
        external_ids={"shotgrid": 45210},
    )
    print(v["name"], v["url"])

Docs: https://www.pictureflow.pro/developers
"""
import mimetypes
import os
import time
import uuid

import requests

API = os.environ.get("PICTUREFLOW_API", "https://www.pictureflow.pro/api/v1")


class PictureFlowError(Exception):
    def __init__(self, status, error):
        self.status = status
        self.code = error.get("code")
        self.error = error
        super().__init__(f"{status} {self.code}: {error.get('message')}")


class PictureFlow:
    def __init__(self, key=None, api=API):
        self.api = api.rstrip("/")
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {key or os.environ['PICTUREFLOW_KEY']}"

    def _call(self, method, path, retries=4, **kw):
        # One idempotency key per call, so a retry after a dropped connection never makes a second version.
        headers = {"Idempotency-Key": str(uuid.uuid4())} if method in ("POST", "PATCH") else {}
        for attempt in range(retries + 1):
            try:
                r = self.session.request(method, f"{self.api}{path}", headers=headers, timeout=60, **kw)
            except requests.ConnectionError:
                if attempt == retries:
                    raise
                time.sleep(2 ** attempt)
                continue
            if r.status_code in (429, 500, 502, 503, 504) and attempt < retries:
                time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
                continue
            data = r.json()
            if r.status_code >= 400:
                raise PictureFlowError(r.status_code, data.get("error", {}))
            return data

    def get(self, path, **params):
        return self._call("GET", path, params=params)

    def post(self, path, body):
        return self._call("POST", path, json=body)

    def patch(self, path, body):
        return self._call("PATCH", path, json=body)

    def upload(self, show_id, path, kind="review"):
        """Hosts a file (kind "review" or "thumbnail") and returns its upload id."""
        content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
        if path.lower().endswith(".mov"):
            content_type = "video/quicktime"
        up = self.post(f"/shows/{show_id}/uploads", {
            "kind": kind, "content_type": content_type,
            "size": os.path.getsize(path), "filename": os.path.basename(path),
        })
        with open(path, "rb") as f:
            r = requests.put(up["upload"]["url"], data=f, headers=up["upload"]["headers"], timeout=3600)
        r.raise_for_status()
        return up["id"]

    def publish_version(self, shot_id, source_url=None, review_file=None, thumbnail_file=None,
                        step_id=None, task_id=None, comment=None, external_ids=None, show_id=None):
        """Sends the shot's next version for review. Returns the new shot_version."""
        if review_file or thumbnail_file:
            show_id = show_id or self.get(f"/shots/{shot_id}")["show_id"]
        body = {k: v for k, v in {
            "source_url": source_url, "step_id": step_id, "task_id": task_id,
            "comment": comment, "external_ids": external_ids,
        }.items() if v is not None}
        if review_file:
            body["review_upload_id"] = self.upload(show_id, review_file, "review")
        if thumbnail_file:
            body["thumbnail_upload_id"] = self.upload(show_id, thumbnail_file, "thumbnail")
        return self.post(f"/shots/{shot_id}/versions", body)

    def shot_by_code(self, show_id, code):
        """Finds a shot by its code (or None)."""
        for s in self.get(f"/shows/{show_id}/shots", code=code, limit=5)["data"]:
            if s["code"].lower() == code.lower():
                return s
        return None
