#!/usr/bin/env bash
# incus-mitm -- Capture and analyze Incus API traffic via event stream
#
# Usage: incus-mitm [ACTION] [OPTIONS]
#
# Actions: --capture, --analyze, --live, --callers
# Requires: incus CLI with cluster remote configured, python3
#
# The primary capture method uses `incus monitor` which captures all
# API operations without requiring TLS interception.

set -euo pipefail

# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------

setup_colors() {
    if [[ "${NO_COLOR:-}" == "1" || "${TERM:-}" == "dumb" ]]; then
        BOLD="" DIM="" RESET="" CYAN="" GREEN="" YELLOW="" RED="" BLUE=""
    else
        BOLD=$'\033[1m'   DIM=$'\033[2m'     RESET=$'\033[0m'
        CYAN=$'\033[36m'  GREEN=$'\033[32m'   YELLOW=$'\033[33m'
        RED=$'\033[31m'   BLUE=$'\033[34m'
    fi
}
setup_colors

# ---------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------

usage() {
    cat <<'EOF'
Usage: incus-mitm [ACTION] [OPTIONS]

Capture and analyze Incus REST API traffic using the event stream.
No TLS interception needed — uses `incus monitor` to observe all
API calls, operations, and lifecycle events in real time.

Actions:
  --capture [SECONDS]   Capture events for N seconds (default: 60)
  --live                Stream events in real time (Ctrl+C to stop)
  --analyze FILE        Analyze a previously captured JSON file
  --callers FILE        Show unique API callers by certificate

Options:
  --remote NAME         Incus remote name (default: oc-node-01)
  --output FILE         Output file for capture (default: /tmp/incus-events-<timestamp>.json)
  --filter CALLER       Only show events from specific caller (aether, cli, node)
  --dry-run             Show commands that would be executed
  --help, -h            Show this help

Examples:
  incus-mitm --capture 120                    # Capture 2 minutes of events
  incus-mitm --capture 30 --output events.json
  incus-mitm --live                           # Stream in real time
  incus-mitm --analyze /tmp/incus-events.json
  incus-mitm --callers /tmp/incus-events.json
  incus-mitm --analyze events.json --filter aether

Certificate Fingerprint Reference:
  Capture events first, then use --callers to identify who is who.
  Common patterns:
    CN=maarten@*     → CLI client
    CN=root@oc-*     → Aether SDN controller
    CN=root@oc-node-* → Cluster inter-node traffic
EOF
    exit 0
}

# ---------------------------------------------------------------------------
# Defaults and argument parsing
# ---------------------------------------------------------------------------

ACTION=""
REMOTE="oc-node-01"
OUTPUT=""
CAPTURE_SECONDS=60
FILTER=""
DRY_RUN=false
ANALYZE_FILE=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --capture)
            ACTION="capture"
            if [[ "${2:-}" =~ ^[0-9]+$ ]]; then
                CAPTURE_SECONDS="$2"
                shift
            fi
            shift
            ;;
        --live)       ACTION="live";     shift ;;
        --analyze)
            ACTION="analyze"
            ANALYZE_FILE="${2:?--analyze requires a file}"
            shift 2
            ;;
        --callers)
            ACTION="callers"
            ANALYZE_FILE="${2:?--callers requires a file}"
            shift 2
            ;;
        --remote)     REMOTE="${2:?--remote requires a name}"; shift 2 ;;
        --output)     OUTPUT="${2:?--output requires a path}"; shift 2 ;;
        --filter)     FILTER="${2:?--filter requires a caller type}"; shift 2 ;;
        --dry-run)    DRY_RUN=true;  shift ;;
        -h|--help)    usage ;;
        *)
            echo "Error: unknown option: $1" >&2
            echo "Run 'incus-mitm --help' for usage." >&2
            exit 1
            ;;
    esac
done

if [[ -z "$ACTION" ]]; then
    usage
fi

# ---------------------------------------------------------------------------
# Capture
# ---------------------------------------------------------------------------

