import json
import os
import re
import shlex
import subprocess
from pathlib import Path

from .db import DB_DIR, now

AGENT_PATTERNS = [
    (r"(?:^|/)claude(?:\s|$)", "claude"),
    (r"(?:^|/)codex(?:\s|$)", "codex"),
    (r"(?:^|/)gemini(?:\s|$)", "gemini"),
    (r"(?:^|/)aider(?:\s|$)", "aider"),
    (r"github-copilot|copilot-cli", "copilot"),
    (r"(?:^|/)cursor(?:\s|$)", "cursor"),
    (r"(?:^|/)sgpt(?:\s|$)|(?:^|/)chatgpt(?:\s|$)", "gpt"),
]

CONFIG_PATH = DB_DIR / "config.json"
IDLE_THRESHOLD_CPU = 2.0
IDLE_SCANS = 3
OWN_PID = os.getpid()


def _load_config():
    global IDLE_THRESHOLD_CPU, IDLE_SCANS
    if not CONFIG_PATH.exists():
        return
    try:
        cfg = json.loads(CONFIG_PATH.read_text())
    except (json.JSONDecodeError, OSError):
        return

    for entry in cfg.get("agents", []):
        pattern = entry.get("pattern", "")
        agent_type = entry.get("type", "custom")
        if pattern:
            AGENT_PATTERNS.append((pattern, agent_type))

    if "idle_threshold_cpu" in cfg:
        IDLE_THRESHOLD_CPU = float(cfg["idle_threshold_cpu"])
    if "idle_scans" in cfg:
        IDLE_SCANS = int(cfg["idle_scans"])


_load_config()


def _extract_cli_name(cmdline, agent_type):
    try:
        tokens = shlex.split(cmdline)
    except ValueError:
        tokens = cmdline.split()

    for token in tokens:
        token_name = Path(token).name
        for pattern, pattern_type in AGENT_PATTERNS:
            if pattern_type != agent_type:
                continue
            if re.search(pattern, token) or re.search(pattern, token_name):
                return token_name

    return agent_type


def _parse_all_processes():
    try:
        result = subprocess.run(
            ["ps", "-eo", "pid,ppid,tty,state,%cpu,lstart,command"],
            capture_output=True, text=True, timeout=5,
        )
        if result.returncode != 0:
            return {}, []
    except Exception:
        return {}, []

    all_procs = {}
    parsed_lines = []

    for line in result.stdout.strip().split("\n")[1:]:
        line = line.strip()
        if not line:
            continue
        parts = line.split(None, 10)
        if len(parts) < 11:
            continue
        try:
            pid = int(parts[0])
            ppid = int(parts[1])
            tty = parts[2]
            state = parts[3]
            cpu = float(parts[4])
            lstart = " ".join(parts[5:10])
            cmdline = parts[10]
        except (ValueError, IndexError):
            continue

        all_procs[pid] = {"ppid": ppid, "state": state, "cpu": cpu}
        parsed_lines.append({
            "pid": pid, "ppid": ppid, "tty": tty, "state": state,
            "cpu": cpu, "lstart": lstart, "cmdline": cmdline,
        })

    return all_procs, parsed_lines


def _has_active_children(agent_pid, all_procs):
    for pid, info in all_procs.items():
        if info["ppid"] != agent_pid:
            continue
        if info["state"].startswith("Z"):
            continue
        if info["state"].startswith("R"):
            return True
        if info["cpu"] >= 0.5:
            return True
        if _has_active_children(pid, all_procs):
            return True
    return False


def _batch_get_cwds(pids):
    if not pids:
        return {}
    try:
        pid_arg = ",".join(str(p) for p in pids)
        result = subprocess.run(
            ["lsof", "-p", pid_arg, "-a", "-d", "cwd", "-Fn"],
            capture_output=True, text=True, timeout=5,
        )
        cwds = {}
        current_pid = None
        for line in result.stdout.strip().split("\n"):
            if line.startswith("p"):
                try:
                    current_pid = int(line[1:])
                except ValueError:
                    current_pid = None
            elif line.startswith("n") and line != "n" and current_pid is not None:
                cwds[current_pid] = line[1:]
        return cwds
    except Exception:
        return {}


