# Copyright (C) 2026 Vijayaraj Paramasivam. Licensed under the GNU GPLv3.
import argparse
import json
import math
import os
import platform
import random
import re
import subprocess
import sys
import time
from datetime import datetime, date
from pathlib import Path
import shutil
import zipfile
import tarfile
import socket
from types import SimpleNamespace

# Constants
YIT_DIR = Path.home() / ".yit"
YIT_BIN = YIT_DIR / "bin"
RESULTS_FILE = YIT_DIR / "results.json"
HISTORY_FILE = YIT_DIR / "history.json"
VIBE_CACHE_FILE = YIT_DIR / "vibe_cache.json"
VIBE_STATE_FILE = YIT_DIR / "vibe_state.json"
VIBE_STATS_FILE = YIT_DIR / "vibe_stats.json"
IPC_PIPE = str(YIT_DIR / "socket") # Unified path name, handled differently by OS

if os.name == 'nt':
    IPC_PIPE = r"\\.\pipe\yit_socket"

def get_mpv_path():
    """Finds MPV or installs it (Windows only)."""
    # 1. Check local bin (Windows priority for portability)
    if os.name == 'nt':
        local_mpv = YIT_BIN / "mpv.exe"
        if local_mpv.exists():
            return str(local_mpv)

    # 2. Check PATH
    if shutil.which("mpv"):
        return "mpv"

    # 3. Not found.
    system = platform.system()
    if system == "Windows":
        print("MPV not found. Downloading portable MPV for Windows...")
        return download_mpv_windows()
    elif system == "Darwin":
        print("MPV is required. Please run: brew install mpv")
        sys.exit(1)
    else:
        print("MPV is required. Please install it (e.g., sudo apt install mpv).")
        sys.exit(1)

def download_mpv_windows():
    if not YIT_BIN.exists(): YIT_BIN.mkdir(parents=True, exist_ok=True)
    
    try:
        import requests
        # Fetch latest release
        print("Fetching latest MPV release info...")
        api_url = "https://api.github.com/repos/shinchiro/mpv-winbuild-cmake/releases/latest"
        resp = requests.get(api_url)
        resp.raise_for_status()
        assets = resp.json().get("assets", [])
        
        url = None
        # Prefer main build (mpv-x86_64...) over dev
        for asset in assets:
            if asset["name"].startswith("mpv-x86_64") and asset["name"].endswith(".7z") and "v3" in asset["name"]:
                url = asset["browser_download_url"]
                break
        
        if not url:
            for asset in assets:
                 if asset["name"].startswith("mpv-x86_64") and asset["name"].endswith(".7z"):
                    url = asset["browser_download_url"]
                    break
        
        if not url:
            raise Exception("No suitable MPV build (mpv-x86_64*.7z) found in latest release.")

        print(f"Downloading {url}...")
        archive_path = YIT_BIN / "mpv.7z"
        with requests.get(url, stream=True) as r:
            r.raise_for_status()
            with open(archive_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192):
                    f.write(chunk)
        
        print("Extracting...")
        print("Extracting (using system tar)...")
        try:
            # Modern Windows (10 build 17063+) has native tar
            subprocess.run(["tar", "-xf", str(archive_path), "-C", str(YIT_BIN)], check=True)
        except Exception as e:
             print(f"Error extracting: {e}")
             print("Please install standard 7-Zip or run 'winget install mpv.mpv'")
             sys.exit(1)
            
        # Flatten: Find mpv.exe and move to YIT_BIN
        found = list(YIT_BIN.rglob("mpv.exe"))
        if not found:
            raise Exception("mpv.exe not found in extracted archive.")
            
        mpv_exe = found[0]
        if mpv_exe.parent != YIT_BIN:
            print(f"Moving {mpv_exe} to {YIT_BIN}...")
            # Move all files from that dir to YIT_BIN to satisfy dependencies? 
            # Usually mpv.exe needs files next to it? No, it's usually portable.
            # But let's verify. Shinchiro builds are usually standalone-ish folders.
            # Let's move the executable.
            shutil.move(str(mpv_exe), str(YIT_BIN / "mpv.exe"))
            
        try:
            os.remove(archive_path)
        except: pass
        
        print("MPV installed successfully.")
        return str(YIT_BIN / "mpv.exe")
        
    except Exception as e:
        print(f"Failed to auto-install MPV: {e}")
        print("Please install it manually via 'winget install mpv.mpv'")
        sys.exit(1)

def ensure_yit_dir():
    if not YIT_DIR.exists():
        YIT_DIR.mkdir()

def _load_json_file(path, default):
    try:
        if not Path(path).exists():
            return default
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return default

def _save_json_file(path, data):
    try:
        ensure_yit_dir()
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)
        return True
    except Exception:
        return False

def _today_iso():
    return date.today().isoformat()

# ----------------------------
# Vibe Engine (Developer Modes)
# ----------------------------

VIBE_MAP = {
    "focus": [
        "lofi coding music",
        "instrumental deep focus music",
        "programming music no lyrics",
    ],
    "debug": [
        "dark synthwave programming",
        "cyberpunk hacker music mix",
        "intense techno coding",
    ],
    "build": [
        "energetic electronic coding music",
        "uplifting trance work mix",
        "high energy coding playlist",
    ],
    "deploy": [
        "epic cinematic victory music",
        "movie soundtrack triumphant",
        "boss fight victory theme",
    ],
    "late-night": [
        "ambient night coding music",
        "deep chill ambient",
        "rainy lofi night",
    ],
    "hackathon": [
        "high bpm edm mix",
        "techno productivity mix",
        "festival edm nonstop",
    ],
}

# Tamil-specific search queries per vibe (only Tamil results when --lang tamil)
VIBE_LANG_QUERIES = {
    "tamil": {
        "focus": ["tamil lofi songs", "tamil lofi bgm", "tamil study music"],
        "debug": ["tamil mass bgm", "tamil villain bgm", "tamil thriller bgm"],
        "build": ["tamil workout songs", "tamil energetic bgm", "tamil dance songs"],
        "deploy": ["tamil victory bgm", "tamil hero elevation bgm", "tamil motivational bgm"],
        "late-night": ["tamil melody songs", "tamil romantic melody", "tamil rain lofi"],
        "hackathon": ["tamil fast beat songs", "tamil kuthu nonstop", "tamil edm remix"],
    },
}

BRANCH_VIBE_RULES = [
    (re.compile(r"^(fix|bug|hotfix)/", re.IGNORECASE), "debug"),
    (re.compile(r"^(feature|feat)/", re.IGNORECASE), "build"),
    (re.compile(r"^(release|deploy)/", re.IGNORECASE), "deploy"),
]

ERROR_KEYWORDS = [
    "error",
    "exception",
    "failed",
    "traceback",
    "panic",
    "segmentation fault",
]

