"""
OctopusWave - Rhino Link Plugin
Captures the active Rhino viewport and syncs it to your OctopusWave canvas.

HOW TO INSTALL
--------------
1. Save this file anywhere on your computer (e.g. C:\\OctopusWave\\rhino-link.py)
2. In Rhino: Tools -> PythonScript -> Run Script -> select this file.
3. On first run a dialog shows a short code. Visit the URL shown, sign in to
   OctopusWave, and enter the code. Click Approve.
4. Done - run the script any time to push the current viewport.

ADD A ONE-CLICK TOOLBAR BUTTON (optional)
------------------------------------------
Right-click any toolbar -> New Button -> paste into the Command field:
    _-RunPythonScript "C:\\OctopusWave\\rhino-link.py" Enter

REQUIREMENTS
------------
Rhino 7 or Rhino 8 (Windows or Mac). No extra packages needed.
"""

import os
import json
import time

import System
import System.Diagnostics
import System.Drawing
import System.Drawing.Imaging
import System.IO
import System.Net
import System.Text

import Rhino
import Rhino.UI

# ---------------------------------------------------------------------------
API_BASE    = "https://octopuswave.com"
CLIENT_NAME = "Rhino Link"

_appdata    = os.environ.get("APPDATA") or os.path.expanduser("~")
_token_dir  = os.path.join(_appdata, "OctopusWave")
_token_file = os.path.join(_token_dir, "token.json")
# ---------------------------------------------------------------------------


# ── HTTP ────────────────────────────────────────────────────────────────────

def _post(path, payload, token=None):
    wc = System.Net.WebClient()
    wc.Encoding = System.Text.Encoding.UTF8
    wc.Headers[System.Net.HttpRequestHeader.ContentType] = "application/json"
    if token:
        wc.Headers["Authorization"] = "Bearer " + token
    body = json.dumps(payload)
    try:
        resp = wc.UploadString(API_BASE + path, body)
        return json.loads(resp)
    except Exception as ex:
        # Try to read the error body
        try:
            clr_ex = getattr(ex, "clrException", None) or ex
            stream = clr_ex.Response.GetResponseStream()
            text   = System.IO.StreamReader(stream, System.Text.Encoding.UTF8).ReadToEnd()
            return json.loads(text)
        except:
            return {"error": str(ex)}


# ── Token storage ────────────────────────────────────────────────────────────

def _load():
    try:
        if not os.path.exists(_token_file):
            return None
        with open(_token_file, "r") as f:
            d = json.load(f)
        if d.get("expires_at", 0) > time.time() + 60:
            return d
    except:
        pass
    return None


def _save(access_token, refresh_token, expires_in):
    try:
        if not os.path.exists(_token_dir):
            os.makedirs(_token_dir)
        with open(_token_file, "w") as f:
            json.dump({
                "access_token":  access_token,
                "refresh_token": refresh_token,
                "expires_at":    time.time() + int(expires_in or 86400),
            }, f)
    except:
        pass


def _clear():
    try:
        if os.path.exists(_token_file):
            os.remove(_token_file)
    except:
        pass


# ── Auth ─────────────────────────────────────────────────────────────────────

def _refresh(refresh_token):
    r = _post("/api/auth/refresh", {"refreshToken": refresh_token})
    if r.get("accessToken"):
        _save(r["accessToken"], r.get("refreshToken", refresh_token), r.get("expiresIn", 86400))
        return r["accessToken"]
    return None