do_capture() {
    local output="${OUTPUT:-/tmp/incus-events-$(date +%Y%m%d-%H%M%S).json}"

    if [[ "$DRY_RUN" == true ]]; then
        echo "${DIM}[dry-run] timeout ${CAPTURE_SECONDS} incus monitor ${REMOTE}: --format=json > ${output}${RESET}"
        return 0
    fi

    echo "${BOLD}Capturing Incus events for ${CAPTURE_SECONDS}s...${RESET}"
    echo "${DIM}Output: ${output}${RESET}"
    echo "${DIM}Trigger operations in Aether or via CLI to generate events.${RESET}"
    echo

    timeout "$CAPTURE_SECONDS" incus monitor "${REMOTE}:" --format=json > "$output" 2>&1 || true

    local count
    count=$(wc -l < "$output")
    echo
    echo "${GREEN}Captured ${count} events → ${output}${RESET}"
    echo "Run: ${BOLD}incus-mitm --analyze ${output}${RESET}"
}

# ---------------------------------------------------------------------------
# Live stream
# ---------------------------------------------------------------------------

do_live() {
    if [[ "$DRY_RUN" == true ]]; then
        echo "${DIM}[dry-run] incus monitor ${REMOTE}: --format=json | python3 (live parser)${RESET}"
        return 0
    fi

    echo "${BOLD}Live event stream (Ctrl+C to stop)${RESET}"
    echo

    incus monitor "${REMOTE}:" --format=json 2>&1 | python3 -u -c "
import sys, json

# Known fingerprint prefixes (populated during session)
known_certs = {}

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        e = json.loads(line)
    except:
        continue

    ts = e.get('timestamp', '')[11:19]
    etype = e.get('type', '')
    meta = e.get('metadata', {})

    # Track cert fingerprints
    if etype == 'logging' and 'Matched trusted cert' in meta.get('message', ''):
        ctx = meta.get('context', {})
        fp = ctx.get('fingerprint', '')
        subj = ctx.get('subject', '')
        if fp and subj:
            known_certs[fp[:20]] = subj

    # Show API calls
    if etype == 'logging' and 'method' in meta.get('context', {}):
        ctx = meta['context']
        fp = ctx.get('username', '')[:20]
        caller = known_certs.get(fp, fp[:12] + '...')
        if 'maarten' in caller:
            caller = 'CLI'
        elif 'oc-server' in caller:
            caller = 'Aether'
        elif 'oc-node' in caller:
            caller = caller.split('root@')[1].split(',')[0] if 'root@' in caller else 'node'

        filter_val = '${FILTER}'.lower()
        if filter_val:
            if filter_val == 'aether' and 'Aether' not in caller:
                continue
            elif filter_val == 'cli' and 'CLI' not in caller:
                continue
            elif filter_val == 'node' and 'oc-node' not in caller:
                continue

        print(f'  \033[2m{ts}\033[0m {ctx[\"method\"]:6s} {ctx[\"url\"]:70s} \033[36m[{caller}]\033[0m', flush=True)

    # Show lifecycle events
    elif etype == 'lifecycle':
        action = meta.get('action', '')
        source = (meta.get('source') or '').split('/')[-1]
        print(f'  \033[2m{ts}\033[0m \033[33m{action:30s}\033[0m {source}', flush=True)

    # Show operation status changes (only success/failure)
    elif etype == 'operation':
        status = meta.get('status', '')
        if status in ('Success', 'Failure'):
            desc = meta.get('description', '')
            resources = []
            for k, v in (meta.get('resources') or {}).items():
                for item in v:
                    resources.append(item.split('/')[-1])
            res = ', '.join(resources)
            color = '\033[32m' if status == 'Success' else '\033[31m'
            print(f'  \033[2m{ts}\033[0m {color}[{status:7s}]\033[0m {desc:30s} {res}', flush=True)
"
}

# ---------------------------------------------------------------------------
# Analyze
# ---------------------------------------------------------------------------