def _normalize_lang(lang):
    if not lang:
        return "en"
    v = str(lang).strip().lower()
    if v in {"any", "all", "*"}:
        return "any"
    if v in {"en", "eng", "english"}:
        return "en"
    if v in {"ta", "tam", "tamil", "தமிழ்"}:
        return "tamil"
    # Unknown: treat as "any" to avoid accidentally filtering everything out.
    return "any"

def _title_has_non_latin_scripts(title):
    """Detect common non-Latin scripts to filter out for English mode."""
    for ch in title:
        o = ord(ch)
        # CJK Unified Ideographs + extensions
        if 0x4E00 <= o <= 0x9FFF or 0x3400 <= o <= 0x4DBF or 0x20000 <= o <= 0x2A6DF:
            return True
        # Hiragana, Katakana
        if 0x3040 <= o <= 0x30FF:
            return True
        # Hangul
        if 0xAC00 <= o <= 0xD7AF:
            return True
        # Cyrillic
        if 0x0400 <= o <= 0x04FF or 0x0500 <= o <= 0x052F:
            return True
        # Arabic
        if 0x0600 <= o <= 0x06FF or 0x0750 <= o <= 0x077F:
            return True
        # Devanagari
        if 0x0900 <= o <= 0x097F:
            return True
        # Thai
        if 0x0E00 <= o <= 0x0E7F:
            return True
        # Hebrew
        if 0x0590 <= o <= 0x05FF:
            return True
    return False

def _title_has_tamil(title):
    for ch in title:
        o = ord(ch)
        # Tamil Unicode block
        if 0x0B80 <= o <= 0x0BFF:
            return True
    return False

def _title_matches_lang(title, lang):
    lang = _normalize_lang(lang)
    if lang == "any":
        return True
    if not title:
        return False
    t = str(title)
    tl = t.lower()
    if lang == "en":
        # Heuristic: keep Latin-ish titles; exclude common non-Latin scripts.
        return not _title_has_non_latin_scripts(t)
    if lang == "tamil":
        # Heuristic: prefer Tamil script or explicit "tamil" marker in title.
        return _title_has_tamil(t) or ("tamil" in tl) or ("தமிழ்" in t)
    return True

def _pick_vibe_query(vibe, lang=None):
    lang_n = _normalize_lang(lang)
    if lang_n in VIBE_LANG_QUERIES and vibe in VIBE_LANG_QUERIES[lang_n]:
        queries = VIBE_LANG_QUERIES[lang_n][vibe]
    else:
        queries = VIBE_MAP.get(vibe) or []
    if not queries:
        return None
    return random.choice(queries)

def _load_vibe_cache():
    return _load_json_file(VIBE_CACHE_FILE, default={})

def _save_vibe_cache(cache):
    _save_json_file(VIBE_CACHE_FILE, cache)

def _cache_key(vibe, limit):
    return f"{vibe}:{int(limit)}"

def _get_cached_results(vibe, limit, ttl_seconds, *, lang="en"):
    if ttl_seconds <= 0:
        return None  # no cache: fresh search every time for different songs
    cache = _load_vibe_cache()
    key = f"{_cache_key(vibe, limit)}:{_normalize_lang(lang)}"
    entry = cache.get(key)
    if not entry:
        return None
    ts = entry.get("ts")
    results = entry.get("results")
    if not ts or not results:
        return None
    try:
        age = time.time() - float(ts)
        if age > ttl_seconds:
            return None
    except Exception:
        return None
    return results

def _set_cached_results(vibe, limit, results, *, lang="en"):
    cache = _load_vibe_cache()
    key = f"{_cache_key(vibe, limit)}:{_normalize_lang(lang)}"
    cache[key] = {"ts": time.time(), "results": results}
    _save_vibe_cache(cache)

def _load_vibe_state():
    return _load_json_file(VIBE_STATE_FILE, default={"vibe": None, "updated_at": None})

def _set_vibe_state(vibe):
    _save_json_file(VIBE_STATE_FILE, {"vibe": vibe, "updated_at": datetime.utcnow().isoformat() + "Z"})

def _load_stats():
    return _load_json_file(
        VIBE_STATS_FILE,
        default={
            "xp": 0,
            "sessions": 0,
            "streak": 0,
            "last_session_date": None,
            "vibe_counts": {},
        },
    )

def _save_stats(stats):
    _save_json_file(VIBE_STATS_FILE, stats)

def _record_vibe_session(vibe):
    stats = _load_stats()
    today = _today_iso()

    last = stats.get("last_session_date")
    streak = int(stats.get("streak") or 0)
    if last != today:
        if last:
            try:
                last_d = date.fromisoformat(last)
                if (date.fromisoformat(today) - last_d).days == 1:
                    streak += 1
                else:
                    streak = 1
            except Exception:
                streak = 1
        else:
            streak = 1

    stats["streak"] = streak
    stats["last_session_date"] = today
    stats["sessions"] = int(stats.get("sessions") or 0) + 1
    stats["xp"] = int(stats.get("xp") or 0) + 10

    counts = stats.get("vibe_counts") or {}
    counts[vibe] = int(counts.get(vibe) or 0) + 1
    stats["vibe_counts"] = counts

    _save_stats(stats)
    return stats

def _print_victory_fx():
    # Lightweight "terminal victory moment" (no external deps).
    banner = r"""
 __     ___ _          __  __           _
 \ \   / (_) |__   ___|  \/  |_   _ ___(_) ___
  \ \ / /| | '_ \ / _ \ |\/| | | | / __| |/ __|
   \ V / | | |_) |  __/ |  | | |_| \__ \ | (__
    \_/  |_|_.__/ \___|_|  |_|\__,_|___/_|\___|
"""
    confetti = ["*  *   *", "  * * * ", "*   *  *", " *  *   "]
    print(banner)
    for _ in range(6):
        print("  ".join(random.choice(confetti) for _ in range(3)))
    print("Ship it. Your code is a movie scene now.\n")

