"""Terminal UI for Superpitch."""

from __future__ import annotations

import shutil
import sys
from datetime import datetime

try:
    import termios
    import tty
except ImportError:
    termios = None
    tty = None

from superpitch.app_paths import log_sync_error
from superpitch.state import (
    build_ranked_items,
    history_bucket,
    load_config,
    load_history,
    mark_pitch_opened,
    profile_status_line,
    save_config,
    update_history_item,
)
from superpitch.updates import run_upgrade

try:
    from superpitch.sync import (
        SessionExpiredError,
        SubscriptionRequiredError,
        SyncPoller,
        check_handle,
        claim_handle,
        get_receive_address,
        poll_cli_auth,
        process_email_queue,
        push_decision,
        set_thesis,
        start_cli_auth,
    )
    _SYNC_AVAILABLE = True
except ImportError:
    _SYNC_AVAILABLE = False

CURRENT_ONBOARDING_VERSION = 3

_IS_TTY = bool(
    sys.stdin.isatty()
    and sys.stdout.isatty()
    and termios is not None
    and tty is not None
)

_KEY_TO_DECISION = {
    "a": "approve", "1": "approve",
    "r": "reject",  "2": "reject",
    "v": "review",  "3": "review",
}
_DECISION_LABELS = {"approve": "approved", "reject": "rejected", "review": "flagged"}

# ── Colors ─────────────────────────────────────────────────────────────────────
_R        = "\033[0m"
_B        = "\033[1m"
_D        = "\033[2m"
_GREEN    = "\033[92m"
_RED      = "\033[91m"
_YELLOW   = "\033[93m"
_CYAN     = "\033[96m"
_AMBER    = "\033[38;2;226;166;70m"
_AMBER_BG = "\033[48;2;226;166;70m"
_FG_DARK  = "\033[38;2;15;15;15m"

_ACTION_COLOR = {
    "ESCALATE": _GREEN,
    "REVIEW":   _YELLOW,
    "PASS":     _RED,
}

def _ac(action: str) -> str:
    return _ACTION_COLOR.get(action.upper(), "")

def _dc(decision: str) -> str:
    return {"approve": _GREEN, "reject": _RED, "review": _YELLOW}.get(decision, "")


# ── Terminal helpers ───────────────────────────────────────────────────────────

def _tw() -> int:
    return max(60, shutil.get_terminal_size((80, 24)).columns - 2)

def _div() -> str:
    return "  " + "─" * _tw()

def _clear() -> None:
    if _IS_TTY:
        sys.stdout.write("\033[H\033[2J\033[3J")
        sys.stdout.flush()

def _vlen(s: str) -> int:
    n, i = 0, 0
    while i < len(s):
        if s[i] == "\033" and i + 1 < len(s) and s[i + 1] == "[":
            i += 2
            while i < len(s) and s[i] != "m":
                i += 1
            i += 1
        else:
            n += 1
            i += 1
    return n

def _wrap(text: str, width: int) -> list[str]:
    lines: list[str] = []
    line: list[str] = []
    cur = 0
    for word in text.split():
        need = len(word) + (1 if line else 0)
        if cur + need > width and line:
            lines.append(" ".join(line))
            line, cur = [word], len(word)
        else:
            line.append(word)
            cur += need
    if line:
        lines.append(" ".join(line))
    return lines or [""]

def _render_split(left_lines: list[str], right_lines: list[str], left_w: int) -> None:
    n = max(len(left_lines), len(right_lines))
    for i in range(n):
        l = left_lines[i] if i < len(left_lines) else ""
        r = right_lines[i] if i < len(right_lines) else ""
        pad = " " * max(0, left_w - _vlen(l))
        print(f"  {l}{pad}  {_D}│{_R}  {r}")

def _print_header(config: dict | None = None) -> None:
    from superpitch import __version__
    line1 = f"  {_AMBER}{_B}Superpitch{_R}  {_D}v{__version__}{_R}"
    line2 = f"  {_D}Iris  ·  pitch screening agent{_R}"
    if config and config.get("user_email"):
        addr = config.get("receive_address", "")
        line3 = f"  {_D}{config['user_email']}" + (f"  ·  {addr}" if addr else "") + f"{_R}"
    else:
        line3 = f"  {_D}not signed in{_R}"
    print(line1)
    print(line2)
    print(line3)

def _st(view="top", selected=0, notice="", quit=False, reload=False):
    return {"view": view, "selected": selected, "notice": notice, "quit": quit, "reload": reload}

def _wait(msg="", reload=False):
    if msg:
        print(f"\n  {msg}")
    input("\n  Press enter to continue...")
    return _st(reload=reload)

