#!/usr/bin/env python3
"""
Atomic screenshot capture for tmux sessions.
UUID is derived from content hash — filename IS the verification.

Usage:
    python3 tmux-screenshot.py [--session NAME] [--outdir DIR]
    python3 tmux-screenshot.py --verify UUID

Outputs:
    {outdir}/{uuid}.png     (human-facing image)
    {outdir}/{uuid}.txt     (primary diagnostic text)
    {outdir}/{uuid}.json    (primary structured metadata)
    {outdir}/manifest.json  (updated index)

UUID = first 12 chars of SHA256(stripped_text)
"""
import re
import subprocess
import sys
import json
import hashlib
import unicodedata
from pathlib import Path
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont

# ═─ config ────────────────────────────────────────────────────────────────────
FONT_PATH = "/usr/local/share/fonts/dejavu/DejaVuSansMono.ttf"
FONT_BOLD_PATH = "/usr/local/share/fonts/dejavu/DejaVuSansMono-Bold.ttf"
FONT_SIZE = 15
PADDING = 20
BG_COLOR = (18, 18, 28)
DEFAULT_FG = (228, 228, 228)

DEFAULT_SESSION = "0"
DEFAULT_OUTDIR = Path("/usr/local/www/clawdie/screenshots")
BASE_URL = "https://clawdie.si/screenshots"

# ═─ signature database ────────────────────────────────────────────────────────
# Each signature has:
#   id            - unique identifier
#   severity      - 'error', 'warn', or 'ok' (success signature)
#   source_skill  - which skill owns the knowledge
#   patterns      - list of text patterns to match (case-insensitive substring)
#   next_action   - what the agent should do next
#   invoke        - (optional) skill to auto-invoke for remediation

# ── failure signatures ────────────────────────────────────────────────────────

