"""Cloud sync — fetch pitches from Cloudflare Worker and push decisions back."""

from __future__ import annotations

import json
import os
import secrets
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
import webbrowser
from datetime import datetime
from typing import Callable

WORKER_URL = os.environ.get("SUPERPITCH_WORKER_URL", "https://superpitch.workers.dev")
WEB_URL    = os.environ.get("SUPERPITCH_WEB_URL",    "https://superpitch.dev")

POLL_INTERVAL  = 2 * 60   # 2 minutes between background polls
_AUTH_INTERVAL = 2.0       # seconds between CLI auth poll attempts
_AUTH_TIMEOUT  = 120       # seconds to wait for browser auth


def _request(
    method: str,
    path: str,
    *,
    body: dict | None = None,
    token: str | None = None,
    timeout: int = 10,
) -> tuple[dict, int]:
    url = f"{WORKER_URL}{path}"
    headers: dict[str, str] = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read()), resp.status
    except urllib.error.HTTPError as exc:
        try:
            return json.loads(exc.read()), exc.code
        except Exception:
            return {"error": str(exc)}, exc.code
    except Exception as exc:
        return {"error": str(exc)}, 0


# ── Auth flow ──────────────────────────────────────────────────────────────────

def start_cli_auth() -> str:
    """Register a pending token with the Worker and open the browser. Returns cli_token."""
    cli_token = f"sp-{secrets.token_urlsafe(20)}"
    data, status = _request("POST", "/auth/cli/init", body={"token": cli_token})
    if status != 200 or not data.get("ok"):
        raise RuntimeError(data.get("error", "failed to start auth"))
    webbrowser.open(f"{WEB_URL}/auth/cli?token={urllib.parse.quote(cli_token)}")
    return cli_token


def poll_cli_auth(
    cli_token: str,
    on_tick: Callable[[int], None] | None = None,
) -> dict | None:
    """Poll until browser auth completes. Returns { session_token, email, slug } or None.

    on_tick(elapsed_seconds) is called after each poll interval if provided.
    """
    start    = time.time()
    deadline = start + _AUTH_TIMEOUT
    while time.time() < deadline:
        time.sleep(_AUTH_INTERVAL)
        if on_tick:
            on_tick(int(time.time() - start))
        data, status = _request("GET", f"/auth/cli/poll?token={urllib.parse.quote(cli_token)}")
        if status == 200 and data.get("ready"):
            return data
    return None


# ── Handle & thesis ────────────────────────────────────────────────────────────

def check_handle(slug: str) -> dict:
    """Check if a handle is available. Returns { available, reason? }."""
    data, _ = _request("GET", f"/handle/check?slug={urllib.parse.quote(slug)}")
    return data


def claim_handle(session_token: str, slug: str, email: str, name: str) -> dict:
    """Claim a handle. Returns { slug, claimed } or { error }."""
    data, status = _request(
        "POST", "/handle/claim",
        body={"slug": slug, "email": email, "name": name},
        token=session_token,
    )
    return data if status in (200, 409) else {"error": data.get("error", "failed")}


def set_thesis(session_token: str, thesis: str) -> bool:
    """Save thesis to KV. Returns True on success."""
    data, status = _request(
        "POST", "/thesis",
        body={"thesis": thesis},
        token=session_token,
    )
    return status == 200 and bool(data.get("saved"))


# ── Pitch sync ─────────────────────────────────────────────────────────────────

def fetch_pitches(session_token: str, since: str | None = None) -> list[dict]:
    """Fetch new pitches from the Worker. Raises PermissionError on auth/subscription failure."""
    path = "/pitches/sync"
    if since:
        path += f"?since={urllib.parse.quote(since)}"
    data, status = _request("GET", path, token=session_token)
    if status == 401:
        raise PermissionError("session_expired")
    if status == 402:
        raise PermissionError("subscription_required")
    if status != 200:
        raise RuntimeError(data.get("error", "sync failed"))
    return data.get("pitches", [])


def _cloud_pitch_to_text_and_inp(cp: dict) -> tuple[str, dict]:
    """Convert a StoredPitch dict from the Worker into (text, inp) for local scoring."""
    parts = [
        cp.get("one_liner", ""),
        f"Stage: {cp.get('stage', '')}",
        f"Traction: {cp.get('traction', '')}",
        f"Team: {cp.get('team', '')}",
        f"Market: {cp.get('market', '')}",
        f"Business model: {cp.get('business_model', '')}",
        f"Competition: {cp.get('competition', '')}",
    ]
    for key, label in (("summary", "Summary"), ("monetization", "Monetization"), ("location", "Location")):
        if cp.get(key):
            parts.append(f"{label}: {cp[key]}")
    if cp.get("raise"):
        parts.append(f"Raising: ${cp['raise']:,}")
    if cp.get("pre_revenue") == "no" and cp.get("revenue"):
        parts.append(f"Revenue: ${cp['revenue']:,}")
    text = "\n".join(p for p in parts if p.strip())
    inp = {k: cp.get(k, "") for k in (
        "one_liner", "stage", "traction", "team",
        "market", "business_model", "competition", "summary",
    )}
    if cp.get("raise"):
        inp["raising"] = f"${cp['raise']:,}"
    return text, inp