def _rel_time(ts: str) -> str:
    if not ts:
        return ""
    try:
        delta = datetime.now() - datetime.fromisoformat(ts)
        if delta.days == 0:
            h = delta.seconds // 3600
            return "just now" if h == 0 else f"{h}h ago"
        return "yesterday" if delta.days == 1 else f"{delta.days}d ago"
    except (ValueError, TypeError):
        return ""


# ── Input ──────────────────────────────────────────────────────────────────────

def read_keypress():
    if not _IS_TTY:
        return input().strip().lower()
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        c = sys.stdin.read(1)
        if c == "\x03":
            raise KeyboardInterrupt
        if c in ("\r", "\n"):
            return "ENTER"
        if c == "\x1b":
            c2 = sys.stdin.read(1)
            if c2 == "[":
                c3 = sys.stdin.read(1)
                if c3 == "A":
                    return "UP"
                if c3 == "B":
                    return "DOWN"
            return "ESC"
        return c.lower()
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)


# ── One-off file output ────────────────────────────────────────────────────────

def print_packet_summary(packet: dict) -> None:
    """Print a compact evidence packet summary for file-mode output."""
    print()
    print(_div())
    action = packet.get("action", "REVIEW")
    conf   = float(packet.get("confidence", 0))
    print(f"  {_ac(action)}{_B}{action}{_R}  confidence {conf:.0%}  ·  {packet.get('action_reason', '')}")
    print()
    if packet.get("company"):
        print(f"  {_B}{packet['company']}{_R}  {packet.get('one_liner', '')}")
    if packet.get("stage") or packet.get("sector"):
        print(f"  {_D}{packet.get('stage', '')}  {packet.get('sector', '')}{_R}")
    if packet.get("ask"):
        print(f"  Raising  {packet['ask']}")
    if packet.get("traction"):
        print(f"  Traction  {packet['traction']}")
    if packet.get("thesis_fit"):
        print(f"  Thesis  {packet['thesis_fit']}")
    if packet.get("missing"):
        print(f"\n  {_D}Missing: {', '.join(packet['missing'][:3])}{_R}")
    if packet.get("questions"):
        print(f"\n  Questions before the call:")
        for q in packet["questions"][:3]:
            print(f"  · {q}")
    print(_div())


# ── Help ───────────────────────────────────────────────────────────────────────

def print_help() -> None:
    print()
    print(_div())
    print("""
  KEYS
  ↑↓             move selection
  enter          open pitch
  a / r / v      approve / reject / flag for review
  u              add a pitch from file
  /              command mode
  ?              this help
  q              quit

  COMMANDS
  /open N        open pitch N
  /approve N     mark approved
  /pass N        mark passed  (same as /reject N)
  /reject N      mark rejected
  /review N      flag for review
  /import <file> import a PDF or text file
  /sync          process new emails now
  /thesis        update your investor thesis
  /autoreply     toggle auto-send follow-up questions to founders (on/off)
  /profile       show profile and receive address
  /signin        sign in with Google
  /signout       sign out
  /subscription  manage billing
  /upgrade       upgrade via pipx
  /help          this help
  /quit          exit

  FEEDBACK
  github.com/eyoellundberg/superpitch/issues""")
    print(_div())


# ── Onboarding ────────────────────────────────────────────────────────────────

def onboarding_flow(config: dict) -> dict:
    if (
        config.get("onboarding_complete")
        and int(config.get("onboarding_version", 0)) >= CURRENT_ONBOARDING_VERSION
        and config.get("signed_in")
        and config.get("anthropic_api_key")
    ):
        return config

    _clear()
    _print_header()
    print(_div())
    print(f"\n  Welcome to Superpitch\n")
    print("  Forward pitch emails to a private address.")
    print("  Every pitch gets screened, researched, and ranked automatically.")
    print()

    # Step 1: sign in
    if not config.get("signed_in"):
        print("  Sign in to get your receive address.\n")
        print("  Press enter to open your browser, or 's' to skip for now: ", end="", flush=True)
        choice = read_keypress()
        print()
        if choice not in ("s",):
            signin_flow(config)
            config = load_config()

    # Step 2: fetch API key automatically
    if config.get("signed_in") and not config.get("anthropic_api_key"):
        if _SYNC_AVAILABLE and config.get("session_token"):
            try:
                from superpitch.sync import fetch_api_key
                key = fetch_api_key(config["session_token"])
                if key:
                    config["anthropic_api_key"] = key
            except Exception:
                pass

    # Step 3: thesis
    if config.get("signed_in") and not config.get("thesis"):
        print()
        thesis = input("  Your thesis (what you invest in): ").strip()
        if thesis:
            config["thesis"] = thesis
            if _SYNC_AVAILABLE and config.get("session_token"):
                try:
                    set_thesis(config["session_token"], thesis)
                except Exception:
                    pass

    config["onboarding_complete"] = True
    config["onboarding_version"]  = CURRENT_ONBOARDING_VERSION
    save_config(config)

    # Show receive address
    if config.get("signed_in") and config.get("session_token"):
        addr = config.get("receive_address", "")
        if not addr and _SYNC_AVAILABLE:
            try:
                addr = get_receive_address(config["session_token"]) or ""
                if addr:
                    config["receive_address"] = addr
                    save_config(config)
            except Exception:
                pass
        if addr:
            print()
            print(_div())
            print(f"\n  You're set up.\n")
            print(f"  Forward pitch emails to:  {_AMBER}{_B}{addr}{_R}")
            print()
            print(f"  {_D}Set up a forwarding rule in Gmail / Outlook to copy")
            print(f"  pitch emails to that address. Pitches appear here automatically.{_R}")
            print()
            input("  Press enter to open your inbox...")

    return config


