"""Privileged tcpdump helper with incremental log streaming.

This module starts tcpdump as a privileged background process via AppleScript,
captures output into a log file, and lets callers poll new lines in real time.
"""
from __future__ import annotations

import os
import shlex
import subprocess
import time
import uuid
from dataclasses import dataclass

from persistence import MUGI_LOGS_DIR


@dataclass
class TcpdumpSession:
    session_id: str
    pid: int
    log_path: str
    started_at: float


def _ensure_capture_dir() -> str:
    d = os.path.join(MUGI_LOGS_DIR, "network_capture")
    os.makedirs(d, exist_ok=True)
    return d


def _safe_filter(filter_expression: str | None) -> str | None:
    if not filter_expression:
        return None
    # Keep this conservative because the string is embedded in a privileged shell command.
    blocked = "\"'\\`;$"
    out = "".join(ch for ch in filter_expression if ch not in blocked).strip()
    return out or None


def _build_tcpdump_command(
    interface: str,
    packet_count: int | None,
    filter_expression: str | None,
) -> list[str]:
    cmd = ["/usr/sbin/tcpdump", "-l", "-U", "-nn", "-vv", "-i", interface]
    if packet_count is not None and int(packet_count) > 0:
        cmd += ["-c", str(int(packet_count))]
    safe_filter = _safe_filter(filter_expression)
    if safe_filter:
        # tcpdump expects filter expression as trailing argv tokens.
        cmd += shlex.split(safe_filter)
    return cmd


def _run_admin_shell(command: str, prompt: str) -> tuple[int, str, str]:
    applescript = (
        f'do shell script "{command}" '
        f'with administrator privileges '
        f'with prompt "{prompt}"'
    )
    r = subprocess.run(
        ["/usr/bin/osascript", "-e", applescript],
        capture_output=True,
        text=True,
        timeout=60,
    )
    return r.returncode, (r.stdout or "").strip(), (r.stderr or "").strip()


def start_tcpdump_session(
    *,
    interface: str,
    packet_count: int | None,
    filter_expression: str | None,
    prompt: str,
) -> TcpdumpSession:
    capture_dir = _ensure_capture_dir()
    session_id = uuid.uuid4().hex[:12]
    log_path = os.path.join(capture_dir, f"tcpdump_{session_id}.log")
    cmd = _build_tcpdump_command(interface, packet_count, filter_expression)
    tcpdump_cmd = " ".join(shlex.quote(part) for part in cmd)
    shell_cmd = (
        "/bin/sh -c "
        + shlex.quote(f"{tcpdump_cmd} > {shlex.quote(log_path)} 2>&1 & echo $!")
    )
    code, out, err = _run_admin_shell(shell_cmd, prompt=prompt)
    if code != 0:
        raise RuntimeError(err or "Failed to start tcpdump.")
    try:
        pid = int((out or "").splitlines()[-1].strip())
    except Exception as exc:
        raise RuntimeError(f"Failed to parse tcpdump PID: {out!r}") from exc
    return TcpdumpSession(
        session_id=session_id,
        pid=pid,
        log_path=log_path,
        started_at=time.time(),
    )


def is_pid_running(pid: int) -> bool:
    if pid <= 0:
        return False
    r = subprocess.run(
        ["/bin/ps", "-p", str(pid), "-o", "pid="],
        capture_output=True,
        text=True,
        timeout=3,
    )
    return r.returncode == 0 and (r.stdout or "").strip() != ""


def stop_tcpdump_session(pid: int) -> None:
    if pid <= 0:
        return
    # Attempt graceful stop first, then hard kill.
    shell_cmd = "/bin/sh -c " + shlex.quote(
        f"/bin/kill -TERM {int(pid)} >/dev/null 2>&1 || true; "
        "sleep 0.2; "
        f"/bin/kill -KILL {int(pid)} >/dev/null 2>&1 || true"
    )
    _run_admin_shell(shell_cmd, prompt="Mugi needs admin rights to stop network capture.")


def read_new_lines(log_path: str, offset: int) -> tuple[list[str], int]:
    if not os.path.isfile(log_path):
        return [], offset
    try:
        with open(log_path, "r", encoding="utf-8", errors="replace") as f:
            f.seek(max(0, int(offset)))
            chunk = f.read()
            new_offset = f.tell()
    except OSError:
        return [], offset
    if not chunk:
        return [], offset
    lines = [ln for ln in chunk.splitlines() if ln.strip()]
    return lines, new_offset


def read_tail(log_path: str, max_chars: int = 16000) -> str:
    if not os.path.isfile(log_path):
        return ""
    try:
        with open(log_path, "r", encoding="utf-8", errors="replace") as f:
            text = f.read()
    except OSError:
        return ""
    if len(text) > max_chars:
        return text[:max_chars] + "\n... (truncated)"
    return text
