"""Read what the sign-in ceremony actually PUT ON THE SCREEN.

`claude setup-token` is a TUI. Its renderer places words with absolute cursor
moves rather than spaces, so the bytes it emits are terminal OPERATIONS, not
text: on 2026-08-29 a perfectly good token arrived as

    sk-ant-\x1b[10Gat01-w_C6…

Stripping the escapes yields `sk-ant-at01-…` — the sequence's own final byte ate
the token's `o` — and no amount of whitespace-joining puts it back. Three mints
failed as "no token captured" seconds after the client printed
"✓ Long-lived authentication token created successfully!".

So the transcript is REPLAYED onto a virtual screen and the result is read, which
is by definition what the operator saw. Only the handful of sequences this TUI
uses are honoured; everything else is skipped, and any byte that would move the
cursor off-screen is clamped rather than trusted.

Run directly:  ceremony.py extract <capture>          -> the setup-token, or nothing
               ceremony.py debrief <capture> <out>    -> last words; writes a
                                                         redacted transcript (0600)
"""

import os
import re
import sys

TOKEN = re.compile(r'sk-ant-oat\d{2}-[A-Za-z0-9_-]{40,}')
ANY_CRED = re.compile(r'sk-ant-[A-Za-z0-9]+-[A-Za-z0-9_-]+')
API_KEY = re.compile(r'sk-ant-api\d{2}-')
_CSI = re.compile(r'\x1b\[([0-9;?<>=]*)([@-~])')
_CHARSET = re.compile(r'\x1b[()][A-Z0-9]|\x1b[78=>]')
# A screen this TUI could never legitimately need; a corrupt stream must not make
# the renderer allocate without bound.
MAX_ROWS = 2000
MAX_COLS = 4000


def render(raw):
    """The transcript's final screen, as a list of lines."""
    rows, cur, col = [[]], 0, 0

    def put(ch):
        nonlocal col
        if cur >= MAX_ROWS or col >= MAX_COLS:
            return
        while len(rows) <= cur:
            rows.append([])
        row = rows[cur]
        while len(row) <= col:
            row.append(' ')
        row[col] = ch
        col += 1

    i, n = 0, len(raw)
    while i < n:
        ch = raw[i]
        if ch == '\x1b':
            if raw.startswith('\x1b]', i):                 # OSC … BEL | ST
                bel, st = raw.find('\x07', i), raw.find('\x1b\\', i)
                ends = [x for x in ((bel, 1), (st, 2)) if x[0] != -1]
                i = min(ends)[0] + min(ends)[1] if ends else n
                continue
            m = _CSI.match(raw, i)
            if m:
                params, fin = m.group(1), m.group(2)
                nums = [int(p) for p in params.split(';') if p.isdigit()]
                a = nums[0] if nums else 1
                if fin == 'G':
                    col = min(max(0, a - 1), MAX_COLS)
                elif fin == 'C':
                    col = min(col + a, MAX_COLS)
                elif fin == 'D':
                    col = max(0, col - a)
                elif fin == 'A':
                    cur = max(0, cur - a)
                elif fin == 'B':
                    cur = min(cur + a, MAX_ROWS)
                elif fin == 'H':
                    cur = min(max(0, a - 1), MAX_ROWS)
                    col = min(max(0, (nums[1] if len(nums) > 1 else 1) - 1), MAX_COLS)
                elif fin == 'K':
                    while len(rows) <= cur:
                        rows.append([])
                    if params.startswith('2'):
                        rows[cur] = []
                    elif not params or params == '0':
                        rows[cur] = rows[cur][:col]
                elif fin == 'J' and params.startswith('2'):
                    rows, cur, col = [[]], 0, 0
                i = m.end()
                continue
            m = _CHARSET.match(raw, i)
            i = m.end() if m else i + 1
            continue
        if ch == '\r':
            col = 0
        elif ch == '\n':
            cur = min(cur + 1, MAX_ROWS)
            col = 0
        elif ord(ch) >= 32:
            put(ch)
        i += 1
    return [''.join(r).rstrip() for r in rows]


# The block the client prints the token inside. Joining WITHIN it is what survives a
# terminal narrow enough to wrap the token across rows, without gluing the token to
# whatever the next paragraph happens to say.
_BLOCK_START = re.compile(r'Your\s*OAuth\s*token[^\n]{0,80}?:')
_BLOCK_END = re.compile(r'Store\s*this\s*token')


def extract_token(raw):
    """The setup-token the ceremony printed, or ''.

    Every reading is a candidate and the LONGEST wins, because the two shapes fail
    each other's method: a token painted out of order is whole only on the rendered
    line, while one the terminal wrapped is whole only across rows. A per-line match
    on a wrapped token returns its first 79 characters — exactly the truncation this
    whole path exists to prevent — so the joined reading has to compete with it.
    """
    lines = render(raw)
    text = '\n'.join(lines)
    cands = [m.group(0) for ln in lines for m in [TOKEN.search(ln)] if m]
    for m in _BLOCK_START.finditer(text):
        block = text[m.end(): m.end() + 4000]
        stop = _BLOCK_END.search(block)
        if stop:
            block = block[: stop.start()]
        cands += TOKEN.findall(re.sub(r'\s+', '', block))
    if not cands:                                    # a TUI this renderer misreads
        flat = _CSI.sub('', _CHARSET.sub('', raw))
        cands = TOKEN.findall(flat) + TOKEN.findall(re.sub(r'\s+', '', flat))
    return max(cands, key=len) if cands else ''


def debrief(raw):
    """(readable lines, api_key_minted). The screen the operator was left looking
    at, with every credential-shaped string masked."""
    api_key = False
    for ln in render(raw):
        if API_KEY.search(ln):
            api_key = True
    if extract_token(raw):
        api_key = False
    lines = []
    for ln in render(raw):
        ln = ANY_CRED.sub('sk-ant-***', ln).strip()
        if not ln or re.fullmatch(r'[\W_]+', ln):        # spinner frames, logo art
            continue
        if 'https://' in ln or re.search(r'Paste\s*code\s*here', ln):
            continue
        if lines and lines[-1] == ln:
            continue
        lines.append(ln)
    if api_key:
        # Said LAST, so it is what the operator reads.
        lines.append('the client minted an API KEY (sk-ant-api…), not a subscription '
                     'setup-token: the browser session belongs to a Console / '
                     'API-billing organization — sign in as the subscription account '
                     'and try again')
    return lines, api_key


def _read(path):
    with open(path, 'rb') as f:
        return f.read().decode('utf-8', 'ignore')


def main(argv):
    if len(argv) < 2:
        return 2
    verb, path = argv[0], argv[1]
    if verb == 'extract':
        sys.stdout.write(extract_token(_read(path)))
        return 0
    if verb == 'debrief':
        lines, _ = debrief(_read(path))
        if len(argv) > 2:
            try:
                fd = os.open(argv[2], os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
                with os.fdopen(fd, 'w') as f:
                    f.write('\n'.join(lines) + '\n')
            except OSError:
                pass
        print(' | '.join(lines[-3:])[:400])
        return 0
    return 2


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