# ── Sign-in ────────────────────────────────────────────────────────────────────

def signin_flow(config: dict) -> None:
    if not _SYNC_AVAILABLE:
        _wait("Sync module unavailable.")
        return

    _clear()
    _print_header()
    print(_div())
    print("  Sign in with Google\n")
    print("  Opening your browser...")

    try:
        cli_token = start_cli_auth()
    except Exception as exc:
        _wait(f"Could not start sign-in: {exc}")
        return

    print("  Waiting for browser", end="", flush=True)

    def _tick(elapsed: int) -> None:
        sys.stdout.write(f"\r  Waiting for browser  {elapsed}s  ")
        sys.stdout.flush()

    result = poll_cli_auth(cli_token, on_tick=_tick)
    print()
    if result is None:
        _wait("Sign-in timed out. Try /signin again.")
        return

    config.update(
        signed_in=True,
        session_token=result.get("session_token", ""),
        user_email=result.get("email", ""),
        user_slug=result.get("slug", ""),
    )
    print(f"\n  Signed in as {config['user_email']}")

    # Handle claiming
    if not config.get("user_slug") and _SYNC_AVAILABLE:
        print()
        print("  Pick a handle for your account:")
        while True:
            raw_slug = input("  Handle: ").strip().lower().replace(" ", "-")
            if not raw_slug:
                continue
            check = check_handle(raw_slug)
            if not check.get("available"):
                print(f"  {raw_slug} is not available ({check.get('reason', 'taken')}). Try another.")
                continue
            name = config["user_email"].split("@")[0]
            claimed = claim_handle(config["session_token"], raw_slug, config["user_email"], name)
            if claimed.get("claimed") or claimed.get("slug"):
                config["user_slug"] = claimed.get("slug", raw_slug)
                break
            elif claimed.get("error") == "already claimed":
                config["user_slug"] = claimed.get("slug", "")
                break
            else:
                print(f"  Error: {claimed.get('error', 'try again')}")

    # Receive address
    if _SYNC_AVAILABLE and config.get("session_token") and not config.get("receive_address"):
        try:
            addr = get_receive_address(config["session_token"]) or ""
            if addr:
                config["receive_address"] = addr
        except Exception:
            pass

    save_config(config)


def signout_flow(config: dict) -> dict:
    config.update(
        signed_in=False, session_token="",
        user_email="", user_slug="", last_synced_at="",
    )
    save_config(config)
    _wait("Signed out.")
    return config


# ── Inbox ──────────────────────────────────────────────────────────────────────

_DONE_SYM = {"approve": "✓", "reject": "✗", "review": "~"}


