#!/usr/bin/env python3
"""
carla-jack-autoconnect — Auto-connect Carla plugins to JACK system ports.

Monitors JACK for new Carla plugin ports and auto-wires them according
to the default mixer routing (carxp preset). Runs as a daemon or one-shot.

Usage:
  python3 carla-jack-autoconnect                    # One-shot: connect existing ports
  python3 carla-jack-autoconnect --watch             # Daemon: watch for new ports
  python3 carla-jack-autoconnect --dry-run           # Show what would be connected
  python3 carla-jack-autoconnect --disconnect-all    # Disconnect all Carla ports
"""

import subprocess
import sys
import time
import re
import json
import os
from typing import Dict, List, Tuple, Set

# ── Routing rules ──────────────────────────────────────
# Format: (source_regex, dest_regex, label)
# Order matters — first match wins for each dest port.
ROUTING_RULES: List[Tuple[str, str, str]] = [
    # System inputs → Channel processing chain
    (r"system:capture_1$",   r".*Ch1.*Gate.*in[12]$",   "Capture 1 → Ch1 Gate"),
    (r"system:capture_2$",   r".*Ch2.*Gate.*in[12]$",   "Capture 2 → Ch2 Gate"),
    (r"system:capture_3$",   r".*Ch3.*NAM.*in1$",       "Capture 3 → Guitar NAM"),
    (r"system:capture_4$",   r".*Ch4.*in[12]$",         "Capture 4 → Ch4"),

    # Chain outputs → chain inputs (plugin internal routing)
    (r".*Ch1.*Gate.*out[12]$",  r".*Ch1.*EQ.*in[12]$",    "Ch1 Gate → Ch1 EQ"),
    (r".*Ch1.*EQ.*out[12]$",    r".*Ch1.*Comp.*in[12]$",  "Ch1 EQ → Ch1 Comp"),
    (r".*Ch2.*Gate.*out[12]$",  r".*Ch2.*EQ.*in[12]$",    "Ch2 Gate → Ch2 EQ"),
    (r".*Ch2.*EQ.*out[12]$",    r".*Ch2.*Comp.*in[12]$",  "Ch2 EQ → Ch2 Comp"),
    (r".*Ch3.*NAM.*out[12]$",   r".*Ch3.*IR.*in[12]$",     "Ch3 NAM → Ch3 IR Loader"),

    # Master output → System playback
    (r".*Master.*Limiter.*out1$", r"system:playback_1$", "Master L → Playback 1"),
    (r".*Master.*Limiter.*out2$", r"system:playback_2$", "Master R → Playback 2"),
]

def jack_lsp() -> List[str]:
    """Get all JACK ports."""
    try:
        result = subprocess.run(
            ["jack_lsp", "-c"],
            capture_output=True, text=True, timeout=5
        )
        return result.stdout.strip().split("\n") if result.stdout.strip() else []
    except (FileNotFoundError, subprocess.TimeoutExpired):
        print("[autoconnect] ERROR: jack_lsp not found. Is JACK running?", file=sys.stderr)
        return []

def jack_connect(source: str, dest: str) -> bool:
    """Connect two JACK ports."""
    try:
        result = subprocess.run(
            ["jack_connect", source, dest],
            capture_output=True, text=True, timeout=5
        )
        return result.returncode == 0
    except Exception as e:
        print(f"[autoconnect] Connect failed: {source} → {dest}: {e}", file=sys.stderr)
        return False

def jack_disconnect(source: str, dest: str) -> bool:
    """Disconnect two JACK ports."""
    try:
        result = subprocess.run(
            ["jack_disconnect", source, dest],
            capture_output=True, text=True, timeout=5
        )
        return result.returncode == 0
    except Exception:
        return False

def get_current_connections() -> Set[Tuple[str, str]]:
    """Get all current JACK connections."""
    try:
        result = subprocess.run(
            ["jack_lsp", "-c"],
            capture_output=True, text=True, timeout=5
        )
        connections = set()
        for line in result.stdout.strip().split("\n"):
            parts = line.strip().split()
            if len(parts) >= 2:
                # First token is the output port, subsequent are inputs
                source = parts[0]
                for dest in parts[1:]:
                    connections.add((source, dest))
        return connections
    except Exception:
        return set()

def match_port(pattern: str, ports: List[str]) -> List[str]:
    """Return ports matching a regex pattern."""
    matched = []
    for port in ports:
        if re.search(pattern, port):
            matched.append(port)
    return matched

def apply_routing(dry_run: bool = False, verbose: bool = True) -> Dict:
    """Apply routing rules to JACK ports. Returns stats."""
    ports = jack_lsp()
    if not ports:
        return {"status": "no_jack", "connected": 0, "failed": 0, "skipped": 0}

    stats = {"connected": 0, "failed": 0, "skipped": 0, "pairs": []}

    for source_regex, dest_regex, label in ROUTING_RULES:
        sources = match_port(source_regex, ports)
        dests = match_port(dest_regex, ports)

        for src in sources:
            for dst in dests:
                if dry_run:
                    print(f"  [DRY-RUN] {src} → {dst}  ({label})")
                    stats["pairs"].append((src, dst, label))
                else:
                    success = jack_connect(src, dst)
                    if success:
                        if verbose:
                            print(f"  ✓ {src} → {dst}  ({label})")
                        stats["connected"] += 1
                    else:
                        print(f"  ✗ FAILED: {src} → {dst}", file=sys.stderr)
                        stats["failed"] += 1
    return stats

def disconnect_all_carla():
    """Disconnect all Carla-related ports."""
    ports = jack_lsp()
    connections = get_current_connections()
    count = 0
    for src, dst in connections:
        if "Carla" in src or "Carla" in dst:
            if jack_disconnect(src, dst):
                print(f"  ✂ {src} → {dst}")
                count += 1
    print(f"\n  Disconnected {count} Carla connections.")
    return count

def watch_loop(interval: float = 1.0):
    """Watch for new JACK ports and auto-connect them."""
    print("[autoconnect] Watching for JACK port changes... (Ctrl+C to stop)")
    seen_ports = set(jack_lsp())

    while True:
        try:
            time.sleep(interval)
            current_ports = set(jack_lsp())
            new_ports = current_ports - seen_ports

            if new_ports:
                carla_new = {p for p in new_ports if "Carla" in p or "system" in p}
                if carla_new:
                    print(f"\n[autoconnect] New ports detected: {', '.join(sorted(carla_new))}")
                    apply_routing(dry_run=False, verbose=True)

            seen_ports = current_ports
        except KeyboardInterrupt:
            print("\n[autoconnect] Stopped.")
            break
        except Exception as e:
            print(f"[autoconnect] Error: {e}", file=sys.stderr)

# ── CLI ────────────────────────────────────────────────
if __name__ == "__main__":
    if "--watch" in sys.argv:
        watch_loop()
    elif "--dry-run" in sys.argv:
        print("[autoconnect] DRY RUN — no connections made:\n")
        apply_routing(dry_run=True)
    elif "--disconnect-all" in sys.argv:
        disconnect_all_carla()
    else:
        print("[autoconnect] One-shot port connection:\n")
        stats = apply_routing(dry_run=False)
        print(f"\n  Connected: {stats['connected']} | Failed: {stats['failed']} | Skipped: {stats['skipped']}")
