"""Cloud sync — email queue, evidence packets, decisions."""

from __future__ import annotations

import email as _email_lib
import email.policy as _email_policy
import base64
import json
import os
import re
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

from superpitch.app_paths import log_sync_error

WORKER_URL = os.environ.get("SUPERPITCH_WORKER_URL", "https://superpitch.eyoel-lundberg.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
_AUTH_TIMEOUT  = 120


class SessionExpiredError(PermissionError):
    pass


class SubscriptionRequiredError(PermissionError):
    pass


def _request(
    method: str,
    path: str,
    *,
    body: dict | None = None,
    token: str | None = None,
    timeout: int = 15,
) -> 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 ──────────────────────────────────────────────────────────────────────

def start_cli_auth() -> str:
    cli_token = f"sp-{secrets.token_urlsafe(20).lower()}"
    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:
    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


def fetch_api_key(session_token: str) -> str | None:
    """Fetch the platform Anthropic API key from the Worker (gated by session + subscription)."""
    data, status = _request("GET", "/api-key", token=session_token)
    if status == 200:
        return data.get("api_key")
    return None


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

def check_handle(slug: str) -> dict:
    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:
    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:
    data, status = _request("POST", "/thesis", body={"thesis": thesis}, token=session_token)
    return status == 200 and bool(data.get("saved"))


# ── Receive address setup ─────────────────────────────────────────────────────

def get_receive_address(session_token: str) -> str | None:
    """Return the full receive address (e.g. abc123@superpitch.dev) or None."""
    data, status = _request("GET", "/setup", token=session_token)
    if status == 200:
        return data.get("receive_address")
    return None


def setup_receive_address(session_token: str) -> str | None:
    """Force-create a new receive address. Returns full address or None."""
    data, status = _request("POST", "/setup", body={}, token=session_token)
    if status == 200:
        return data.get("receive_address")
    return None


# ── Email queue ───────────────────────────────────────────────────────────────

def fetch_raw_emails(session_token: str) -> list[dict]:
    """Fetch unprocessed raw emails from the Worker queue."""
    data, status = _request("GET", "/email/queue", token=session_token)
    if status == 401:
        raise SessionExpiredError()
    if status == 402:
        raise SubscriptionRequiredError()
    if status != 200:
        raise RuntimeError(data.get("error", "queue fetch failed"))
    return data.get("emails", [])


def submit_evidence_packet(
    session_token: str,
    email_id: str,
    packet: dict | None,
    action: str = "pitch",
    auto_reply: bool = True,
) -> bool:
    """Mark a raw email as processed and optionally store its evidence packet."""
    data, status = _request(
        "POST", "/email/processed",
        body={"email_id": email_id, "packet": packet, "action": action, "auto_reply": auto_reply},
        token=session_token,
    )
    return status == 200 and bool(data.get("saved"))


# ── MIME parsing ──────────────────────────────────────────────────────────────

def _parse_mime(raw_entry: dict) -> dict:
    """
    Parse a raw email KV entry into a structured dict with body_text and attachments.
    The KV entry contains the raw MIME string in 'mime'.
    """
    mime_str = raw_entry.get("mime", "")
    result = {
        "id":          raw_entry.get("id", ""),
        "from":        raw_entry.get("from", ""),
        "subject":     raw_entry.get("subject", ""),
        "received_at": raw_entry.get("received_at", ""),
        "body_text":   "",
        "attachments": [],
    }

    if not mime_str:
        return result

    try:
        msg = _email_lib.message_from_string(mime_str, policy=_email_policy.default)
        result["from"] = result["from"] or str(msg.get("from", ""))
        result["subject"] = result["subject"] or str(msg.get("subject", ""))

        if msg.is_multipart():
            for part in msg.walk():
                ct = part.get_content_type()
                disp = str(part.get("Content-Disposition", ""))
                fn = part.get_filename()

                if ct == "text/plain" and "attachment" not in disp:
                    try:
                        result["body_text"] += part.get_content() or ""
                    except Exception:
                        pass
                elif fn or "attachment" in disp:
                    try:
                        payload = part.get_payload(decode=True)
                        if payload:
                            result["attachments"].append({
                                "filename":     fn or "attachment",
                                "content_type": ct,
                                "data":         base64.b64encode(payload).decode(),
                            })
                    except Exception:
                        pass
        else:
            try:
                result["body_text"] = msg.get_content() or ""
            except Exception:
                result["body_text"] = str(msg.get_payload(decode=True) or "")

    except Exception:
        pass

    return result


# ── Reply detection ───────────────────────────────────────────────────────────

_REF_RE = re.compile(r'superpitch-ref:([a-zA-Z0-9_-]+)')


def _extract_thread_ref(body_text: str) -> str | None:
    """Extract superpitch thread reference ID from email body. Returns email_id or None."""
    m = _REF_RE.search(body_text)
    return m.group(1) if m else None


def _process_reply(config: dict, reply_email: dict, original_email_id: str) -> bool:
    """Process a founder's reply to follow-up questions. Returns True if successfully updated."""
    from superpitch.process import update_evidence_from_reply
    from superpitch.state import load_history, normalize_history_item, save_history

    api_key = config.get("anthropic_api_key", "")
    thesis  = config.get("thesis", "")

    items = load_history()
    idx   = next((i for i, it in enumerate(items) if it.get("raw_email_id") == original_email_id), None)
    if idx is None:
        return False

    original = items[idx]
    updated  = update_evidence_from_reply(original, reply_email, thesis, api_key)
    if updated is None:
        return False

    prior_replies = original.get("replies") or []
    reply_body    = reply_email.get("body_text", "").strip()
    items[idx] = normalize_history_item({
        **original,
        **updated,
        "timestamp":    original.get("timestamp", ""),
        "raw_email_id": original_email_id,
        "source_label": original.get("source_label", ""),
        "replies":      prior_replies + ([reply_body] if reply_body else []),
    })
    save_history(items)

    # Send another round only if questions remain and we haven't hit the limit
    still_open  = bool(updated.get("questions"))
    round_count = updated.get("reply_count", 0)
    if still_open and round_count < 2:
        submit_evidence_packet(session_token, email_id, updated, action="pitch", auto_reply=True)
    else:
        submit_evidence_packet(session_token, email_id, None, action="reply_processed")

    return True


# ── Full processing pipeline ──────────────────────────────────────────────────

def process_email_queue(
    config: dict,
    on_processed: Callable[[int], None] | None = None,
) -> int:
    """
    Fetch raw emails, classify, extract evidence packets, save locally, submit back.
    Returns the number of pitches added to history.
    """
    from superpitch.process import classify_email, extract_evidence_packet
    from superpitch.state import save_evidence_entry

    session_token = config.get("session_token", "")
    api_key       = config.get("anthropic_api_key", "")
    thesis        = config.get("thesis", "")

    if not session_token:
        return 0
    if not api_key:
        api_key = fetch_api_key(session_token) or ""
        if api_key:
            from superpitch.state import save_config
            config["anthropic_api_key"] = api_key
            save_config(config)
        else:
            return 0

    try:
        raw_emails = fetch_raw_emails(session_token)
    except (SessionExpiredError, SubscriptionRequiredError):
        raise  # let caller handle auth/billing errors
    except Exception as exc:
        log_sync_error("fetch_queue", exc)
        return 0

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

    added = 0
    for raw_entry in raw_emails:
        email_id = raw_entry.get("id") or raw_entry.get("_kv_key", "").split(":")[-1]
        if not email_id:
            continue

        parsed = _parse_mime(raw_entry)

        # Check if this is a founder's reply to follow-up questions
        thread_ref = _extract_thread_ref(parsed.get("body_text", ""))
        if thread_ref:
            _process_reply(config, parsed, thread_ref)  # handles submit internally
            added += 1
            continue

        try:
            label = classify_email(parsed, api_key)
        except Exception as exc:
            log_sync_error("classify", exc)
            label = "unsure"

        if label == "noise":
            submit_evidence_packet(session_token, email_id, None, action="noise")
            continue

        # pitch or unsure — extract
        try:
            packet = extract_evidence_packet(parsed, thesis, api_key)
        except Exception as exc:
            log_sync_error("extract", exc)
            packet = None

        if packet is None:
            # Failed extraction — tag as unsure so it surfaces for manual review
            packet = {
                "company":      parsed.get("subject") or "Unknown",
                "one_liner":    "Could not extract — review manually",
                "action":       "REVIEW",
                "confidence":   0.0,
                "action_reason": "Extraction failed",
                "missing":      ["all fields"],
                "questions":    [],
                "research":     [],
                "unsure":       label == "unsure",
            }

        if label == "unsure":
            packet["unsure"] = True
            packet["action"] = "REVIEW"

        save_evidence_entry(packet, email_id, source_label=f"email:{parsed.get('from', '')}")
        submit_evidence_packet(session_token, email_id, packet, action="pitch",
                               auto_reply=config.get("auto_reply", True))
        added += 1

    if added and on_processed:
        on_processed(added)

    return added


# ── Decisions ─────────────────────────────────────────────────────────────────

def push_decision(session_token: str, pitch_id: str, decision: str) -> bool:
    data, status = _request(
        "POST", "/decisions",
        body={"pitch_id": pitch_id, "decision": decision},
        token=session_token,
    )
    return status == 200 and bool(data.get("saved"))


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

class SyncPoller:
    """
    Polls for new emails every POLL_INTERVAL seconds in a daemon thread.
    Calls on_synced(count) when new pitches are added.
    """

    def __init__(self, config: dict, on_synced: Callable[[int], None] | None = None):
        self._config    = config
        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:
        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
        if not self._config.get("anthropic_api_key"):
            return
        try:
            process_email_queue(self._config, on_processed=self._on_synced)
            from superpitch.state import save_config
            save_config(self._config)  # persist last_synced_at
        except Exception as exc:
            log_sync_error("background_poll", exc)