def _git_branch(repo_path="."):
    try:
        result = subprocess.run(
            ["git", "-C", str(repo_path), "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode != 0:
            return None
        branch = (result.stdout or "").strip()
        if not branch or branch == "HEAD":
            return None
        return branch
    except Exception:
        return None

def _vibe_from_branch(branch):
    if not branch:
        return None
    for pattern, vibe in BRANCH_VIBE_RULES:
        if pattern.search(branch):
            return vibe
    # Common branches that *feel* like deploy moments, but don't force it unless user asks.
    if branch in {"main", "master"}:
        return "focus"
    return None

def _is_late_night(now=None, night_start=23, night_end=6):
    # night_start: inclusive hour, night_end: exclusive hour (wraps midnight)
    now = now or datetime.now()
    h = now.hour
    if night_start < night_end:
        return night_start <= h < night_end
    return (h >= night_start) or (h < night_end)

def _determine_auto_vibe(repo_path=".", night_start=23, night_end=6, allow_branch_override_at_night=False):
    if _is_late_night(night_start=night_start, night_end=night_end) and not allow_branch_override_at_night:
        return "late-night"
    branch = _git_branch(repo_path=repo_path)
    by_branch = _vibe_from_branch(branch)
    if by_branch:
        return by_branch
    if _is_late_night(night_start=night_start, night_end=night_end):
        return "late-night"
    return "focus"



def save_to_history(track):
    """Saves a track to the persistent history file."""
    ensure_yit_dir()
    history = []
    if HISTORY_FILE.exists():
        try:
            with open(HISTORY_FILE, "r") as f:
                history = json.load(f)
        except Exception:
            pass 

    # Check for duplicates or update
    existing = None
    for item in history:
        if item.get("url") == track["url"]:
            existing = item
            break
            
    if not existing:
        history.append(track)
        
    try:
        with open(HISTORY_FILE, "w") as f:
            json.dump(history, f, indent=4)
    except Exception as e:
        print(f"Warning: Could not save history: {e}")

class SocketWrapper:
    """Wraps a socket to behave like a file object (read/write/flush)."""
    def __init__(self, sock):
        self.sock = sock
        self.r_file = sock.makefile("rb", buffering=0)
        self.w_file = sock.makefile("wb", buffering=0)

    def write(self, data):
        self.w_file.write(data)

    def flush(self):
        self.w_file.flush()

    def readline(self):
        return self.r_file.readline()

    def close(self):
        try:
            self.r_file.close()
            self.w_file.close()
            self.sock.close()
        except: pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

def connect_ipc():
    """Connects to the MPV IPC."""
    if os.name == 'nt':
        return open(IPC_PIPE, "r+b", buffering=0)
    else:
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.connect(IPC_PIPE)
        return SocketWrapper(sock)

def send_ipc_command(command):
    """Sends a JSON-formatted command to the MPV IPC pipe."""
    try:
        with connect_ipc() as f:
            payload = json.dumps(command).encode("utf-8") + b"\n"
            f.write(payload)
            f.flush() # Essential for socket
            response_line = f.readline().decode("utf-8")
            if response_line:
                return json.loads(response_line)
            return {"error": "no_response"}
    except (FileNotFoundError, ConnectionRefusedError, OSError):
        # On Linux, OSError might be "Connection refused" or "No such file"
        return None
    except Exception as e:
        print(f"Error communicating with player: {e}")
        return None

def get_ipc_property(prop):
    """Gets a property from MPV."""
    try:
        with connect_ipc() as f:
            cmd = {"command": ["get_property", prop]}
            payload = json.dumps(cmd).encode("utf-8") + b"\n"
            f.write(payload)
            f.flush()
            
            # Simple read line
            response = f.readline().decode("utf-8")
            return json.loads(response)
    except (FileNotFoundError, ConnectionRefusedError, OSError):
        return None
    except Exception:
        return None

def _search_youtube(query, limit=5, *, lang=None):
    """
    Returns a list of results: [{"title": str, "url": str}, ...]
    Uses yt-dlp subprocess for speed and portability.
    """
    ensure_yit_dir()
    query = (query or "").strip()
    if not query:
        return []

    limit = int(limit) if limit else 5
    limit = max(1, min(limit, 25))

    command = [
        "yt-dlp",
        "--print",
        "%(title)s||||%(webpage_url)s",
        "--flat-playlist",
        f"ytsearch{limit}:{query}",
    ]

    result = subprocess.run(
        command,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
        check=False,
    )
    if result.returncode != 0:
        raise RuntimeError(f"yt-dlp failed ({result.returncode}): {result.stderr}")

    lines = (result.stdout or "").strip().split("\n")
    lang = _normalize_lang(lang)
    results = []
    for line in lines:
        if "||||" not in line:
            continue
        title, url = line.split("||||", 1)
        title = (title or "").strip()
        url = (url or "").strip()
        if not url:
            continue
        item = {"title": title or url, "url": url}
        if not _title_matches_lang(item["title"], lang):
            continue
        results.append(item)
    return results

def cmd_search(args):
    """Searches YouTube and stores results."""
    ensure_yit_dir()
    query = " ".join(args.query)
    print(f"Searching for '{query}'...")

    try:
        results = _search_youtube(query, limit=5)
        print("\nResults:")
        for i, item in enumerate(results):
            print(f"{i+1}. {item['title']}")
        
        if not results:
            print("No results found.")
            return

        with open(RESULTS_FILE, "w") as f:
            json.dump(results, f, indent=4)

    except subprocess.CalledProcessError as e:
        print(f"Error searching: {e}")
    except RuntimeError as e:
        print(f"Error searching: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}\nTry running the setup_installer.bat")

    if args.play and results:
        print("\nAuto-playing result #1...")
        cmd_play(SimpleNamespace(number=1))

def cmd_play(args):
    """Plays the selected track number."""
    if not RESULTS_FILE.exists():
        print("No search results found. Run 'yit search <query>' first.")
        return

    try:
        with open(RESULTS_FILE, "r") as f:
            results = json.load(f)
        
        idx = args.number - 1
        if idx < 0 or idx >= len(results):
            print("Invalid selection number.")
            return

        track = results[idx]
        print(f"Playing: {track['title']}")
        save_to_history(track)
        
        # Check if running
        is_running = send_ipc_command({"command": ["loadfile", track["url"]]})
        
        if is_running:
             print("Added to existing player.")
             # Ensure it plays immediately even if previously paused
             send_ipc_command({"command": ["set_property", "pause", False]})
        else:
            # Spawn new
            mpv_exe = get_mpv_path()
            cmd = [
                mpv_exe,
                "--no-video",
                "--idle",
                "--cache=yes",
                "--prefetch-playlist=yes",
                "--demuxer-max-bytes=128M",
                "--demuxer-max-back-bytes=128M",
                f"--input-ipc-server={IPC_PIPE}",
                track["url"]
            ]
            # Prepare env with yt-dlp in path
            env = os.environ.copy()
            yt_dlp_path = Path(sys.executable).parent
            env["PATH"] = str(yt_dlp_path) + os.pathsep + env["PATH"]

            # Prepare subprocess args based on OS
            kwargs = {
                "stdout": subprocess.DEVNULL,
                "stderr": subprocess.DEVNULL,
                "env": env,
                "close_fds": True
            }

            if os.name == 'nt':
                kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
            else:
                kwargs["start_new_session"] = True

            # Detach process
            subprocess.Popen(cmd, **kwargs)
            print("Player started in background.")

    except Exception as e:
        print(f"Error playing: {e}")

def cmd_pause(args):
    send_ipc_command({"command": ["set_property", "pause", True]})
    print("Paused.")

def cmd_resume(args):
    send_ipc_command({"command": ["set_property", "pause", False]})
    print("Resumed.")
    
def cmd_toggle(args):
    send_ipc_command({"command": ["cycle", "pause"]})
    print("Toggled playback.")

def cmd_stop(args):
    send_ipc_command({"command": ["quit"]})
    print("Stopped.")

def cmd_loop(args):
    send_ipc_command({"command": ["set_property", "loop-file", "inf"]})
    print("Looping current track.")

def cmd_unloop(args):
    send_ipc_command({"command": ["set_property", "loop-file", "no"]})
    print("Unlooped. Playback will continue normally.")

def cmd_add(args):
    """Appends the selected track number to the queue."""
    if not RESULTS_FILE.exists():
        print("No search results found. Run 'yit search <query>' first.")
        return

    try:
        with open(RESULTS_FILE, "r") as f:
            results = json.load(f)
        
        idx = args.number - 1
        if idx < 0 or idx >= len(results):
            print("Invalid selection number.")
            return

        track = results[idx]
        print(f"Adding to queue: {track['title']}")
        save_to_history(track)
        
        # Determine if we need to spawn mpv or just append
        # Try to append first
        res = send_ipc_command({"command": ["loadfile", track["url"], "append-play"]})
        
        if not res or res.get("error") != "success":
            # If not running, play normally (which spawns)
            print("Player not running (or append failed), starting new queue...")
            cmd_play(args) # This will spawn it
        else:
            print("Added to queue.")

    except Exception as e:
        print(f"Error adding to queue: {e}")

def cmd_next(args):
    send_ipc_command({"command": ["playlist-next"]})
    print("Skipping to next track...")

def cmd_prev(args):
    send_ipc_command({"command": ["playlist-prev"]})
    print("Going to previous track...")

def cmd_restart(args):
    send_ipc_command({"command": ["seek", 0, "absolute"]})
    send_ipc_command({"command": ["set_property", "pause", False]})
    print("Restarting current track...")

def cmd_clear(args):
    send_ipc_command({"command": ["playlist-clear"]})
    print("Queue cleared.")

def extract_video_id(url):
    """Extracts YouTube Video ID from URL."""
    if not url: return None
    # Standard v= parameter
    if "v=" in url:
        try:
            return url.split("v=")[1].split("&")[0][:11] # ID is 11 chars
        except:
            pass
    # Shortened youtu.be/ID
    if "youtu.be/" in url:
        try:
            return url.split("youtu.be/")[1][:11]
        except:
            pass
    return None

def cmd_queue(args):
    # Get playlist info
    resp = get_ipc_property("playlist")
    if not resp or resp.get("error") != "success":
        print("Queue is empty (or player not running).")
        return

    playlist = resp.get("data", [])
    if not playlist:
        print("Queue is empty.")
        return
    
    # Load local results to resolve titles if missing in MPV
    # Map both full URL and Video ID to title
    url_map = {}
    id_map = {}
    
    # Helper to load a list of items into the maps
    def load_into_maps(items):
        for item in items:
            url = item["url"].strip("| ")
            title = item["title"]
            url_map[url] = title
            vid = extract_video_id(url)
            if vid:
                id_map[vid] = title

    # 1. Load Results (Current Search)
    if RESULTS_FILE.exists():
        try:
            with open(RESULTS_FILE, "r") as f:
                load_into_maps(json.load(f))
        except Exception: pass

    # 2. Load History (Persistent Cache)
    if HISTORY_FILE.exists():
        try:
            with open(HISTORY_FILE, "r") as f:
                load_into_maps(json.load(f))
        except Exception: pass

    print("\nCurrent Queue:")
    for i, item in enumerate(playlist):
        prefix = "-> " if item.get("current") else "   "
        
        # Priority: MPV Title -> Local Cache (ID) -> Local Cache (URL) -> Filename -> Unknown
        title = item.get("title")
        
        if not title:
            # MPV 'filename' field holds the URL
            url = item.get("filename", "")
            
            # Try exact match
            if url in url_map:
                title = url_map[url]
            else:
                # Try ID match
                vid = extract_video_id(url)
                if vid and vid in id_map:
                    title = id_map[vid]
                else:
                    title = url or "Unknown"

        print(f"{prefix}{i+1}. {title}")

def cmd_status(args):
    resp = get_ipc_property("media-title")
    if resp and resp.get("error") == "success" and resp.get("data"):
        title = resp.get("data")
        
        # Check pause status
        paused = get_ipc_property("pause")
        looping = get_ipc_property("loop-file")

        status_str = "[Paused]" if paused and paused.get("data") else "[Playing]"
        
        if looping and looping.get("data") in ["inf", "yes"]:
            status_str += " [Looped]"
            
        print(f"{status_str} {title}")
    else:
        # Check if running at least
        if get_ipc_property("idle-active"):
            print("Queue is empty.")
        else:
            print("Vibe Music player is not running.")

def _format_bar(value, max_value=100, width=24, fill="#", empty="-"):
    try:
        v = float(value or 0.0)
    except Exception:
        v = 0.0
    v = max(0.0, min(v, float(max_value)))
    filled = int(width * (v / float(max_value))) if max_value else 0
    return fill * filled + empty * (width - filled)

# Unicode block characters for wave bars (low to high)
_WAVE_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"  # ▁▂▃▄▅▆▇█

def _banner_wave(phase=0.0, num_bars=14):
    """Static wave line for the banner (phase fixed per call)."""
    out = []
    for i in range(num_bars):
        v = 0.5 * math.sin(phase + i / 2.0) + 0.4 * math.sin(phase * 1.2 + i / 2.8)
        v = (v + 1.0) / 2.0
        idx = max(0, min(7, int(v * 7.999)))
        out.append(_WAVE_CHARS[idx])
    return "".join(out)

def _print_banner():
    """Print waves + Vibe Music branding (only when stdout is a TTY)."""
    if not sys.stdout.isatty():
        return
    left = _banner_wave(0.0, 14)
    right = _banner_wave(3.0, 14)
    sys.stdout.write("  " + left + "  Vibe Music  " + right + "\n\n")
    sys.stdout.flush()

def _wave_line(time_pos, num_bars=48, speed=2.5, width=2.0):
    """One line of wave bars; phase advances with time_pos so wave syncs with song."""
    try:
        t = float(time_pos or 0.0)
    except Exception:
        t = 0.0
    phase = t * speed
    out = []
    for i in range(num_bars):
        # Two overlapping waves for a fuller look; phase tied to playback time
        v = 0.5 * math.sin(phase + i / width) + 0.4 * math.sin(phase * 1.3 + i / 3.0)
        v = (v + 1.0) / 2.0  # 0..1
        idx = int(v * 7.999)
        idx = max(0, min(7, idx))
        out.append(_WAVE_CHARS[idx])
    return "".join(out)

def cmd_visual(args):
    """Wave visualizer: runs only while a track is playing; waves sync with playback time."""
    title_resp = get_ipc_property("media-title")
    if not title_resp or title_resp.get("error") != "success" or not title_resp.get("data"):
        if get_ipc_property("idle-active"):
            print("No active track. Start a vibe or playback first.")
        else:
            print("Vibe Music player is not running.")
        return

    title = title_resp.get("data")
    print(f"  {title}")
    print("  Waves sync with playback. Press Ctrl+C to exit.\n")

    fail_count = 0
    try:
        while True:
            idle_resp = get_ipc_property("idle-active")
            time_resp = get_ipc_property("time-pos")
            dur_resp = get_ipc_property("duration")
            pause_resp = get_ipc_property("pause")

            # Stop when no track (idle) or player gone
            if idle_resp and idle_resp.get("data"):
                sys.stdout.write("\r  Stopped. No track playing.\n")
                sys.stdout.flush()
                break
            if time_resp is None:
                fail_count += 1
                if fail_count >= 5:
                    sys.stdout.write("\r  Stopped. Player disconnected.\n")
                    sys.stdout.flush()
                    break
                time.sleep(0.1)
                continue
            fail_count = 0

            pos = time_resp.get("data") if time_resp else None
            if time_resp.get("error") != "success":
                time.sleep(0.1)
                continue

            try:
                pos = float(pos or 0.0)
            except (TypeError, ValueError):
                pos = 0.0

            dur = dur_resp.get("data") if dur_resp and dur_resp.get("error") == "success" else None
            try:
                dur_f = float(dur) if dur is not None else 0.0
            except (TypeError, ValueError):
                dur_f = 0.0

            # When track ends (position near or past duration), stop
            if dur_f > 0 and pos >= dur_f - 0.3:
                sys.stdout.write("\r  Stopped. Track ended.\n")
                sys.stdout.flush()
                break

            paused = pause_resp and pause_resp.get("data") is True
            # Wave phase tied to time_pos so wave syncs with song progress
            wave = _wave_line(pos, num_bars=52, speed=2.8, width=2.2)
            status = " [Paused] " if paused else " [Playing] "
            sys.stdout.write("\r  " + wave + status + f" {pos:.1f}s / {dur_f:.1f}s   ")
            sys.stdout.flush()
            time.sleep(0.08)
    except KeyboardInterrupt:
        sys.stdout.write("\n")
        sys.stdout.flush()
            
def cmd_agent(args):
    """Outputs full player state as JSON for AI agents."""
    state = {
        "status": "stopped",
        "track": {},
        "position": 0,
        "duration": 0,
        "volume": 0,
        "loop": False,
        "queue_length": 0
    }

    # Check if running
    idle_resp = get_ipc_property("idle-active")
    if not idle_resp:
        print(json.dumps(state, indent=2))
        return

    # Gather data sequentially
    pause_resp = get_ipc_property("pause")
    title_resp = get_ipc_property("media-title")
    path_resp = get_ipc_property("path") # often URL
    time_resp = get_ipc_property("time-pos")
    dur_resp = get_ipc_property("duration")
    vol_resp = get_ipc_property("volume")
    loop_resp = get_ipc_property("loop-file")
    playlist_resp = get_ipc_property("playlist-count")

    # Process Status
    if pause_resp and pause_resp.get("data") is True:
        state["status"] = "paused"
    elif pause_resp and pause_resp.get("data") is False:
        state["status"] = "playing"

    # Process Track Info
    if title_resp and title_resp.get("data"):
        state["track"]["title"] = title_resp["data"]
    if path_resp and path_resp.get("data"):
        state["track"]["url"] = path_resp["data"]

    # Playback Info
    if time_resp and time_resp.get("data"):
        state["position"] = time_resp["data"]
    if dur_resp and dur_resp.get("data"):
        state["duration"] = dur_resp["data"]
    if vol_resp and vol_resp.get("data"):
        state["volume"] = vol_resp["data"]
    
    # Loop Status
    if loop_resp and loop_resp.get("data") in ["inf", "yes"]:
        state["loop"] = True
    
    # Queue Info
    if playlist_resp and playlist_resp.get("data"):
        state["queue_length"] = playlist_resp["data"]

    print(json.dumps(state, indent=2))

def cmd_commands(args):
    """Outputs available commands as JSON for AI agents."""
    cmds = [
        {"cmd": "search", "usage": "vibe search <query> [-p]", "desc": "Search YouTube. -p to auto-play."},
        {"cmd": "play", "usage": "vibe play <index>", "desc": "Play a track from results."},
        {"cmd": "add", "usage": "vibe add <index>", "desc": "Add a track to queue."},
        {"cmd": "pause", "usage": "vibe pause", "desc": "Pause playback."},
        {"cmd": "resume", "usage": "vibe resume", "desc": "Resume playback."},
        {"cmd": "stop", "usage": "vibe stop", "desc": "Stop playback completely."},
        {"cmd": "next", "usage": "vibe next", "desc": "Skip to next track."},
        {"cmd": "back", "usage": "vibe back", "desc": "Go to previous track."},
        {"cmd": "loop", "usage": "vibe loop", "desc": "Loop current track indefinitely."},
        {"cmd": "unloop", "usage": "vibe unloop", "desc": "Stop looping."},
        {"cmd": "queue", "usage": "vibe queue", "desc": "Show current queue."},
        {"cmd": "clear", "usage": "vibe clear", "desc": "Clear the queue."},
        {"cmd": "status", "usage": "vibe status", "desc": "Show playback status (text)."},
        {"cmd": "agent", "usage": "vibe agent", "desc": "Get full system state (JSON)."},
        {"cmd": "commands", "usage": "vibe commands", "desc": "Get this list (JSON)."},
        {"cmd": "vibe", "usage": "vibe <mode>", "desc": "Adaptive vibe engine (focus/debug/build/deploy/late-night/hackathon/auto)."},
        {"cmd": "hooks", "usage": "vibe hooks install", "desc": "Install optional git hooks for branch/push-triggered vibes."},
        {"cmd": "0", "usage": "vibe 0", "desc": "Replay current track."}
    ]
    print(json.dumps(cmds, indent=2))

def _wait_for_ipc_ready(max_seconds=3.0):
    deadline = time.time() + float(max_seconds)
    while time.time() < deadline:
        resp = get_ipc_property("idle-active")
        if resp is not None:
            return True
        time.sleep(0.15)
    return False

def _mpv_running():
    return get_ipc_property("idle-active") is not None

def _spawn_mpv_detached(url):
    mpv_exe = get_mpv_path()
    cmd = [
        mpv_exe,
        "--no-video",
        "--idle",
        "--cache=yes",
        "--prefetch-playlist=yes",
        "--demuxer-max-bytes=128M",
        "--demuxer-max-back-bytes=128M",
        f"--input-ipc-server={IPC_PIPE}",
        url,
    ]

    env = os.environ.copy()
    # Ensure venv scripts dir is on PATH for yt-dlp resolver inside mpv.
    env["PATH"] = str(Path(sys.executable).parent) + os.pathsep + env.get("PATH", "")

    kwargs = {
        "stdout": subprocess.DEVNULL,
        "stderr": subprocess.DEVNULL,
        "env": env,
        "close_fds": True,
    }
    if os.name == "nt":
        kwargs["creationflags"] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        kwargs["start_new_session"] = True

    subprocess.Popen(cmd, **kwargs)

def _play_results_as_playlist(results, *, shuffle=True, clear=True, quiet=False):
    """
    Plays results as an mpv playlist:
    - If mpv is running, clears + replaces playlist.
    - Else spawns mpv detached and then appends remainder.
    """
    if not results:
        return False

    results = list(results)
    if shuffle:
        random.shuffle(results)

    _save_json_file(RESULTS_FILE, results)
    for track in results:
        try:
            save_to_history(track)
        except Exception:
            pass

    first = results[0]["url"]
    rest = [r["url"] for r in results[1:]]

    if _mpv_running():
        if clear:
            send_ipc_command({"command": ["playlist-clear"]})
        send_ipc_command({"command": ["loadfile", first, "replace"]})
        for url in rest:
            send_ipc_command({"command": ["loadfile", url, "append-play"]})
        send_ipc_command({"command": ["set_property", "pause", False]})
        if not quiet:
            print("Vibe synced to existing player.")
        return True

    _spawn_mpv_detached(first)
    if not _wait_for_ipc_ready(max_seconds=4.0):
        if not quiet:
            print("Player started, but IPC not ready yet.")
        return True

    for url in rest:
        send_ipc_command({"command": ["loadfile", url, "append-play"]})
    send_ipc_command({"command": ["set_property", "pause", False]})
    if not quiet:
        print("Player started in background (vibe mode).")
    return True

def _start_vibe(vibe, *, limit=5, shuffle=True, cache_ttl_seconds=0, quiet=False, victory=False, lang="en"):
    vibe = (vibe or "").strip().lower()
    if vibe not in VIBE_MAP:
        raise ValueError(f"Unknown vibe: {vibe}")

    if victory and vibe == "deploy" and not quiet:
        _print_victory_fx()

    _set_vibe_state(vibe)
    stats = _record_vibe_session(vibe)

    cached = _get_cached_results(vibe, limit, ttl_seconds=cache_ttl_seconds, lang=lang)
    if cached:
        results = cached
        if not quiet:
            print(f"Vibe '{vibe}' (cached). XP={stats.get('xp')} Streak={stats.get('streak')}")
        return _play_results_as_playlist(results, shuffle=shuffle, clear=True, quiet=quiet)

    query = _pick_vibe_query(vibe, lang=lang)
    if not query:
        if not quiet:
            print(f"No query configured for vibe '{vibe}'.")
        return False

    if not quiet:
        print(f"Vibe '{vibe}' → searching: {query}")
    lang_n = _normalize_lang(lang)
    # For Tamil use query as-is (already Tamil-specific); for English add "english"; else use query.
    if lang_n == "en":
        q = f"{query} english"
    elif lang_n == "tamil":
        q = query  # already from VIBE_LANG_QUERIES["tamil"] or append tamil
        if "tamil" not in q.lower():
            q = f"{query} tamil"
    else:
        q = query

    # Fetch more when filtering by language so we still get enough after filtering.
    fetch_n = 25 if lang_n == "tamil" else min(25, max(limit, limit * 4))
    results = _search_youtube(q, limit=fetch_n, lang=lang_n)
    # Keep only up to limit; all results are already language-filtered.
    results = results[:limit]
    if not results:
        if not quiet:
            print("No results found for vibe query.")
        return False

    if cache_ttl_seconds > 0:
        _set_cached_results(vibe, limit, results, lang=lang)
    return _play_results_as_playlist(results, shuffle=shuffle, clear=True, quiet=quiet)

def cmd_vibe_mode(args):
    _start_vibe(
        args.vibe,
        limit=args.limit,
        shuffle=(not args.no_shuffle),
        cache_ttl_seconds=args.cache_ttl,
        quiet=args.quiet,
        victory=getattr(args, "victory", False),
        lang=args.lang,
    )

def cmd_vibe_branch(args):
    branch = _git_branch(repo_path=args.repo)
    vibe = _vibe_from_branch(branch) or args.fallback
    if not args.quiet:
        if branch:
            print(f"Branch '{branch}' → vibe '{vibe}'")
        else:
            print(f"No git branch detected → vibe '{vibe}'")
    _start_vibe(
        vibe,
        limit=args.limit,
        shuffle=(not args.no_shuffle),
        cache_ttl_seconds=args.cache_ttl,
        quiet=args.quiet,
        lang=args.lang,
    )

def cmd_vibe_auto(args):
    last = None
    while True:
        vibe = _determine_auto_vibe(
            repo_path=args.repo,
            night_start=args.night_start,
            night_end=args.night_end,
            allow_branch_override_at_night=args.allow_branch_override_at_night,
        )
        if vibe != last:
            if not args.quiet:
                print(f"Auto vibe → {vibe}")
            _start_vibe(
                vibe,
                limit=args.limit,
                shuffle=(not args.no_shuffle),
                cache_ttl_seconds=args.cache_ttl,
                quiet=args.quiet,
                lang=args.lang,
            )
            last = vibe
            if args.once:
                return
        time.sleep(args.interval)

def cmd_vibe_card(args):
    stats = _load_stats()
    state = _load_vibe_state()
    vibe = state.get("vibe") or "unknown"
    payload = {
        "vibe": vibe,
        "xp": stats.get("xp", 0),
        "sessions": stats.get("sessions", 0),
        "streak": stats.get("streak", 0),
        "vibe_counts": stats.get("vibe_counts", {}),
        "updated_at": state.get("updated_at"),
    }
    if args.json:
        print(json.dumps(payload, indent=2))
        return

    lines = [
        "┌───────────────────────────────┐",
        "│         DEV VIBE CARD         │",
        "├───────────────────────────────┤",
        f"│ vibe:   {vibe:<22} │",
        f"│ xp:     {str(payload['xp']):<22} │",
        f"│ streak: {str(payload['streak']):<22} │",
        f"│ runs:   {str(payload['sessions']):<22} │",
        "└───────────────────────────────┘",
    ]
    print("\n".join(lines))

def cmd_vibe_watch(args):
    if not args.cmd:
        print("Usage: yit vibe watch -- <command>")
        return
    cmd = args.cmd
    if cmd and cmd[0] == "--":
        cmd = cmd[1:]
    if not cmd:
        print("Usage: yit vibe watch -- <command>")
        return

    if not args.quiet:
        print(f"Watching command output for errors → vibe '{args.error_vibe}'")

    proc = subprocess.Popen(
        cmd,
        cwd=str(args.cwd) if args.cwd else None,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1,
        universal_newlines=True,
    )

    triggered = False
    try:
        for line in proc.stdout or []:
            if not args.no_passthrough:
                print(line, end="")
            lower = (line or "").lower()
            if not triggered and any(k in lower for k in ERROR_KEYWORDS):
                triggered = True
                if not args.quiet:
                    print("\n⚠ Detected error-like output → switching vibe.\n")
                _start_vibe(
                    args.error_vibe,
                    limit=args.limit,
                    shuffle=(not args.no_shuffle),
                    cache_ttl_seconds=args.cache_ttl,
                    quiet=args.quiet,
                    lang=args.lang,
                )
    finally:
        rc = proc.wait()
    if (rc == 0) and args.on_success:
        _start_vibe(
            args.on_success,
            limit=args.limit,
            shuffle=(not args.no_shuffle),
            cache_ttl_seconds=args.cache_ttl,
            quiet=args.quiet,
            lang=args.lang,
        )
    sys.exit(rc)

def _hooks_dir(repo_path="."):
    return Path(repo_path) / ".git" / "hooks"

def _write_hook(path, content):
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    try:
        os.chmod(path, 0o755)
    except Exception:
        pass

def cmd_hooks_install(args):
    hooks_path = _hooks_dir(args.repo)
    if not hooks_path.exists():
        print(f"No .git/hooks found at: {hooks_path}")
        print("Run this inside a git repo (or pass --repo /path/to/repo).")
        return

    # Keep hooks fast: run in background and rely on cached vibe results.
    sh_bg = '(\n  vibe branch --repo "$(pwd)" --quiet --limit 5 >/dev/null 2>&1 &\n) >/dev/null 2>&1 || true\n'
    post_checkout = "#!/bin/sh\n# Auto-vibe on branch change (post-checkout)\n" + sh_bg
    post_merge = "#!/bin/sh\n# Auto-vibe on merge (post-merge)\n" + sh_bg

    pre_push = r"""#!/bin/sh
# Auto-vibe on push (pre-push) — triggers deploy vibe for main/master/release
remote="$1"
url="$2"

deploy=0
while read local_ref local_sha remote_ref remote_sha
do
  case "$remote_ref" in
    refs/heads/main|refs/heads/master|refs/heads/release* )
      deploy=1
      ;;
  esac
done

if [ "$deploy" -eq 1 ]; then
  echo ""
  echo "=============================="
  echo "        DEPLOY VIBE"
  echo "  cinematic drop incoming..."
  echo "=============================="
  echo " *   *  *   *   *  *   *"
  echo "   *   *   *   *   *   "
  echo " *   *  *   *   *  *   *"
  echo ""
  (
    vibe deploy --quiet --limit 5 >/dev/null 2>&1 &
  ) >/dev/null 2>&1 || true
else
  (
    vibe branch --repo "$(pwd)" --quiet --limit 5 >/dev/null 2>&1 &
  ) >/dev/null 2>&1 || true
fi

exit 0
"""

    _write_hook(hooks_path / "post-checkout", post_checkout)
    _write_hook(hooks_path / "post-merge", post_merge)
    _write_hook(hooks_path / "pre-push", pre_push)

    print("Installed git hooks:")
    print(f"- {hooks_path / 'post-checkout'}")
    print(f"- {hooks_path / 'post-merge'}")
    print(f"- {hooks_path / 'pre-push'}")

def cmd_hooks_uninstall(args):
    hooks_path = _hooks_dir(args.repo)
    removed = []
    for name in ["post-checkout", "post-merge", "pre-push"]:
        p = hooks_path / name
        try:
            if p.exists():
                p.unlink()
                removed.append(str(p))
        except Exception:
            pass
    if removed:
        print("Removed hooks:")
        for p in removed:
            print(f"- {p}")
    else:
        print("No hooks removed (none found or insufficient permissions).")

def main():
    # Allow quick flags (developer muscle memory)
    # Examples: yit -n (next), yit -b (back), yit -q (queue)
    argv = sys.argv[1:]
    if len(argv) == 1:
        quick = {
            "-n": ["next"],
            "--next": ["next"],
            "-b": ["back"],
            "--back": ["back"],
            "-q": ["queue"],
            "--queue": ["queue"],
            "-c": ["clear"],
            "--clear": ["clear"],
            "-s": ["stop"],
            "--stop": ["stop"],
            "-t": ["toggle"],
            "--toggle": ["toggle"],
            "-r": ["resume"],
            "--resume": ["resume"],
            "-p": ["pause"],   # only when used as the *only* argument (doesn't affect `search -p`)
            "--pause": ["pause"],
            "-0": ["replay"],
        }
        if argv[0] in quick:
            sys.argv = [sys.argv[0]] + quick[argv[0]]
            argv = sys.argv[1:]

    # Allow direct vibe commands: `vibe focus` -> `vibe vibe focus`
    if argv:
        top = argv[0]
        vibe_entrypoints = {
            "focus",
            "debug",
            "build",
            "deploy",
            "late-night",
            "hackathon",
            "auto",
            "branch",
            "card",
            "watch",
            "visual",
        }
        if top in vibe_entrypoints:
            sys.argv = [sys.argv[0], "vibe"] + argv

    # Show waves + Vibe Music banner for every command (when running in a terminal)
    _print_banner()

    # Ensure MPV is available (auto-install on Windows if needed)
    # checking this once here avoids checks later, although get_mpv_path caches fairly well?
    # get_mpv_path checks file existence, which is fast.
    try:
        get_mpv_path()
    except SystemExit:
        return # already printed error

    parser = argparse.ArgumentParser(description="Vibe Music - Adaptive coding atmosphere engine (CLI)")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # Search
    parser_search = subparsers.add_parser("search", help="Search YouTube")
    parser_search.add_argument("query", nargs="+", help="Search query")
    parser_search.add_argument("-p", "--play", action="store_true", help="Auto-play the first result")
    parser_search.set_defaults(func=cmd_search)

    # Play
    parser_play = subparsers.add_parser("play", help="Play a song by number")
    parser_play.add_argument("number", type=int, help="Track number from search results")
    parser_play.set_defaults(func=cmd_play)

    # Controls
    parser_pause = subparsers.add_parser("pause", aliases=["p"], help="Pause playback")
    parser_pause.set_defaults(func=cmd_pause)

    parser_resume = subparsers.add_parser("resume", aliases=["r"], help="Resume playback")
    parser_resume.set_defaults(func=cmd_resume)
    
    parser_toggle = subparsers.add_parser("toggle", help="Toggle pause/resume")
    parser_toggle.set_defaults(func=cmd_toggle)

    parser_stop = subparsers.add_parser("stop", help="Stop playback")
    parser_stop.set_defaults(func=cmd_stop)

    # Add to queue
    parser_add = subparsers.add_parser("add", help="Add song to queue")
    parser_add.add_argument("number", type=int, help="Track number")
    parser_add.set_defaults(func=cmd_add)

    # Queue management
    parser_queue = subparsers.add_parser("queue", help="Show queue")
    parser_queue.set_defaults(func=cmd_queue)

    parser_clear = subparsers.add_parser("clear", help="Clear queue")
    parser_clear.set_defaults(func=cmd_clear)

    # Fast Navigation (Safe Aliases)
    # Next
    parser_next = subparsers.add_parser("next", aliases=["n"], help="Next track")
    parser_next.set_defaults(func=cmd_next)

    # Previous (Back)
    parser_prev = subparsers.add_parser("back", aliases=["b"], help="Previous track")
    parser_prev.set_defaults(func=cmd_prev)

    # Replay/Restart
    parser_restart = subparsers.add_parser("replay", aliases=["0"], help="Replay current track")
    parser_restart.set_defaults(func=cmd_restart)

    parser_status = subparsers.add_parser("status", help="Show status")
    parser_status.set_defaults(func=cmd_status)
    
    # Agent Interface
    parser_agent = subparsers.add_parser("agent", help="JSON output for AI agents")
    parser_agent.set_defaults(func=cmd_agent)

    parser_cmds = subparsers.add_parser("commands", help="JSON command list for AI agents")
    parser_cmds.set_defaults(func=cmd_commands)

    parser_visual = subparsers.add_parser("visual", help="Show a simple terminal visualizer for the current track")
    parser_visual.set_defaults(func=cmd_visual)

    # Vibe Engine
    parser_vibe = subparsers.add_parser("vibe", help="Adaptive vibe engine for developers")
    vibe_sub = parser_vibe.add_subparsers(dest="vibe_cmd", required=True)

    for v in ["focus", "debug", "build", "deploy", "late-night", "hackathon"]:
        p = vibe_sub.add_parser(v, help=f"Start '{v}' vibe")
        p.add_argument("--quiet", action="store_true", help="Reduce output (useful for hooks)")
        p.add_argument("--limit", type=int, default=5, help="Number of results to fetch (1-25)")
        p.add_argument("--no-shuffle", action="store_true", help="Disable shuffling results")
        p.add_argument("--cache-ttl", dest="cache_ttl", type=int, default=0, help="Cache TTL seconds (0 = no cache, fresh songs each time)")
        p.add_argument("--lang", default="en", help="Language filter for vibe results: en (default), tamil, or any")
        p.set_defaults(func=cmd_vibe_mode, vibe=v)
        if v == "deploy":
            p.add_argument("--victory", action="store_true", help="Print victory FX")

    p_branch = vibe_sub.add_parser("branch", help="Pick vibe based on current git branch")
    p_branch.add_argument("--quiet", action="store_true", help="Reduce output (useful for hooks)")
    p_branch.add_argument("--limit", type=int, default=5, help="Number of results to fetch (1-25)")
    p_branch.add_argument("--no-shuffle", action="store_true", help="Disable shuffling results")
    p_branch.add_argument("--cache-ttl", dest="cache_ttl", type=int, default=0, help="Cache TTL seconds (0 = no cache)")
    p_branch.add_argument("--lang", default="en", help="Language filter for vibe results: en (default), tamil, or any")
    p_branch.add_argument("--repo", default=".", help="Path to a git repo (default: .)")
    p_branch.add_argument("--fallback", default="focus", choices=list(VIBE_MAP.keys()), help="Fallback vibe")
    p_branch.set_defaults(func=cmd_vibe_branch)

    p_auto = vibe_sub.add_parser("auto", help="Auto-switch vibe based on time + git branch")
    p_auto.add_argument("--quiet", action="store_true", help="Reduce output")
    p_auto.add_argument("--limit", type=int, default=5, help="Number of results to fetch (1-25)")
    p_auto.add_argument("--no-shuffle", action="store_true", help="Disable shuffling results")
    p_auto.add_argument("--cache-ttl", dest="cache_ttl", type=int, default=0, help="Cache TTL seconds (0 = no cache)")
    p_auto.add_argument("--lang", default="en", help="Language filter for vibe results: en (default), tamil, or any")
    p_auto.add_argument("--repo", default=".", help="Path to a git repo (default: .)")
    p_auto.add_argument("--interval", type=int, default=10, help="Polling interval seconds")
    p_auto.add_argument("--once", action="store_true", help="Decide once and exit")
    p_auto.add_argument("--night-start", type=int, default=23, help="Night mode start hour (0-23)")
    p_auto.add_argument("--night-end", type=int, default=6, help="Night mode end hour (0-23)")
    p_auto.add_argument("--allow-branch-override-at-night", action="store_true", help="Let branch rules override late-night vibe")
    p_auto.set_defaults(func=cmd_vibe_auto)

    p_card = vibe_sub.add_parser("card", help="Print a shareable Dev Vibe Card")
    p_card.add_argument("--json", action="store_true", help="Output JSON instead of ASCII card")
    p_card.set_defaults(func=cmd_vibe_card)

    p_visual_vibe = vibe_sub.add_parser("visual", help="Show a simple terminal visualizer for the current track")
    p_visual_vibe.set_defaults(func=cmd_visual)

    p_watch = vibe_sub.add_parser("watch", help="Run a command and switch vibe if errors appear")
    p_watch.add_argument("--quiet", action="store_true", help="Reduce output")
    p_watch.add_argument("--limit", type=int, default=5, help="Number of results to fetch (1-25)")
    p_watch.add_argument("--no-shuffle", action="store_true", help="Disable shuffling results")
    p_watch.add_argument("--cache-ttl", dest="cache_ttl", type=int, default=0, help="Cache TTL seconds (0 = no cache)")
    p_watch.add_argument("--lang", default="en", help="Language filter for vibe results: en (default), tamil, or any")
    p_watch.add_argument("--error-vibe", default="debug", choices=list(VIBE_MAP.keys()), help="Vibe to switch to on errors")
    p_watch.add_argument("--on-success", default=None, choices=list(VIBE_MAP.keys()), help="Optional vibe to switch to when command succeeds")
    p_watch.add_argument("--cwd", default=None, help="Working directory for the command")
    p_watch.add_argument("--no-passthrough", action="store_true", help="Do not print command output")
    p_watch.add_argument("cmd", nargs=argparse.REMAINDER, help="Command to run (use: yit vibe watch -- <cmd>)")
    p_watch.set_defaults(func=cmd_vibe_watch)

    # Git hooks (optional automation)
    parser_hooks = subparsers.add_parser("hooks", help="Install optional git hooks for auto-vibes")
    hooks_sub = parser_hooks.add_subparsers(dest="hooks_cmd", required=True)
    p_hi = hooks_sub.add_parser("install", help="Install hooks in a git repo")
    p_hi.add_argument("--repo", default=".", help="Path to a git repo (default: .)")
    p_hi.set_defaults(func=cmd_hooks_install)

    p_hu = hooks_sub.add_parser("uninstall", help="Remove installed hooks")
    p_hu.add_argument("--repo", default=".", help="Path to a git repo (default: .)")
    p_hu.set_defaults(func=cmd_hooks_uninstall)

    # Loop
    subparsers.add_parser("loop", help="Loop current track").set_defaults(func=cmd_loop)
    subparsers.add_parser("unloop", help="Stop looping").set_defaults(func=cmd_unloop)

    args = parser.parse_args()
    args.func(args)

if __name__ == "__main__":
    main()