do_analyze() {
    if [[ ! -f "$ANALYZE_FILE" ]]; then
        echo "Error: file not found: $ANALYZE_FILE" >&2
        exit 1
    fi

    python3 -c "
import json, sys
from collections import defaultdict

events = []
with open('${ANALYZE_FILE}') as f:
    for line in f:
        line = line.strip()
        if not line: continue
        try: events.append(json.loads(line))
        except: pass

# Build cert map
cert_map = {}
for e in events:
    if e.get('type') == 'logging' and 'Matched trusted cert' in e.get('metadata', {}).get('message', ''):
        ctx = e['metadata']['context']
        fp = ctx.get('fingerprint', '')
        subj = ctx.get('subject', '')
        if fp and subj:
            cert_map[fp[:20]] = subj

def caller_name(fp):
    subj = cert_map.get(fp[:20], '')
    if 'maarten' in subj: return 'CLI'
    if 'oc-server' in subj: return 'Aether'
    if 'oc-node' in subj:
        return subj.split('root@')[1].split(',')[0] if 'root@' in subj else 'node'
    return fp[:12] + '...'

filter_val = '${FILTER}'.lower()

print('\033[1m\033[36m=== API Calls ===\033[0m')
api_calls = []
for e in events:
    if e.get('type') == 'logging' and 'method' in e.get('metadata', {}).get('context', {}):
        ctx = e['metadata']['context']
        caller = caller_name(ctx.get('username', ''))
        if filter_val:
            if filter_val == 'aether' and caller != 'Aether': continue
            elif filter_val == 'cli' and caller != 'CLI': continue
        ts = e['timestamp'][11:19]
        api_calls.append((ts, ctx['method'], ctx['url'], caller))
        print(f\"  {ts} {ctx['method']:6s} {ctx['url']:70s} [{caller}]\")

print()
print('\033[1m\033[36m=== Operations ===\033[0m')
seen = set()
for e in events:
    if e.get('type') == 'operation':
        meta = e.get('metadata', {})
        key = f\"{meta.get('id')}_{meta.get('status')}\"
        if key in seen: continue
        seen.add(key)
        status = meta.get('status', '')
        if status not in ('Success', 'Failure', 'Cancelled'): continue
        resources = []
        for k, v in (meta.get('resources') or {}).items():
            for item in v: resources.append(item.split('/')[-1])
        ts = e['timestamp'][11:19]
        color = '\033[32m' if status == 'Success' else '\033[31m'
        print(f\"  {ts} {color}[{status:7s}]\033[0m {meta.get('description'):30s} {', '.join(resources)}\")

print()
print('\033[1m\033[36m=== Lifecycle Events ===\033[0m')
for e in events:
    if e.get('type') == 'lifecycle':
        meta = e.get('metadata', {})
        ts = e['timestamp'][11:19]
        source = (meta.get('source') or '').split('/')[-1]
        print(f\"  {ts} {meta.get('action'):30s} {source}\")

print()
print('\033[1m\033[36m=== Summary ===\033[0m')
print(f'  Total events: {len(events)}')
print(f'  API calls: {len(api_calls)}')
callers = defaultdict(int)
methods = defaultdict(int)
for _, method, url, caller in api_calls:
    callers[caller] += 1
    methods[method] += 1
print(f'  By caller:')
for c, n in sorted(callers.items(), key=lambda x: -x[1]):
    print(f'    {c}: {n}')
print(f'  By method:')
for m, n in sorted(methods.items(), key=lambda x: -x[1]):
    print(f'    {m}: {n}')
"
}

# ---------------------------------------------------------------------------
# Callers
# ---------------------------------------------------------------------------

do_callers() {
    if [[ ! -f "$ANALYZE_FILE" ]]; then
        echo "Error: file not found: $ANALYZE_FILE" >&2
        exit 1
    fi

    python3 -c "
import json
from collections import defaultdict

events = []
with open('${ANALYZE_FILE}') as f:
    for line in f:
        line = line.strip()
        if not line: continue
        try: events.append(json.loads(line))
        except: pass

cert_map = {}
call_counts = defaultdict(int)

for e in events:
    if e.get('type') == 'logging':
        meta = e.get('metadata', {})
        ctx = meta.get('context', {})
        if 'Matched trusted cert' in meta.get('message', ''):
            fp = ctx.get('fingerprint', '')
            subj = ctx.get('subject', '')
            if fp and subj:
                cert_map[fp] = subj
        if 'method' in ctx:
            call_counts[ctx.get('username', '')] += 1

print('\033[1m\033[36m=== Certificate Fingerprints ===\033[0m')
for fp, subj in sorted(cert_map.items(), key=lambda x: x[1]):
    calls = call_counts.get(fp, 0)
    if 'maarten' in subj:
        role = 'CLI client'
    elif 'oc-server' in subj:
        role = 'Aether SDN controller'
    elif 'oc-node' in subj:
        role = 'Cluster node'
    else:
        role = 'Unknown'
    print(f'  {fp[:40]}...')
    print(f'    Subject: {subj}')
    print(f'    Role:    {role}')
    print(f'    Calls:   {calls}')
    print()
"
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

case "$ACTION" in
    capture)  do_capture  ;;
    live)     do_live     ;;
    analyze)  do_analyze  ;;
    callers)  do_callers  ;;
esac