def _device_login():
    r = _post("/api/auth/device/start", {"clientName": CLIENT_NAME})
    device_code = r.get("deviceCode")
    user_code   = r.get("userCode", "???")
    url         = r.get("verificationUrlComplete") or r.get("verificationUrl", API_BASE + "/device")
    interval    = int(r.get("interval", 5))
    expires_in  = int(r.get("expiresIn", 900))

    if not device_code:
        Rhino.UI.Dialogs.ShowMessage(
            "Could not reach OctopusWave.\nCheck your internet connection and try again.",
            "OctopusWave - Connection Error")
        return None

    Rhino.UI.Dialogs.ShowMessage(
        "Connect Rhino to OctopusWave\n"
        "________________________________\n\n"
        "1. Your browser will open to OctopusWave.\n"
        "   (If it doesn't, copy the URL below.)\n\n"
        "2. Sign in if prompted.\n\n"
        "3. Enter this code when asked:\n\n"
        "        " + user_code + "\n\n"
        "4. Click Approve - then close this dialog.\n\n"
        "URL: " + url,
        "OctopusWave - Connect Rhino Link")

    try:
        System.Diagnostics.Process.Start(url)
    except:
        pass

    deadline = time.time() + expires_in
    Rhino.RhinoApp.SetCommandPrompt("Waiting for browser approval...")

    while time.time() < deadline:
        time.sleep(interval)
        poll = _post("/api/auth/device/poll", {"deviceCode": device_code})
        status = poll.get("status")

        if status == "approved":
            _save(poll["accessToken"], poll.get("refreshToken"), poll.get("expiresIn", 86400))
            Rhino.UI.Dialogs.ShowMessage(
                "Connected! Your Rhino viewports will now sync to OctopusWave.\n\n"
                "Run this script any time to push the active viewport.",
                "OctopusWave - Connected")
            return poll["accessToken"]

        elif status not in ("pending", None):
            Rhino.UI.Dialogs.ShowMessage("Login failed: " + str(poll), "OctopusWave - Error")
            return None

    Rhino.UI.Dialogs.ShowMessage(
        "Login timed out. Please run the script again.",
        "OctopusWave - Timeout")
    return None


def _get_token():
    d = _load()
    if d:
        if d.get("expires_at", 0) < time.time() + 300 and d.get("refresh_token"):
            t = _refresh(d["refresh_token"])
            if t:
                return t
        if d.get("access_token"):
            return d["access_token"]
    return _device_login()


# ── Viewport capture ──────────────────────────────────────────────────────────

def _capture():
    doc  = Rhino.RhinoDoc.ActiveDoc
    view = doc.Views.ActiveView
    vp   = view.ActiveViewport

    view_name    = vp.Name or "Perspective"
    display_mode = None
    try:
        dm = vp.DisplayMode
        if dm:
            display_mode = dm.LocalName or dm.EnglishName
    except:
        pass

    size   = System.Drawing.Size(1920, 1080)
    bitmap = view.CaptureToBitmap(size, True)

    stream = System.IO.MemoryStream()
    bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png)
    b64 = System.Convert.ToBase64String(stream.ToArray())
    return "data:image/png;base64," + b64, view_name, display_mode


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    token = _get_token()
    if not token:
        return

    Rhino.RhinoApp.SetCommandPrompt("Capturing viewport...")
    try:
        image_data, view_name, display_mode = _capture()
    except Exception as ex:
        Rhino.UI.Dialogs.ShowMessage(
            "Could not capture viewport:\n" + str(ex),
            "OctopusWave - Error")
        return

    Rhino.RhinoApp.SetCommandPrompt("Syncing to OctopusWave...")
    payload = {"imageData": image_data, "viewName": view_name}
    if display_mode:
        payload["displayMode"] = display_mode

    result = _post("/api/plugin/rhino-link/push", payload, token=token)

    if result.get("id") or result.get("imageUrl"):
        Rhino.RhinoApp.SetCommandPrompt("Synced! Viewport is live in your canvas.")

    elif result.get("error") in ("unauthorized", "invalid_token", "token_expired"):
        _clear()
        Rhino.UI.Dialogs.ShowMessage(
            "Session expired. Run the script again to reconnect.",
            "OctopusWave - Reconnect")

    elif result.get("error") == "plan_required":
        Rhino.UI.Dialogs.ShowMessage(
            "Rhino Link is part of the OctopusWave Studio plan.\n"
            "Upgrade at octopuswave.com/pricing.",
            "OctopusWave - Studio plan")

    elif result.get("error") == "internal_beta_only":
        Rhino.UI.Dialogs.ShowMessage(
            "Rhino Link is currently in private beta.\n"
            "Contact hello@octopuswave.com to join the waitlist.",
            "OctopusWave - Private Beta")

    else:
        msg = result.get("message") or result.get("error") or str(result)
        Rhino.UI.Dialogs.ShowMessage("Sync failed: " + str(msg), "OctopusWave - Error")


main()