def scan_processes():
    all_procs, parsed_lines = _parse_all_processes()
    if not parsed_lines:
        return []

    raw_matches = []
    for entry in parsed_lines:
        if entry["pid"] == OWN_PID:
            continue
        for pattern, agent_type in AGENT_PATTERNS:
            if re.search(pattern, entry["cmdline"]):
                cli_name = _extract_cli_name(entry["cmdline"], agent_type)
                raw_matches.append({
                    "pid": entry["pid"], "ppid": entry["ppid"],
                    "tty": entry["tty"], "state": entry["state"],
                    "cpu": entry["cpu"], "type": agent_type,
                    "cli_name": cli_name, "cmdline": entry["cmdline"],
                    "lstart": entry["lstart"],
                })
                break

    matched_pids = {m["pid"] for m in raw_matches}
    leaders = [m for m in raw_matches if m["ppid"] not in matched_pids]

    leader_pids = [m["pid"] for m in leaders]
    cwds = _batch_get_cwds(leader_pids)

    enriched = []
    for m in leaders:
        cwd = cwds.get(m["pid"])
        base = f"{m['cli_name']}:{Path(cwd).name}" if cwd else f"{m['cli_name']}:pid-{m['pid']}"
        enriched.append((m, cwd or "", base))

    base_counts = {}
    for _, _, base in enriched:
        base_counts[base] = base_counts.get(base, 0) + 1

    name_seq = {}
    found = []
    for m, cwd, base in enriched:
        if base_counts[base] > 1:
            name_seq[base] = name_seq.get(base, 0) + 1
            name = f"{base}#{name_seq[base]}"
        else:
            name = base

        has_children = _has_active_children(m["pid"], all_procs)
        waiting = m["state"].startswith("S") and m["cpu"] < IDLE_THRESHOLD_CPU and not has_children
        tty = m.get("tty", "??")
        tty_short = tty.replace("ttys", "s") if tty.startswith("ttys") else tty
        found.append({
            "pid": m["pid"], "type": m["type"], "name": name,
            "cwd": cwd, "waiting": waiting, "cpu": m["cpu"],
            "lstart": m["lstart"], "state": m["state"],
            "has_children": has_children, "tty": tty_short,
        })

    return found


def _notify(title, message):
    try:
        subprocess.Popen(
            ["osascript", "-e",
             'display notification "{}" with title "{}"'.format(
                 message.replace('"', '\\"'), title.replace('"', '\\"'))],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
    except Exception:
        pass


STATUS_TITLE = {
    "running": "\U0001f504",
    "waiting": "\U0001f514",
    "done": "\u2705",
}


def _update_tab_titles(db):
    agents = db.execute(
        "SELECT name, status, tty FROM agents "
        "WHERE auto_discovered = 1 AND tty != '' AND tty IS NOT NULL"
    ).fetchall()

    for a in agents:
        tty = a["tty"]
        if tty.startswith("s"):
            tty_dev = "/dev/ttys" + tty[1:]
        elif tty.startswith("ttys"):
            tty_dev = "/dev/" + tty
        else:
            continue

        icon = STATUS_TITLE.get(a["status"], "")
        title = "{} {}".format(icon, a["name"])

        try:
            fd = os.open(tty_dev, os.O_WRONLY | os.O_NONBLOCK)
            try:
                os.write(fd, "\033]0;{}\007".format(title).encode())
            finally:
                os.close(fd)
        except (PermissionError, OSError):
            pass


def discover_agents(db, processes=None):
    if processes is None:
        processes = scan_processes()
    live_keys = {(p["pid"], p["lstart"]): p for p in processes}
    ts = now()

    tracked = db.execute(
        "SELECT id, pid, name, status, started_at, low_cpu_count, notified_idle FROM agents "
        "WHERE auto_discovered = 1 AND status IN ('running', 'waiting') AND pid IS NOT NULL"
    ).fetchall()

    for row in tracked:
        key = (row["pid"], row["started_at"])
        old_status = row["status"]
        already_notified = row["notified_idle"] or 0
        if key in live_keys:
            proc = live_keys[key]
            prev_count = row["low_cpu_count"] or 0

            if proc["waiting"]:
                new_count = prev_count + 1
                new_status = "waiting" if new_count >= IDLE_SCANS else "running"
            else:
                new_count = 0
                new_status = "running"

            if new_status == "waiting" and not already_notified:
                _notify("Wooster", "{} is now idle".format(row["name"]))
                already_notified = 1
            elif new_status == "running" and not proc["waiting"]:
                already_notified = 0

            db.execute(
                "UPDATE agents SET status = ?, low_cpu_count = ?, notified_idle = ?, updated_at = ? WHERE id = ?",
                (new_status, new_count, already_notified, ts, row["id"]),
            )
        else:
            if not already_notified:
                _notify("Wooster", "{} has finished".format(row["name"]))
            db.execute(
                "UPDATE agents SET status = 'done', notified_idle = 1, updated_at = ? WHERE id = ?",
                (ts, row["id"]),
            )

    known = {(row["pid"], row["started_at"]) for row in db.execute(
        "SELECT pid, started_at FROM agents WHERE pid IS NOT NULL"
    ).fetchall()}
    for proc in processes:
        if (proc["pid"], proc["lstart"]) not in known:
            db.execute(
                "INSERT INTO agents (name, type, status, pid, cwd, tty, auto_discovered, started_at, created_at, updated_at, low_cpu_count) "
                "VALUES (?, ?, 'running', ?, ?, ?, 1, ?, ?, ?, 0)",
                (proc["name"], proc["type"], proc["pid"], proc["cwd"], proc.get("tty", ""), proc["lstart"], ts, ts),
            )

    db.commit()

    _update_tab_titles(db)
