# -*- coding: utf-8 -*-
"""Parse sing-box TUN log: which processes use VPN / Squid / direct."""
from __future__ import annotations

import os
import re
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
# Examples after ANSI strip:
# [3408043073 0ms] router: found process path: C:\...\Cursor.exe
# router: found process path: C:\...\Cursor.exe
# [3408043073 1ms] outbound/vless[sota-vless]: outbound connection to 1.2.3.4:443
CONN_PROC_RE = re.compile(
    r"(?:\[(?:[^\]]*?)?(\d{5,})\s+\d+ms\]\s+)?router:\s+found process path:\s+(.+)$"
)
CONN_OUT_RE = re.compile(
    r"\[(?:[^\]]*?)?(\d{5,})\s+\d+ms\]\s+outbound/(vless|http|direct|block)\[([^\]]+)\]"
)
PROC_ONLY_RE = re.compile(r"router:\s+found process path:\s+(.+)$")
TIME_RE = re.compile(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})")


def tun_log_path() -> Path:
    return Path(os.environ["USERPROFILE"]) / ".claude-proxy-control" / "merged" / "vpn-claude-tun.log"


def _strip(line: str) -> str:
    return ANSI_RE.sub("", line).rstrip()


def _exe_name(path: str) -> str:
    p = path.strip().strip('"')
    return Path(p).name or p


def _route_label(kind: str, tag: str) -> str:
    if kind == "vless" or "sota" in tag.lower() or tag.lower() in {"proxy", "vpn"}:
        return "VPN"
    if kind == "http" or "squid" in tag.lower() or "claude" in tag.lower():
        return "Squid"
    if kind == "direct":
        return "Direct"
    if kind == "block":
        return "Block"
    return f"{kind}:{tag}"


# Средняя строка лога sing-box — около 120 байт; берём запас, чтобы после
# отбрасывания первой (обрезанной) строки нужное количество точно осталось.
_BYTES_PER_LINE = 400
_MIN_TAIL_BYTES = 256 * 1024


def _read_tail_lines(path: Path, max_lines: int) -> list[str]:
    """Прочитать последние max_lines строк, не поднимая файл целиком.

    Лог TUN растёт примерно на 25 строк в секунду и между перезапусками туннеля
    доходит до сотен мегабайт. Прежний read_text() тянул его весь в память каждые
    15 секунд — это и тормозило обновление, и грозило сорвать его исключением.
    """
    want = max(1, int(max_lines))
    size = path.stat().st_size
    take = min(size, max(_MIN_TAIL_BYTES, want * _BYTES_PER_LINE))
    with path.open("rb") as f:
        f.seek(size - take)
        chunk = f.read(take)
    lines = chunk.decode("utf-8", "ignore").splitlines()
    if take < size and lines:
        lines = lines[1:]  # первая строка обрезана посередине
    return lines[-want:]


def parse_live_traffic(max_lines: int = 2500, recent_sec: int = 120) -> list[dict[str, Any]]:
    """Return processes seen recently with route VPN/Squid/Direct."""
    path = tun_log_path()
    if not path.is_file():
        return []

    try:
        lines = _read_tail_lines(path, max_lines)
    except Exception:
        return []

    # conn_id -> last process path
    conn_proc: dict[str, str] = {}
    # fallback: last process seen globally (for lines without id)
    last_proc: str | None = None

    # key: (exe, route) -> stats
    stats: dict[tuple[str, str], dict[str, Any]] = {}
    cutoff = datetime.now() - timedelta(seconds=recent_sec)
    last_ts: datetime | None = None

    def touch(exe: str, route: str, full: str, ts: datetime | None, dest: str | None = None) -> None:
        key = (exe, route)
        row = stats.get(key)
        if not row:
            row = {
                "exe": exe,
                "route": route,
                "path": full,
                "hits": 0,
                "last_seen": ts.isoformat(timespec="seconds") if ts else None,
                "last_dest": dest or "",
            }
            stats[key] = row
        row["hits"] += 1
        if ts:
            row["last_seen"] = ts.isoformat(timespec="seconds")
        if dest:
            row["last_dest"] = dest
        row["path"] = full

    for raw in lines:
        line = _strip(raw)
        if not line:
            continue
        mtime = TIME_RE.search(line)
        if mtime:
            try:
                last_ts = datetime.strptime(mtime.group(1), "%Y-%m-%d %H:%M:%S")
            except Exception:
                pass
        if last_ts and last_ts < cutoff:
            # still update conn maps for correlation, but optionally skip old hits
            pass

        mp = CONN_PROC_RE.search(line)
        if mp:
            cid, pth = mp.group(1), mp.group(2).strip()
            last_proc = pth
            if cid:
                conn_proc[cid] = pth
            continue

        mo = CONN_OUT_RE.search(line)
        if mo:
            cid, kind, tag = mo.group(1), mo.group(2), mo.group(3)
            route = _route_label(kind, tag)
            pth = conn_proc.get(cid) or last_proc
            if not pth:
                continue
            if last_ts and last_ts < cutoff:
                continue
            dest = ""
            if " to " in line:
                dest = line.split(" to ", 1)[-1].strip()
            touch(_exe_name(pth), route, pth, last_ts, dest)
            continue

        # process-only line without id
        mp2 = PROC_ONLY_RE.search(line)
        if mp2:
            last_proc = mp2.group(1).strip()

    rows = list(stats.values())
    # Prefer VPN first, then by hits
    order = {"VPN": 0, "Squid": 1, "Direct": 2, "Block": 3}
    rows.sort(key=lambda r: (order.get(r["route"], 9), -r["hits"], r["exe"].lower()))
    return rows


def summarize_by_route(rows: list[dict[str, Any]] | None = None) -> dict[str, list[str]]:
    rows = rows if rows is not None else parse_live_traffic()
    out: dict[str, list[str]] = defaultdict(list)
    for r in rows:
        out[r["route"]].append(f"{r['exe']} ({r['hits']})")
    return dict(out)


if __name__ == "__main__":
    for r in parse_live_traffic():
        print(f"{r['route']:6} {r['hits']:4}  {r['exe']:30}  {r.get('last_dest','')}")