FAILURE_SIGNATURES = [
    # bastille-network
    {
        'id': 'missing_warden_bridge',
        'severity': 'error',
        'source_skill': 'bastille-network',
        'patterns': [
            'ifconfig: interface warden0 does not exist',
        ],
        'next_action': 'Recreate warden0 and make it persistent in /etc/rc.conf before starting controlplane.',
        'invoke': 'bastille-network',
    },
    {
        'id': 'stale_bastille_epair',
        'severity': 'warn',
        'source_skill': 'bastille-network',
        'patterns': [
            'ioctl SIOCSIFNAME (set name): File exists',
            'ifconfig ${epair0} up name e0a_bastille1',
        ],
        'next_action': 'Destroy stale e0a_bastille1/e0b_bastille1 interfaces and retry Bastille start.',
        'invoke': 'bastille-network',
    },
    {
        'id': 'bridge_no_gateway',
        'severity': 'error',
        'source_skill': 'bastille-network',
        'patterns': [
            'BRIDGE_HAS_GATEWAY: false',
        ],
        'next_action': 'Assign 192.168.100.1/24 to warden0 and persist in rc.conf.',
        'invoke': 'bastille-network',
    },

    # warden-pf
    {
        'id': 'missing_default_route',
        'severity': 'error',
        'source_skill': 'warden-pf',
        'patterns': [
            'route: route has not been found',
            'Network is unreachable',
        ],
        'next_action': 'Verify controlplane has a default route to 192.168.100.1 and that host forwarding and pf are configured.',
        'invoke': 'warden-pf',
    },
    {
        'id': 'resolver_failure',
        'severity': 'error',
        'source_skill': 'warden-pf',
        'patterns': [
            'Transient resolver failure',
        ],
        'next_action': 'Validate resolver config, host forwarding, and pf/NAT for the Warden subnet.',
        'invoke': 'warden-pf',
    },
    {
        'id': 'missing_pf_table',
        'severity': 'warn',
        'source_skill': 'warden-pf',
        'patterns': [
            'pfctl: Table does not exist.',
        ],
        'next_action': 'Add the Warden pf table and NAT rules to the host pf.conf before relying on Bastille lifecycle cleanup.',
        'invoke': 'warden-pf',
    },
    {
        'id': 'pf_blocks_all_no_warden',
        'severity': 'error',
        'source_skill': 'warden-pf',
        'patterns': [
            'block drop all',
        ],
        'next_action': 'Host firewall blocks jail traffic. Add: pass quick on warden0 inet from 192.168.100.0/24 to any keep state.',
        'invoke': 'warden-pf',
    },
    {
        'id': 'pf_nat_empty',
        'severity': 'error',
        'source_skill': 'warden-pf',
        'patterns': [
            'No ALTQ support in kernel',
        ],
        'next_action': 'NAT rules may be missing. Check pfctl -sn and add: nat on vtnet0 from 192.168.100.0/24 to any -> (vtnet0).',
        'invoke': 'warden-pf',
    },

    # freebsd-admin
    {
        'id': 'host_forwarding_disabled',
        'severity': 'error',
        'source_skill': 'freebsd-admin',
        'patterns': [
            'net.inet.ip.forwarding: 0',
        ],
        'next_action': 'Set gateway_enable=YES in /etc/rc.conf and apply net.inet.ip.forwarding=1.',
        'invoke': 'freebsd-admin',
    },
    {
        'id': 'tailscale_resolver_leak',
        'severity': 'warn',
        'source_skill': 'freebsd-admin',
        'patterns': [
            'nameserver 100.100.100.100',
        ],
        'next_action': 'Host Tailscale resolver leaked into jail. Create dedicated Bastille resolver at /usr/local/etc/bastille/resolv.conf with 1.1.1.1 and 9.9.9.9.',
        'invoke': 'freebsd-admin',
    },

    # warden-bootstrap
    {
        'id': 'controlplane_missing',
        'severity': 'error',
        'source_skill': 'warden-bootstrap',
        'patterns': [
            'jexec: jail "controlplane" not found',
        ],
        'next_action': 'Create or start the controlplane jail before running Warden tasks.',
        'invoke': 'warden-bootstrap',
    },
    {
        'id': 'wrong_gateway_provisioned',
        'severity': 'error',
        'source_skill': 'warden-bootstrap',
        'patterns': [
            'add net default: gateway 51.83.197.1',
            'Invalid argument',
        ],
        'next_action': 'VNET jail was created without -g 192.168.100.1. Destroy and recreate with the correct Warden gateway.',
        'invoke': 'warden-bootstrap',
    },
    {
        'id': 'jail_no_default_route',
        'severity': 'warn',
        'source_skill': 'warden-bootstrap',
        'patterns': [
            'netstat -rn',
        ],
        'next_action': 'Check if jail has a default route. If missing, recreate with -g 192.168.100.1.',
        'invoke': 'warden-bootstrap',
    },

    # postgres-memory
    {
        'id': 'pg_shmget_fail',
        'severity': 'error',
        'source_skill': 'postgres-memory',
        'patterns': [
            'could not create shared memory segment',
            'shmget',
            'Function not implemented',
        ],
        'next_action': 'Set allow.sysvipc=1 on the db jail, restart jail, then rerun service postgresql initdb.',
        'invoke': 'postgres-memory',
    },
    {
        'id': 'pg_initdb_fail',
        'severity': 'error',
        'source_skill': 'postgres-memory',
        'patterns': [
            'initdb: error:',
        ],
        'next_action': 'PostgreSQL initdb failed. Check allow.sysvipc=1 and jail permissions.',
        'invoke': 'postgres-memory',
    },

    # sanoid
    {
        'id': 'sanoid_sample_dataset_warning',
        'severity': 'info',
        'source_skill': 'sanoid',
        'patterns': [
            'zpoolname/parent2',
            'dataset does not exist',
        ],
        'next_action': 'Benign warning from shipped sample config. Remove or comment out the zpoolname/parent2 block in sanoid.conf.',
    },
    {
        'id': 'sanoid_no_config',
        'severity': 'warn',
        'source_skill': 'sanoid',
        'patterns': [
            'no sanoid.conf found',
            'sanoid.conf: No such file',
        ],
        'next_action': 'Write /usr/local/etc/sanoid/sanoid.conf for controlplane and db datasets.',
        'invoke': 'sanoid',
    },

    # warden-zfs
    {
        'id': 'zfs_pool_degraded',
        'severity': 'error',
        'source_skill': 'warden-zfs',
        'patterns': [
            'state: DEGRADED',
            'state: FAULTED',
        ],
        'next_action': 'ZFS pool is unhealthy. Investigate with zpool status before any jail operations.',
    },
    {
        'id': 'zfs_dataset_not_found',
        'severity': 'error',
        'source_skill': 'warden-zfs',
        'patterns': [
            "cannot open 'zroot/clawdie-runtime'",
        ],
        'next_action': 'Clawdie-runtime dataset missing. Run warden-zfs to create the dataset layout.',
        'invoke': 'warden-zfs',
    },

    # telegram-admin
    {
        'id': 'telegram_typing_no_reply',
        'severity': 'warn',
        'source_skill': 'telegram-admin',
        'patterns': [
            'Typing...',
        ],
        'next_action': 'Bot shows typing but no reply. Check downstream jail health with npm run doctor and logs/clawdie.log.',
        'invoke': 'warden-health',
    },
]