def score_and_save_cloud_pitches(
    cloud_pitches: list[dict],
    bundle,
    index_vecs,
    examples,
    config: dict,
) -> int:
    """
    Score a list of cloud pitches locally and persist them to history.
    Updates config['last_synced_at'] and saves config.
    Returns the number of pitches successfully added.
    """
    from superpitch.core import evaluate_pitch
    from superpitch.local import explain, is_available
    from superpitch.state import save_config, save_history_entry

    ai_ready = is_available()
    added = 0
    for cp in cloud_pitches:
        text, inp = _cloud_pitch_to_text_and_inp(cp)
        try:
            result = evaluate_pitch(bundle, text, index_vecs=index_vecs, examples=examples, inp=inp)
            ai = explain(text, result) if ai_ready else None
            save_history_entry(
                text, inp, result,
                source_label=f"web:{cp.get('founder_name', '')}|{cp.get('founder_email', 'unknown')}",
                cloud_pitch_id=cp.get("id"),
                ai_explanation=ai.get("explanation") if ai else None,
                ai_questions=ai.get("questions") if ai else None,
            )
            added += 1
        except Exception as exc:
            from superpitch.app_paths import log_sync_error
            log_sync_error(f"score_cloud_pitch:{cp.get('id', '?')}", exc)

    config["last_synced_at"] = datetime.now().isoformat()
    save_config(config)
    return added


def push_decision(session_token: str, pitch_id: str, decision: str) -> bool:
    """Push an approve/reject/review decision to the Worker. Silent on failure."""
    data, status = _request(
        "POST", "/decisions",
        body={"pitch_id": pitch_id, "decision": decision},
        token=session_token,
    )
    return status == 200 and bool(data.get("saved"))


def contribute_decision(
    session_token: str,
    text: str,
    decision: str,
    reason: str = "",
    explanation: str = "",
    questions: list | None = None,
) -> bool:
    """Opt-in: send an anonymized pitch+decision to the Worker for base model training."""
    data, status = _request(
        "POST", "/contribute",
        body={
            "text": text[:10000],
            "decision": decision,
            "reason": reason,
            "explanation": explanation,
            "questions": questions or [],
        },
        token=session_token,
    )
    return status == 200 and bool(data.get("saved"))


# ── Background poller ──────────────────────────────────────────────────────────

class SyncPoller:
    """
    Polls for new cloud pitches every POLL_INTERVAL seconds in a daemon thread.
    Never touches the terminal — calls on_synced(count) when new pitches arrive.
    """

    def __init__(self, config: dict, bundle, index_vecs, examples,
                 on_synced: Callable[[int], None] | None = None):
        self._config     = config
        self._bundle     = bundle
        self._index_vecs = index_vecs
        self._examples   = examples
        self._on_synced  = on_synced
        self._stop       = threading.Event()
        self._thread     = threading.Thread(target=self._run, daemon=True)

    def start(self) -> None:
        self._thread.start()

    def stop(self) -> None:
        self._stop.set()

    def _run(self) -> None:
        # Stagger first poll so it doesn't double up with startup sync
        if self._stop.wait(POLL_INTERVAL):
            return
        while not self._stop.is_set():
            self._poll()
            if self._stop.wait(POLL_INTERVAL):
                break

    def _poll(self) -> None:
        if not self._config.get("signed_in") or not self._config.get("session_token"):
            return
        try:
            since = self._config.get("last_synced_at") or None
            cloud_pitches = fetch_pitches(self._config["session_token"], since=since)
            if not cloud_pitches:
                from superpitch.state import save_config
                self._config["last_synced_at"] = datetime.now().isoformat()
                save_config(self._config)
                return
            added = score_and_save_cloud_pitches(
                cloud_pitches, self._bundle, self._index_vecs, self._examples, self._config,
            )
            if added and self._on_synced:
                self._on_synced(added)
        except Exception as exc:
            from superpitch.app_paths import log_sync_error
            log_sync_error("background_poll", exc)