def print_inbox(config: dict, selected: int = 0, view: str = "top", notice: str = "", items=None):
    if items is None:
        items = load_history()
    ranked_all = build_ranked_items(items)
    tw         = _tw()
    _, term_rows = shutil.get_terminal_size((80, 24))
    today      = datetime.now().date().isoformat()

    queue = [(ri, item) for ri, item in ranked_all if history_bucket(item) != "decided"]
    done  = [(ri, item) for ri, item in ranked_all if history_bucket(item) == "decided"]

    if view == "today":
        queue = [e for e in queue if e[1].get("timestamp", "")[:10] == today]
        done  = [e for e in done  if e[1].get("timestamp", "")[:10] == today]

    done_cap    = None if view == "all" else 5
    done_shown  = done if done_cap is None else done[:done_cap]
    done_hidden = len(done) - len(done_shown)

    displayed = queue + done_shown
    selected  = max(0, min(selected, len(displayed) - 1)) if displayed else 0

    if _IS_TTY:
        sys.stdout.write("\033[H")
        sys.stdout.flush()

    # Column widths
    DATE_W   = 10
    ACTION_W = 9   # ESCALATE
    CONF_W   = 5   # 0.81
    STATUS_W = 9
    overhead = 2 + 2 + 4 * 2 + STATUS_W
    COMP_W   = max(16, tw - DATE_W - ACTION_W - CONF_W - overhead)

    solid = "  " + "─" * (tw - 2)
    dots  = "  " + "·" * (tw - 2)
    row_inner = DATE_W + 2 + COMP_W + 2 + ACTION_W + 2 + CONF_W + 2 + STATUS_W

    out: list[str] = []

    # Header
    from superpitch import __version__
    last      = config.get("last_synced_at", "")
    right_str = f"{_D}{_rel_time(last)}{_R}" if last else f"{_D}Local{_R}"
    hdr_l     = f"  {_AMBER}{_B}Superpitch{_R}  {_D}v{__version__}{_R}"
    gap       = tw + 2 - _vlen(hdr_l) - _vlen(right_str)
    out.append(f"{hdr_l}{' ' * max(2, gap)}{right_str}")

    if config.get("user_email"):
        addr     = config.get("receive_address", "")
        sub_l    = f"  {_D}Iris  ·  {config['user_email']}" + (f"  ·  {addr}" if addr else "") + f"{_R}"
        out.append(sub_l)
    else:
        out.append(f"  {_D}Iris  ·  not signed in{_R}")

    if notice:
        out.append(f"  {_B}{notice}{_R}")
    out.append(solid)

    view_label = {"today": "TODAY", "all": "ALL PITCHES"}.get(view, "Inbox")
    out.append(f"\n  {_AMBER}{view_label}{_R}")
    out.append(dots)

    out.append(
        f"  "
        f"  {_AMBER}{'DATE':<{DATE_W}}{_R}"
        f"  {_AMBER}{'COMPANY':<{COMP_W}}{_R}"
        f"  {_AMBER}{'ACTION':<{ACTION_W}}{_R}"
        f"  {_AMBER}{'CONF':<{CONF_W}}{_R}"
        f"  {_AMBER}{'STATUS':>{STATUS_W}}{_R}"
    )
    out.append(dots)

    if not displayed:
        out.append("")
        addr = config.get("receive_address", "")
        if addr:
            out.append(f"  {_D}Inbox empty.  Forward pitch emails to {addr}{_R}")
        elif config.get("signed_in"):
            out.append(f"  {_D}Inbox empty.  Run /profile to see your receive address.{_R}")
        else:
            out.append(f"  {_D}Inbox empty.  Use /signin to get started.{_R}")
    else:
        for i, (ri, item) in enumerate(displayed):
            is_sel = i == selected
            dec    = item.get("decision_state") or ""
            is_new = not item.get("opened_at") and not dec

            ts      = item.get("timestamp", "")[:10].replace("-", ":")
            date_v  = ts[:DATE_W].ljust(DATE_W)
            company = (item.get("company") or item.get("one_liner") or "")[:COMP_W]
            comp_v  = company.ljust(COMP_W)
            action  = item.get("action", "PASS").upper()
            act_v   = action[:ACTION_W].ljust(ACTION_W)
            conf    = float(item.get("confidence", 0))
            conf_v  = f"{conf:.0%}".ljust(CONF_W)

            awaiting = item.get("awaiting_reply") and not dec
            if dec:
                sym        = _DONE_SYM.get(dec, "")
                stat_plain = {"approve": "Approved", "reject": "Rejected", "review": "Flagged"}.get(dec, dec)
                stat_plain = f"{sym} {stat_plain}"
            elif awaiting:
                stat_plain = "Awaiting"
            elif is_new:
                stat_plain = "New" + (" ?" if item.get("unsure") else "")
            else:
                stat_plain = "Opened"
            stat_v = stat_plain.rjust(STATUS_W)

            out.append("")

            if is_sel:
                inner  = f" {date_v}  {comp_v}  {act_v}  {conf_v}  {stat_v}"
                padded = inner.ljust(row_inner + 1)
                out.append(f"  {_AMBER_BG}{_FG_DARK}{padded}{_R}")
            else:
                act_col = f"{_ac(action)}{act_v}{_R}" if not dec else f"{_D}{act_v}{_R}"

                if dec:
                    stat_col = f"{_dc(dec)}{stat_v}{_R}"
                    out.append(f"    {_D}{date_v}  {comp_v}  {_R}{act_col}  {_D}{conf_v}  {_R}{stat_col}")
                elif awaiting:
                    stat_col = f"{_CYAN}{stat_v}{_R}"
                    out.append(f"    {date_v}  {comp_v}  {act_col}  {conf_v}  {stat_col}")
                elif is_new:
                    out.append(f"    {date_v}  {comp_v}  {act_col}  {conf_v}  {stat_v}")
                else:
                    out.append(f"    {_D}{date_v}  {comp_v}  {_R}{act_col}  {_D}{conf_v}  {stat_v}{_R}")

    if done_hidden:
        out.append(f"\n  {_D}+ {done_hidden} more  ·  /all{_R}")

    for line in out:
        print(line)

    if _IS_TTY:
        footer_l = f"  {_D}↑↓ move  ·  ↵ open{_R}"
        footer_r = f"{_D}a/r/v decide  ·  u add  ·  / commands  ·  q quit{_R}"
        gap_f    = tw + 2 - _vlen(footer_l) - _vlen(footer_r)
        footer_line = f"{footer_l}{' ' * max(4, gap_f)}{footer_r}"
        total_content_lines = sum(1 + s.count("\n") for s in out)
        padding = max(0, term_rows - total_content_lines - 2)
        print("\n" * padding, end="")
        print(solid)
        print(footer_line, end="", flush=True)
        sys.stdout.write("\033[J")
        sys.stdout.flush()
    else:
        print(f"\n  {_D}/help for commands{_R}")

    return displayed, selected


