# aider-watch-client aiderw | 2026-04-20 @ Asia/Taipei
"""aider subprocess wrapper — capture events, dispatch to awoooi (fallback buffer)."""
from __future__ import annotations
import atexit, json, os, subprocess, sys, threading, time
from datetime import datetime
from pathlib import Path
from ulid import ULID
from aider_watch_client import config, buffer
from aider_watch_client.api_client import post_events
from aider_watch_client.parsers import (
    parse_stdout_banner, parse_chat_history, git_numstat, git_diff_head,
)
from aider_watch_client.redactor import redact

IDLE_THRESHOLD_SEC = 120


def _now_iso() -> str:
    return datetime.now(config.TAIPEI).isoformat()


def _emit_live(ev: dict) -> None:
    try:
        with config.LIVE_LOG.open("a") as f:
            red = redact(ev.get("payload", {}))
            f.write(f"[{ev['ts']}] {ev['type']} "
                    f"{json.dumps(red, ensure_ascii=False, default=str)[:200]}\n")
    except Exception:
        pass


def _dispatch(events: list[dict]) -> None:
    """batch：redact → live.log → HTTP POST；失敗進 buffer。"""
    redacted = [{**e, "payload": redact(e.get("payload", {}))} for e in events]
    for e in redacted:
        _emit_live(e)
    if not post_events(redacted):
        buffer.write(redacted)


def _pre_git_sha(cwd: Path) -> str | None:
    try:
        r = subprocess.run(["git","-C",str(cwd),"rev-parse","HEAD"],
                           capture_output=True, text=True, timeout=3)
        return r.stdout.strip() if r.returncode == 0 else None
    except Exception:
        return None


def main(argv: list[str] | None = None) -> int:
    argv = list(argv if argv is not None else sys.argv[1:])
    config.ensure_dirs()
    cwd = Path.cwd()
    sid = str(ULID())
    start_ts = _now_iso()
    start_epoch = time.time()
    stdout_buf: list[str] = []
    last_output = [time.time()]
    error_count = [0]
    pre_sha = _pre_git_sha(cwd)
    host = os.environ.get("AIDER_WATCH_HOSTNAME") or os.uname().nodename
    aider_bin = os.environ.get("AIDER_BIN") or str(Path.home() / ".local/bin/aider")

    # subprocess start
    try:
        proc = subprocess.Popen([aider_bin] + argv, cwd=str(cwd),
                                stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                text=True, bufsize=1)
    except FileNotFoundError:
        print(f"aider binary not found at {aider_bin}", file=sys.stderr)
        return 127

    # session_start ASAP
    _dispatch([{
        "ts": start_ts, "session_id": sid, "host": host,
        "type": "session_start",
        "payload": {"cwd": str(cwd), "model": "unknown",
                    "aider_args": argv, "aider_pid": proc.pid,
                    "cli_version": "unknown"},
    }])

    # stdout reader
    def _reader():
        for line in proc.stdout:  # type: ignore[arg-type]
            sys.stdout.write(line); sys.stdout.flush()
            stdout_buf.append(line)
            last_output[0] = time.time()
            lo = line.lower()
            if "error" in lo or "exception" in lo:
                error_count[0] += 1
    t = threading.Thread(target=_reader, daemon=True); t.start()

    # silent watcher
    silent_fired = [False]
    def _silent_watch():
        while proc.poll() is None:
            time.sleep(10)
            idle = time.time() - last_output[0]
            if idle >= IDLE_THRESHOLD_SEC and not silent_fired[0]:
                silent_fired[0] = True
                tail = "".join(stdout_buf[-1:])[-80:] if stdout_buf else ""
                _dispatch([{"ts": _now_iso(), "session_id": sid, "host": host,
                            "type": "silent_timeout",
                            "payload": {"idle_sec": int(idle),
                                        "last_output_tail": tail}}])
    w = threading.Thread(target=_silent_watch, daemon=True); w.start()

    # atexit safety
    emergency_fired = [False]
    def _emergency():
        if emergency_fired[0]:
            return
        emergency_fired[0] = True
        _dispatch([{"ts": _now_iso(), "session_id": sid, "host": host,
                    "type": "session_end",
                    "payload": {"duration_sec": int(time.time() - start_epoch),
                                "tokens_sent": 0, "tokens_received": 0,
                                "cost_usd": 0, "files_changed": 0,
                                "error_count": error_count[0], "exit_code": -999,
                                "wrapper_crash": True}}])
    atexit.register(_emergency)

    exit_code = proc.wait()
    emergency_fired[0] = True  # 正常結束，關閉保險

    duration = int(time.time() - start_epoch)
    stdout_text = "".join(stdout_buf)

    events_tail: list[dict] = []

    # canonical file edits from git diff
    if pre_sha:
        for e in git_numstat(cwd, pre_sha, "HEAD"):
            head = git_diff_head(cwd, e["path"], pre_sha, 3)
            events_tail.append({
                "ts": _now_iso(), "session_id": sid, "host": host,
                "type": "file_edit",
                "payload": {"path": e["path"], "lines_added": e["lines_added"],
                            "lines_deleted": e["lines_deleted"],
                            "diff_head": head,
                            "is_new_file": e.get("is_new_file", False)},
            })

    # tokens from chat_history
    tokens_sent = tokens_recv = 0
    chat_md = cwd / ".aider.chat.history.md"
    if chat_md.exists():
        stats = parse_chat_history(chat_md.read_text(errors="ignore"))
        tokens_sent = stats["tokens_sent"]; tokens_recv = stats["tokens_received"]

    if exit_code != 0:
        events_tail.append({
            "ts": _now_iso(), "session_id": sid, "host": host,
            "type": "error",
            "payload": {"kind": "non_zero_exit",
                        "message": f"aider exited with {exit_code}",
                        "context_50chars": stdout_text[-50:]},
        })

    banner = parse_stdout_banner(stdout_text)
    events_tail.append({
        "ts": _now_iso(), "session_id": sid, "host": host,
        "type": "session_end",
        "payload": {"duration_sec": duration,
                    "tokens_sent": tokens_sent,
                    "tokens_received": tokens_recv, "cost_usd": 0,
                    "files_changed": len([e for e in events_tail
                                           if e["type"] == "file_edit"]),
                    "error_count": error_count[0],
                    "exit_code": exit_code,
                    "model": banner.get("model", "unknown"),
                    "cwd": str(cwd)},
    })

    if events_tail:
        _dispatch(events_tail)

    return exit_code


if __name__ == "__main__":
    sys.exit(main())