# ── success signatures ────────────────────────────────────────────────────────

SUCCESS_SIGNATURES = [
    # bastille-network
    {
        'id': 'bridge_healthy',
        'severity': 'ok',
        'source_skill': 'bastille-network',
        'patterns': [
            'inet 192.168.100.1',
        ],
        'next_action': 'Warden bridge warden0 is healthy.',
    },
    {
        'id': 'vnet_epair_created',
        'severity': 'ok',
        'source_skill': 'bastille-network',
        'patterns': [
            'Applying template: default/vnet',
            'Creating a thickjail',
        ],
        'next_action': 'VNET thick jail creation succeeded.',
    },

    # freebsd-admin
    {
        'id': 'forwarding_enabled',
        'severity': 'ok',
        'source_skill': 'freebsd-admin',
        'patterns': [
            'net.inet.ip.forwarding: 1',
        ],
        'next_action': 'Host IP forwarding is enabled.',
    },

    # warden-bootstrap
    {
        'id': 'controlplane_hostname_ok',
        'severity': 'ok',
        'source_skill': 'warden-bootstrap',
        'patterns': [
            'ai.clawdie.si',
        ],
        'next_action': 'Controlplane hostname is correctly set.',
    },
    {
        'id': 'controlplane_ip_ok',
        'severity': 'ok',
        'source_skill': 'warden-bootstrap',
        'patterns': [
            'inet 192.168.100.100',
        ],
        'next_action': 'Controlplane jail has the correct IP.',
    },

    # postgres-memory
    {
        'id': 'db_ip_ok',
        'severity': 'ok',
        'source_skill': 'postgres-memory',
        'patterns': [
            'inet 192.168.100.2',
        ],
        'next_action': 'Database jail has the correct IP.',
    },
    {
        'id': 'pg_running',
        'severity': 'ok',
        'source_skill': 'postgres-memory',
        'patterns': [
            'postgres: checkpointer',
            'postgres: background writer',
        ],
        'next_action': 'PostgreSQL is running inside the db jail.',
    },

    # warden-zfs
    {
        'id': 'zpool_healthy',
        'severity': 'ok',
        'source_skill': 'warden-zfs',
        'patterns': [
            'state: ONLINE',
        ],
        'next_action': 'ZFS pool is healthy.',
    },
    {
        'id': 'clawdie_runtime_exists',
        'severity': 'ok',
        'source_skill': 'warden-zfs',
        'patterns': [
            'zroot/clawdie-runtime/jails/controlplane',
        ],
        'next_action': 'Controlplane dataset exists under clawdie-runtime.',
    },

    # sanoid
    {
        'id': 'sanoid_snapshots_active',
        'severity': 'ok',
        'source_skill': 'sanoid',
        'patterns': [
            'autosnap_',
        ],
        'next_action': 'Sanoid automated snapshots are being created.',
    },

    # warden-pf
    {
        'id': 'nat_rule_present',
        'severity': 'ok',
        'source_skill': 'warden-pf',
        'patterns': [
            'nat on vtnet0 from 192.168.100.0/24',
        ],
        'next_action': 'NAT rule for Warden subnet is active.',
    },
    {
        'id': 'warden_pass_rule',
        'severity': 'ok',
        'source_skill': 'warden-pf',
        'patterns': [
            'pass quick on warden0',
        ],
        'next_action': 'PF pass rule for warden0 is active.',
    },
]