# ── Detail view ────────────────────────────────────────────────────────────────

def pitch_detail_flow(ranked: list, selected: int, config: dict | None = None) -> None:
    if not ranked:
        return
    real_index, item = ranked[selected]
    item = mark_pitch_opened(real_index, item)

    while True:
        _clear()
        tw     = _tw()
        left_w = max(30, min(42, tw // 3))
        right_w = tw - left_w - 6

        action = item.get("action", "PASS").upper()
        conf   = float(item.get("confidence", 0))

        # ── Left column: judgment + metadata ──────────────────────────────────
        left: list[str] = []

        # Action + confidence + decision state
        dec_str = ""
        if item.get("decision_state"):
            dec_str = f"  {_dc(item['decision_state'])}{item['decision_state'].upper()}{_R}"
        left.append(f"{_ac(action)}{_B}{action}{_R}  {_D}{conf:.0%}{_R}{dec_str}")

        # Company + one-liner
        company  = item.get("company") or item.get("one_liner") or "(unknown)"
        one_liner = item.get("one_liner", "")
        left.append(f"{_B}{company[:left_w]}{_R}")
        if one_liner and one_liner != company:
            for ln in _wrap(one_liner, left_w):
                left.append(f"{_D}{ln}{_R}")
        left.append("")

        # Stage · sector · geo
        meta_parts = [p for p in [item.get("stage"), item.get("sector"), item.get("geography")] if p]
        if meta_parts:
            left.append(f"{_D}{('  ·  '.join(meta_parts))[:left_w]}{_R}")

        if item.get("ask"):
            left.append(f"Raising  {item['ask'][:left_w - 8]}")

        if item.get("thesis_fit"):
            left.append("")
            left.append(f"{_D}thesis{_R}")
            for ln in _wrap(item["thesis_fit"], left_w - 2):
                left.append(f"  {ln}")

        # Unsure tag
        if item.get("unsure"):
            left.append("")
            left.append(f"{_YELLOW}? uncertain classification{_R}")

        # Reply status + thread
        replies   = item.get("replies") or []
        questions = item.get("questions") or []
        if replies or item.get("awaiting_reply"):
            left.append("")
            left.append(f"{_D}thread{_R}")
            if questions:
                left.append(f"  {_D}Iris asked:{_R}")
                for q in questions[:4]:
                    for ln in _wrap(f"· {q}", left_w - 4):
                        left.append(f"    {_D}{ln}{_R}")
            for reply_body in replies:
                left.append(f"  {_CYAN}Founder replied:{_R}")
                for ln in _wrap(reply_body[:600], left_w - 4):
                    left.append(f"    {ln}")
            if item.get("awaiting_reply"):
                left.append(f"  {_CYAN}↩ awaiting reply{_R}")
        elif item.get("reply_count"):
            left.append("")
            rc = item["reply_count"]
            left.append(f"{_D}↩ {rc} follow-up round{'s' if rc > 1 else ''} complete{_R}")

        # Missing info
        missing = item.get("missing") or []
        if missing:
            left.append("")
            left.append(f"{_D}missing{_R}")
            for m in missing[:4]:
                left.append(f"  {_D}· {m[:left_w - 4]}{_R}")

        # Questions (shown standalone only when no thread yet)
        questions = item.get("questions") or []
        if questions and not replies and not item.get("awaiting_reply"):
            left.append("")
            left.append(f"{_D}questions{_R}")
            for q in questions[:4]:
                for ln in _wrap(f"· {q}", left_w - 2):
                    left.append(f"  {_D}{ln}{_R}")

        # Action reason
        if item.get("action_reason"):
            left.append("")
            for ln in _wrap(item["action_reason"], left_w):
                left.append(f"{_D}{ln}{_R}")

        # Contact
        src = item.get("source_label", "")
        if src.startswith("email:"):
            contact = src[6:]
            if contact:
                left.append("")
                left.append(f"{_D}from  {contact[:left_w - 6]}{_R}")

        left.append("")
        left.append(f"{_GREEN}{_B}[a]{_R} approve")
        left.append(f"{_RED}{_B}[r]{_R} reject")
        left.append(f"{_YELLOW}{_B}[v]{_R} review")
        left.append(f"[b] back")

        # ── Right column: research + evidence ─────────────────────────────────
        right: list[str] = []

        if item.get("traction"):
            right.append(f"{_D}Traction{_R}")
            for ln in _wrap(item["traction"], right_w):
                right.append(f"  {ln}")
            right.append("")

        if item.get("team"):
            right.append(f"{_D}Team{_R}")
            for ln in _wrap(item["team"], right_w):
                right.append(f"  {ln}")
            right.append("")

        research = item.get("research") or []
        if research:
            right.append(f"{_D}Research{_R}")
            for r in research[:6]:
                finding = r.get("finding", "")
                source  = r.get("source", "")
                for ln in _wrap(f"· {finding}", right_w - 2):
                    right.append(f"  {ln}")
                if source:
                    src_short = source[:right_w - 4]
                    right.append(f"  {_D}{src_short}{_R}")
            right.append("")

        if not right:
            right.append(f"{_D}(no evidence extracted){_R}")

        # ── Render ─────────────────────────────────────────────────────────────
        print(_div())
        _render_split(left, right, left_w)
        print(_div())
        print("\n  > ", end="", flush=True)

        choice = read_keypress()
        if choice in {"b", "q", "ESC", ""}:
            return
        decision = _KEY_TO_DECISION.get(choice)
        if decision:
            print(f"\n  Reason (optional, enter to skip): ", end="", flush=True)
            reason = input().strip()
            item["decision_state"] = decision
            item["decided_at"]     = datetime.now().isoformat()
            item["decision_reason"] = reason
            update_history_item(real_index, item)
            if config:
                _post_decision_hooks(item, config)
            return


def _post_decision_hooks(item: dict, config: dict) -> None:
    session_token = config.get("session_token")
    cloud_id      = item.get("cloud_pitch_id")
    if session_token and _SYNC_AVAILABLE:
        try:
            if cloud_id:
                push_decision(session_token, cloud_id, item["decision_state"])
        except Exception as exc:
            log_sync_error("post_decision", exc)

    # Feed investor decision back to the Autoforge specialist for continuous retraining
    try:
        from specialist.ask import record  # type: ignore[import]
        features = {
            "stage":      item.get("stage", ""),
            "sector":     item.get("sector", ""),
            "geography":  item.get("geography", ""),
            "ask":        item.get("ask", ""),
            "traction":   item.get("traction", ""),
            "team":       item.get("team", ""),
            "thesis_fit": item.get("thesis_fit", ""),
            "missing":    ", ".join(item.get("missing") or []),
            "confidence": item.get("confidence", 0.0),
        }
        record(features, item.get("decision_state", ""))
    except ImportError:
        pass
    except Exception as exc:
        log_sync_error("specialist_record", exc)


# ── Profile ────────────────────────────────────────────────────────────────────

def profile_flow(config: dict) -> None:
    _clear()
    _print_header(config)
    print(_div())
    print(f"  {profile_status_line(config)}")
    print()

    addr = config.get("receive_address", "")
    if addr:
        print(f"  Receive address:  {_AMBER}{_B}{addr}{_R}")
        print(f"  {_D}Forward pitch emails here — they'll appear in your inbox automatically.{_R}")
    else:
        print(f"  {_D}No receive address — sign in to get one.{_R}")

    print()
    print(f"  Thesis:   {config.get('thesis') or '(none)'}")
    print(_div())
    input("\n  Press enter to go back...")


# ── Thesis ────────────────────────────────────────────────────────────────────

def thesis_flow(config: dict) -> dict:
    print()
    print(f"  Current thesis: {config.get('thesis') or '(none)'}")
    print()
    print("  New thesis (enter to keep current): ", end="")
    new_thesis = input().strip()
    if not new_thesis:
        return _st()
    config["thesis"] = new_thesis
    save_config(config)
    if config.get("signed_in") and _SYNC_AVAILABLE:
        try:
            set_thesis(config["session_token"], new_thesis)
            return _wait("Thesis updated and synced.")
        except Exception:
            pass
    return _wait("Thesis saved locally.")


# ── Import / add file ─────────────────────────────────────────────────────────

def import_file_flow(path_str: str, config: dict) -> dict:
    from pathlib import Path

    path = Path(path_str.strip().strip("'\"").replace("\\ ", " ")).expanduser()
    if not path.exists():
        return _wait("File not found.")

    api_key = config.get("anthropic_api_key", "")
    if not api_key:
        return _wait("No Anthropic API key configured. Use /profile to check your setup.")

    from superpitch.process import process_file
    from superpitch.state import save_evidence_entry

    print(f"\n  Processing {path.name}...")
    packet = process_file(str(path), config.get("thesis", ""), api_key)
    if not packet:
        return _wait("Could not extract evidence from that file.")

    print_packet_summary(packet)
    save_evidence_entry(packet, source_label=str(path))
    return _st(reload=True)


# ── Sync ──────────────────────────────────────────────────────────────────────

def _run_sync(config: dict) -> dict:
    if not _SYNC_AVAILABLE:
        return _wait("Sync module unavailable.")
    if not config.get("signed_in") or not config.get("session_token"):
        return _wait("Not signed in. Use /signin first.")
    if not config.get("anthropic_api_key"):
        return _wait("No Anthropic API key. Run /profile to check setup.")

    print("\n  Processing email queue...")
    try:
        added = process_email_queue(config)
        save_config(config)  # persist last_synced_at
        if added:
            return _wait(f"Processed {added} new pitch{'es' if added != 1 else ''}.", reload=True)
        return _wait("No new pitches.")
    except SubscriptionRequiredError:
        return _wait("No active subscription. Use /subscription to manage billing.")
    except (SessionExpiredError, PermissionError):
        config.update(signed_in=False, session_token="")
        save_config(config)
        return _wait("Session expired. Use /signin to reconnect.")
    except Exception as exc:
        return _wait(f"Sync failed: {exc}")


# ── Commands ──────────────────────────────────────────────────────────────────

def handle_command(command: str, ranked: list, config: dict) -> dict:
    if not command:
        return _st()
    if command in {"/quit", "/q", "q"}:
        return _st(quit=True)
    if command == "/help":
        print_help()
        return _wait()
    if command == "/profile":
        profile_flow(config)
        return _st()
    if command == "/thesis":
        return thesis_flow(config)
    if command == "/autoreply":
        current = config.get("auto_reply", True)
        config["auto_reply"] = not current
        save_config(config)
        state = "ON" if config["auto_reply"] else "OFF"
        return _wait(f"Auto-reply {state}. Superpitch {'will' if config['auto_reply'] else 'will not'} email founders with follow-up questions.")
    if command == "/signin":
        signin_flow(config)
        return _wait()
    if command == "/signout":
        signout_flow(config)
        return _st()
    if command == "/subscription":
        import webbrowser
        webbrowser.open("https://superpitch.dev/auth")
        return _wait("Opening superpitch.dev in your browser.")
    if command == "/sync":
        return _run_sync(config)
    if command in {"/today", "/all", "/top"}:
        return _st(view=command[1:])
    if command.startswith("/import"):
        parts = command.split(None, 1)
        if len(parts) < 2:
            return _wait("Usage: /import path/to/file.pdf")
        return import_file_flow(parts[1], config)
    if command == "u":
        print()
        path_raw = input("  Drop a file path here: ").strip()
        if path_raw:
            return import_file_flow(path_raw, config)
        return _st()
    if command == "/upgrade":
        print("\n  Running uv tool upgrade superpitch...")
        success, output = run_upgrade()
        if output:
            print(f"\n{output}")
        return _wait("Restart Superpitch to use the new version." if success else "Upgrade failed.")

    # Numeric commands
    for prefix, action in (
        ("/open ", "open"), ("/approve ", "approve"),
        ("/reject ", "reject"), ("/pass ", "reject"), ("/review ", "review"),
    ):
        if command.startswith(prefix):
            try:
                n = int(command.split()[1])
            except (IndexError, ValueError):
                return _wait(f"Use {prefix.strip()} N")
            if action == "open":
                pitch_detail_flow(ranked, n - 1, config=config)
                return _st(reload=True)
            if n < 1 or n > len(ranked):
                return _wait("Pitch not found.")
            ri, item = ranked[n - 1]
            item["decision_state"] = action
            item["decided_at"]     = datetime.now().isoformat()
            update_history_item(ri, item)
            if config:
                _post_decision_hooks(item, config)
            title = (item.get("company") or item.get("one_liner") or "pitch")[:48]
            return _st(notice=f"[{n}] {_DECISION_LABELS[action]}  ·  {title}", reload=True)

    return _wait("Unknown command. Type /help")


# ── Key handler ───────────────────────────────────────────────────────────────

def handle_keypress(key: str, ranked: list, selected: int, config: dict, view: str) -> dict:
    if key in ("UP", "DOWN"):
        n     = len(ranked)
        delta = -1 if key == "UP" else 1
        return _st(view=view, selected=max(0, min(n - 1, selected + delta)) if n else 0)
    if key == "ENTER":
        if ranked:
            pitch_detail_flow(ranked, selected, config=config)
        return _st(view=view, selected=selected, reload=True)
    if key in ("a", "r", "v"):
        if not ranked:
            return _st(view=view, selected=selected)
        decision = _KEY_TO_DECISION[key]
        ri, item = ranked[selected]
        item["decision_state"] = decision
        item["decided_at"]     = datetime.now().isoformat()
        update_history_item(ri, item)
        if config:
            _post_decision_hooks(item, config)
        title = (item.get("company") or item.get("one_liner") or "pitch")[:48]
        notice = f"{_DECISION_LABELS[decision]}  ·  {title}"
        return _st(view=view, selected=selected, notice=notice, reload=True)
    if key == "u":
        return handle_command("u", ranked, config)
    if key == "?":
        print_help()
        input("\n  Press enter to continue...")
        return _st(view=view, selected=selected)
    if key == "/":
        print(f"\n  {_D}approve N · reject N · pass N · review N · open N{_R}")
        print(f"  {_D}sync · import <file> · thesis · autoreply · profile · today · all · signin · signout · quit{_R}")
        raw = input("  /").strip()
        cmd = f"/{raw}" if raw and not raw.startswith("/") else raw
        result = handle_command(cmd, ranked, config)
        result["selected"] = 0 if result.get("view", view) != view else selected
        return result
    if key in ("q", "ESC"):
        return _st(quit=True)
    return _st(view=view, selected=selected)


# ── Main loop ──────────────────────────────────────────────────────────────────

def run_interactive_app(config: dict) -> None:
    if _IS_TTY:
        sys.stdout.write("\033[?1049h\033[H\033[2J\033[3J")
        sys.stdout.flush()
        sys.stdout.write("\033[H  Loading...")
        sys.stdout.flush()

    poller       = None
    sync_pending = 0

    def _on_synced(count: int) -> None:
        nonlocal sync_pending
        sync_pending += count

    try:
        config = onboarding_flow(config)

        def _start_poller():
            nonlocal poller
            if (
                _SYNC_AVAILABLE and poller is None
                and config.get("signed_in")
                and config.get("session_token")
                and config.get("anthropic_api_key")
            ):
                poller = SyncPoller(config, on_synced=_on_synced)
                poller.start()

        _start_poller()

        view, selected, notice = "top", 0, ""
        cached_items = None

        while True:
            if sync_pending and not notice:
                n            = sync_pending
                sync_pending = 0
                notice       = f"Processed {n} new pitch{'es' if n != 1 else ''}."
                cached_items = None

            if cached_items is None:
                cached_items = load_history()

            ranked, selected = print_inbox(
                config, selected=selected, view=view, notice=notice, items=cached_items,
            )
            notice = ""

            if _IS_TTY:
                key    = read_keypress()
                result = handle_keypress(key, ranked, selected, config, view)
            else:
                cmd    = input("\n  > ").strip().lstrip("/")
                result = handle_command(cmd, ranked, config)

            view     = result.get("view", "top")
            selected = result.get("selected", 0)
            notice   = result.get("notice", "")
            if result.get("reload"):
                cached_items = None
            if result.get("quit"):
                break

            _start_poller()

    finally:
        if poller:
            poller.stop()
        if _IS_TTY:
            sys.stdout.write("\033[?25h\033[?1049l")
            sys.stdout.flush()