# Combined for the detector — failures checked first, then success
SIGNATURES = FAILURE_SIGNATURES + SUCCESS_SIGNATURES


def tmux_meta(session_name: str) -> dict:
    def q(fmt: str) -> str:
        result = subprocess.run(
            ['tmux', 'display-message', '-p', '-t', session_name, fmt],
            capture_output=True,
        )
        if result.returncode != 0:
            return ''
        return result.stdout.decode('utf-8', errors='replace').strip()

    return {
        'session': q('#S') or session_name,
        'window_index': q('#I'),
        'window_name': q('#W'),
        'pane_id': q('#D'),
        'pane_title': q('#T'),
        'pane_current_command': q('#{pane_current_command}'),
        'pane_current_path': q('#{pane_current_path}'),
    }

# ═─ char width ────────────────────────────────────────────────────────────────
def char_width(ch: str) -> int:
    """Return terminal column width: 2 for East Asian Wide/Fullwidth, else 1."""
    if ch in ('\n', '\r'):
        return 0
    eaw = unicodedata.east_asian_width(ch)
    return 2 if eaw in ('F', 'W') else 1


def visual_len(text: str) -> int:
    """Column width of a string accounting for double-width characters."""
    return sum(char_width(c) for c in text)


# ═─ ANSI 256-color palette ────────────────────────────────────────────────────
def build_256_palette():
    palette: dict[int, tuple[int, int, int]] = {}
    basic = [
        (0,0,0),(170,0,0),(0,170,0),(170,85,0),
        (0,0,170),(170,0,170),(0,170,170),(170,170,170),
        (85,85,85),(255,85,85),(85,255,85),(255,255,85),
        (85,85,255),(255,85,255),(85,255,255),(255,255,255),
    ]
    for i, c in enumerate(basic):
        palette[i] = c
    for i in range(216):
        r = (i // 36) * 51
        g = ((i // 6) % 6) * 51
        b = (i % 6) * 51
        palette[16 + i] = (r, g, b)
    for i in range(24):
        v = 8 + i * 10
        palette[232 + i] = (v, v, v)
    return palette

PALETTE = build_256_palette()

# ═─ ANSI parsing ──────────────────────────────────────────────────────────────
ANSI_SGR = re.compile(r'\x1b\[([0-9;]*)m')
STRIP_ESC = re.compile(r'\x1b\[[0-9;]*[A-Za-ln-z]')  # exclude 'm' (SGR)
STRIP_CTRL = re.compile(r'[\x00-\x08\x0b-\x1f\x7f]')


def parse_ansi(text: str):
    """Yield (char, fg_color, bg_color, bold) tuples from ANSI-colored text."""
    fg = DEFAULT_FG
    bg = BG_COLOR
    bold = False

    text = STRIP_ESC.sub('', text)
    text = STRIP_CTRL.sub('', text)

    pos = 0
    for m in ANSI_SGR.finditer(text):
        for ch in text[pos:m.start()]:
            yield (ch, fg, bg, bold)
        pos = m.end()

        raw = m.group(1)
        params = [int(x) if x else 0 for x in raw.split(';')] if raw else [0]
        i = 0
        while i < len(params):
            p = params[i]
            if p == 0:
                fg = DEFAULT_FG; bg = BG_COLOR; bold = False
            elif p == 1:
                bold = True
            elif p == 22:
                bold = False
            elif p == 39:
                fg = DEFAULT_FG
            elif p == 49:
                bg = BG_COLOR
            elif 30 <= p <= 37:
                fg = PALETTE[p - 30]
            elif 40 <= p <= 47:
                bg = PALETTE[p - 40]
            elif 90 <= p <= 97:
                fg = PALETTE[p - 90 + 8]
            elif 100 <= p <= 107:
                bg = PALETTE[p - 100 + 8]
            elif p == 38:
                if i + 2 < len(params) and params[i+1] == 5:
                    # 256-color foreground
                    fg = PALETTE.get(params[i+2], DEFAULT_FG)
                    i += 2
                elif i + 4 < len(params) and params[i+1] == 2:
                    # 24-bit truecolor foreground
                    fg = (params[i+2], params[i+3], params[i+4])
                    i += 4
            elif p == 48:
                if i + 2 < len(params) and params[i+1] == 5:
                    # 256-color background
                    bg = PALETTE.get(params[i+2], BG_COLOR)
                    i += 2
                elif i + 4 < len(params) and params[i+1] == 2:
                    # 24-bit truecolor background
                    bg = (params[i+2], params[i+3], params[i+4])
                    i += 4
            i += 1

    for ch in text[pos:]:
        yield (ch, fg, bg, bold)


def strip_ansi(text: str) -> str:
    text = ANSI_SGR.sub('', text)
    text = STRIP_ESC.sub('', text)
    text = STRIP_CTRL.sub('', text)
    return text


# ═─ capture ───────────────────────────────────────────────────────────────────
def capture_tmux(session_name: str) -> str:
    result = subprocess.run(
        ['tmux', 'capture-pane', '-t', session_name, '-e', '-p'],
        capture_output=True,
    )
    if result.returncode != 0:
        raise RuntimeError(f"tmux capture failed: {result.stderr.decode('utf-8', errors='replace')}")
    return result.stdout.decode('utf-8', errors='replace')


def calculate_hash(content: str) -> str:
    return hashlib.sha256(content.encode('utf-8')).hexdigest()


def detect_signatures(text: str) -> dict:
    """Detect known failure and success signatures in terminal text.

    Returns a dict with 'failures', 'warnings', 'info', and 'healthy' lists.
    Failures are checked first so the agent can act on problems before
    acknowledging healthy state.
    """
    lowered = text.lower()
    failures = []
    warnings = []
    info = []
    healthy = []

    for signature in FAILURE_SIGNATURES:
        for pattern in signature['patterns']:
            if pattern.lower() in lowered:
                entry = {
                    'id': signature['id'],
                    'severity': signature['severity'],
                    'source_skill': signature['source_skill'],
                    'matched_text': pattern,
                    'next_action': signature['next_action'],
                }
                if 'invoke' in signature:
                    entry['invoke'] = signature['invoke']
                if signature['severity'] == 'error':
                    failures.append(entry)
                elif signature['severity'] == 'warn':
                    warnings.append(entry)
                else:
                    info.append(entry)
                break

    for signature in SUCCESS_SIGNATURES:
        for pattern in signature['patterns']:
            if pattern.lower() in lowered:
                healthy.append({
                    'id': signature['id'],
                    'severity': 'ok',
                    'source_skill': signature['source_skill'],
                    'matched_text': pattern,
                    'next_action': signature['next_action'],
                })
                break

    return {
        'failures': failures,
        'warnings': warnings,
        'info': info,
        'healthy': healthy,
        # flat list for backward compatibility with JSON metadata
        'all': failures + warnings + info + healthy,
    }


# ═─ render ────────────────────────────────────────────────────────────────────
def render_png(raw: str, session_name: str) -> Image.Image:
    font_reg = ImageFont.truetype(FONT_PATH, FONT_SIZE)
    font_bold = ImageFont.truetype(FONT_BOLD_PATH, FONT_SIZE)

    bbox = font_reg.getbbox("M")
    CELL_W = bbox[2] - bbox[0]
    CELL_H = int(FONT_SIZE * 1.35)

    lines = raw.split('\n')
    while lines and not lines[-1].strip():
        lines.pop()

    # Use visual_len for correct wide-char column counting
    cols = max((visual_len(strip_ansi(l)) for l in lines), default=80)
    rows = len(lines)

    img_w = int(cols * CELL_W + PADDING * 2)
    img_h = int(rows * CELL_H + PADDING * 2)

    img = Image.new('RGB', (img_w, img_h), BG_COLOR)

    # Title bar
    TITLEBAR_H = 28
    full_img = Image.new('RGB', (img_w, img_h + TITLEBAR_H), (30, 30, 45))
    full_img.paste(img, (0, TITLEBAR_H))
    draw = ImageDraw.Draw(full_img)

    for i, col in enumerate([(255,95,87),(255,189,46),(39,201,63)]):
        draw.ellipse([PADDING + i*20, 8, PADDING + i*20 + 12, 20], fill=col)

    title_font = ImageFont.truetype(FONT_PATH, 12)
    title_text = f"tmux · {session_name}"
    tw = title_font.getbbox(title_text)[2]
    draw.text(((img_w - tw) // 2, 8), title_text, fill=(150, 150, 170), font=title_font)

    y = TITLEBAR_H + PADDING
    for line in lines:
        x = PADDING
        for ch, fg, bg, bold in parse_ansi(line):
            if ch == '\n':
                break
            font = font_bold if bold else font_reg
            cw = char_width(ch)
            cell_w = CELL_W * cw
            if bg != BG_COLOR and bg != (30, 30, 45):
                draw.rectangle([x, y, x + cell_w, y + CELL_H], fill=bg)
            draw.text((x, y), ch, fill=fg, font=font)
            x += cell_w
        y += CELL_H

    return full_img


# ═─ verify ────────────────────────────────────────────────────────────────────
def verify_by_filename(uuid: str, txt_path: Path) -> bool:
    content = txt_path.read_text(encoding='utf-8')
    calculated = calculate_hash(content)[:12]
    if calculated != uuid:
        raise ValueError(
            f"Hash mismatch!\n"
            f"Filename:   {uuid}\n"
            f"Calculated: {calculated}"
        )
    return True


# ═─ manifest ──────────────────────────────────────────────────────────────────
def update_manifest(outdir: Path, new_metadata: dict) -> None:
    manifest_path = outdir / 'manifest.json'
    existing = []
    if manifest_path.exists():
        try:
            data = json.loads(manifest_path.read_text())
            existing = data.get('screenshots', [])
        except Exception as e:
            print(f"Warning: failed to load manifest: {e}", file=sys.stderr)

    existing = [s for s in existing if s['uuid'] != new_metadata['uuid']]
    existing.append(new_metadata)
    existing.sort(key=lambda x: x['captured_at'], reverse=True)

    manifest_path.write_text(json.dumps({
        'generated_at': datetime.now().isoformat(),
        'count': len(existing),
        'screenshots': existing,
    }, indent=2))
    print(f"Manifest: {len(existing)} screenshots")


# ═─ main capture ──────────────────────────────────────────────────────────────
def capture_screenshot(session: str, outdir: Path) -> dict:
    outdir.mkdir(parents=True, exist_ok=True)

    capture_time = datetime.now()
    print(f"Capturing: {session}")
    raw = capture_tmux(session)
    meta = tmux_meta(session)

    stripped = strip_ansi(raw)
    txt_hash = calculate_hash(stripped)
    uuid = txt_hash[:12]
    sig_result = detect_signatures(stripped)
    detected_signatures = sig_result['all']
    if sig_result['failures']:
        print(f"UUID: {uuid}  *** {len(sig_result['failures'])} FAILURE(S) DETECTED ***")
        for f in sig_result['failures']:
            print(f"  ERROR: {f['id']} — {f['next_action']}")
    elif sig_result['warnings']:
        print(f"UUID: {uuid}  ({len(sig_result['warnings'])} warning(s))")
    else:
        print(f"UUID: {uuid}  ({len(sig_result['healthy'])} healthy signal(s))")

    print("Rendering PNG...")
    img = render_png(raw, session)

    date_str = capture_time.strftime('%d.%m.%Y %H:%M:%S')

    png_path = outdir / f"{uuid}.png"
    txt_path = outdir / f"{uuid}.txt"
    json_path = outdir / f"{uuid}.json"

    metadata = {
        'uuid': uuid,
        'session': meta['session'],
        'captured_at': capture_time.isoformat(),
        'captured_at_display': date_str,
        'host': subprocess.getoutput('hostname').strip(),
        'window_index': meta['window_index'],
        'window_name': meta['window_name'],
        'pane_id': meta['pane_id'],
        'pane_title': meta['pane_title'],
        'pane_current_command': meta['pane_current_command'],
        'pane_current_path': meta['pane_current_path'],
        'txt_hash': txt_hash,
        'text_path': txt_path.name,
        'png_path': png_path.name,
        'json_path': json_path.name,
        'line_count': len(stripped.splitlines()),
        'tags': ['tmux', 'terminal', 'diagnostic'],
        'detected_signatures': detected_signatures,
        'dimensions': {'width': img.width, 'height': img.height},
    }

    print(f"Saving: {png_path}")
    img.save(png_path, 'PNG', optimize=True)

    print(f"Saving: {txt_path}")
    txt_path.write_text(stripped, encoding='utf-8')

    print(f"Saving: {json_path}")
    json_path.write_text(json.dumps(metadata, indent=2))

    update_manifest(outdir, metadata)

    print("Verifying...")
    try:
        verify_by_filename(uuid, txt_path)
        print("Verified OK")
    except ValueError as e:
        print(f"Verification failed: {e}", file=sys.stderr)
        return {'success': False, 'error': str(e)}

    url = f"{BASE_URL}/{uuid}.png"
    print(f"\nScreenshot saved: {uuid}")
    print(f"  PNG: {png_path} ({img.width}x{img.height}px)")
    print(f"  TXT: {txt_path}")
    print(f"  URL: {url}")

    return {
        'success': True,
        'uuid': uuid,
        'png': str(png_path),
        'txt': str(txt_path),
        'json': str(json_path),
        'url': url,
        'metadata': metadata,
    }


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser(description='Atomic tmux screenshot for Clawdie')
    parser.add_argument('--session', '-s', default=DEFAULT_SESSION, help=f'tmux session (default: {DEFAULT_SESSION})')
    parser.add_argument('--outdir', '-o', default=str(DEFAULT_OUTDIR), help=f'output directory (default: {DEFAULT_OUTDIR})')
    parser.add_argument('--verify', metavar='UUID', help='verify an existing screenshot by UUID')
    args = parser.parse_args()

    outdir = Path(args.outdir)

    if args.verify:
        uuid = args.verify
        txt_path = outdir / f"{uuid}.txt"
        if not txt_path.exists():
            print(f"Not found: {txt_path}", file=sys.stderr)
            sys.exit(1)
        try:
            verify_by_filename(uuid, txt_path)
            print("Verified OK")
            sys.exit(0)
        except ValueError as e:
            print(f"Failed: {e}", file=sys.stderr)
            sys.exit(1)
    else:
        result = capture_screenshot(args.session, outdir)
        sys.exit(0 if result['success'] else 1)
