from __future__ import annotations

import argparse
import csv
import errno
import io
import json
import os
import shutil
import signal
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, Optional

from core.process_enrichment import ProcessEnricher
from core.paths import (application_root, config_path as default_config_path,
                        is_frozen, relay_executable, relay_runtime_dir)
from core.service_runtime import TelemetryRelayService, utc_now_iso


PROJECT_ROOT = application_root()
DEFAULT_CONFIG_PATH = default_config_path()
DEFAULT_RUNTIME_DIR = relay_runtime_dir()


class RelayServiceWrapper:
    """Controls one persistent telemetry relay subprocess."""

    def __init__(self, config_path: Optional[str | Path] = None, runtime_dir: Optional[str | Path] = None):
        self.config_path = Path(config_path).resolve() if config_path is not None else DEFAULT_CONFIG_PATH
        self.runtime_dir = Path(runtime_dir).resolve() if runtime_dir is not None else DEFAULT_RUNTIME_DIR
        self.state_path = self.runtime_dir / "relay_state.json"
        self.stop_path = self.runtime_dir / "stop_requested"
        self.lock_path = self.runtime_dir / "relay.lock"
        self.lock_owner_path = self.lock_path / "owner_pid"
        self.service = TelemetryRelayService(config_path=self.config_path)
        self.process_enricher = ProcessEnricher()

    def _current_snapshot(self) -> Dict[int, Dict[str, Any]]:
        enriched = self.process_enricher.snapshot()
        if enriched:
            return enriched
        try:
            completed = subprocess.run(
                ["tasklist", "/FO", "CSV", "/NH"], capture_output=True, text=True, check=True,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            )
            result: Dict[int, Dict[str, Any]] = {}
            for row in csv.reader(io.StringIO(completed.stdout)):
                if len(row) < 2:
                    continue
                try:
                    pid = int(str(row[1]).replace(",", "").strip())
                except ValueError:
                    continue
                result[pid] = self.process_enricher.empty_process(pid, row[0].strip())
            return result
        except Exception:
            return {}

    @staticmethod
    def _pid_is_alive(pid: Any) -> bool:
        try:
            pid = int(pid)
            if pid <= 0:
                return False
            os.kill(pid, 0)
            return True
        except OSError as exc:
            return exc.errno == errno.EPERM
        except (TypeError, ValueError):
            return False

    def _read_state(self) -> Dict[str, Any]:
        try:
            return json.loads(self.state_path.read_text(encoding="utf-8"))
        except (FileNotFoundError, json.JSONDecodeError, OSError):
            return {}

    def _write_state(self, state: Dict[str, Any]) -> None:
        self.runtime_dir.mkdir(parents=True, exist_ok=True)
        temporary = self.state_path.with_suffix(f".tmp.{os.getpid()}")
        temporary.write_text(json.dumps(state, sort_keys=True), encoding="utf-8")
        os.replace(temporary, self.state_path)

    def _remove_runtime_state(self, include_lock: bool = True) -> None:
        for path in (self.stop_path, self.state_path):
            try:
                path.unlink()
            except FileNotFoundError:
                pass
        if include_lock:
            shutil.rmtree(self.lock_path, ignore_errors=True)

    def _lock_owner_is_alive(self) -> bool:
        try:
            return self._pid_is_alive(self.lock_owner_path.read_text(encoding="ascii").strip())
        except (FileNotFoundError, OSError):
            return False

    def _live_state(self, clean_stale: bool = True) -> Dict[str, Any]:
        state = self._read_state()
        if state and self._pid_is_alive(state.get("pid")):
            return state
        if clean_stale and state:
            self._remove_runtime_state()
        return {}

    def start(self, timeout: float = 10.0) -> Dict[str, Any]:
        if self._live_state():
            return self.status()
        self.runtime_dir.mkdir(parents=True, exist_ok=True)
        if self.lock_path.exists():
            lock_deadline = time.monotonic() + min(1.0, timeout)
            while time.monotonic() < lock_deadline:
                if self._live_state(clean_stale=False):
                    return self.status()
                if self._lock_owner_is_alive():
                    time.sleep(0.05)
                    continue
                time.sleep(0.05)  # Allow a new worker time to publish its owner PID.
            if self._lock_owner_is_alive():
                raise RuntimeError("relay process holds the runtime lock but is not ready")
        self._remove_runtime_state()
        command = self._worker_command()
        creationflags = 0
        if os.name == "nt":
            creationflags = (getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
                             | getattr(subprocess, "DETACHED_PROCESS", 0)
                             | getattr(subprocess, "CREATE_NO_WINDOW", 0))
        subprocess.Popen(
            command, cwd=str(application_root()), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL, close_fds=True, creationflags=creationflags,
            start_new_session=os.name != "nt",
        )
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            state = self._live_state(clean_stale=False)
            if state:
                return self.status()
            time.sleep(0.05)
        self._remove_runtime_state()
        raise RuntimeError("relay process did not become ready")

    def _worker_command(self) -> list[str]:
        arguments = ["--run", "--config", str(self.config_path), "--runtime-dir", str(self.runtime_dir)]
        if not is_frozen():
            return [sys.executable, "-m", "core.service_wrapper", *arguments]
        executable = Path(sys.executable) if Path(sys.executable).name.lower() == "processmonitorrelay.exe" else relay_executable()
        if not executable.is_file():
            raise FileNotFoundError(f"Packaged relay executable not found: {executable}")
        return [str(executable), *arguments]

    def _terminate_pid(self, pid: int) -> None:
        if os.name == "nt":
            subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True,
                           creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), check=False)
        else:
            try:
                os.kill(pid, signal.SIGTERM)
            except ProcessLookupError:
                pass

    def stop(self, timeout: float = 10.0) -> Dict[str, Any]:
        state = self._live_state()
        if not state:
            self._remove_runtime_state()
            return self.status()
        pid = int(state["pid"])
        self.runtime_dir.mkdir(parents=True, exist_ok=True)
        self.stop_path.touch()
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline and self._pid_is_alive(pid):
            time.sleep(0.05)
        if self._pid_is_alive(pid):
            self._terminate_pid(pid)
            deadline = time.monotonic() + 2.0
            while time.monotonic() < deadline and self._pid_is_alive(pid):
                time.sleep(0.05)
        self._remove_runtime_state()
        return self.status()

    def status(self) -> Dict[str, Any]:
        state = self._live_state()
        running = bool(state)
        return {
            "running": running, "enabled": self.service.enabled,
            "pid": int(state["pid"]) if running else None,
            "poll_interval": self.service.poll_interval, "output_path": str(self.service.output_path),
        }

    def run(self) -> int:
        self.runtime_dir.mkdir(parents=True, exist_ok=True)
        try:
            self.lock_path.mkdir()
        except FileExistsError:
            return 2
        self.lock_owner_path.write_text(str(os.getpid()), encoding="ascii")
        previous = self._current_snapshot()
        state = {"pid": os.getpid(), "started_at": utc_now_iso(), "config_path": str(self.config_path),
                 "poll_interval": self.service.poll_interval, "output_path": str(self.service.output_path)}
        try:
            self.stop_path.unlink(missing_ok=True)
            self.service.append_jsonl({"timestamp": utc_now_iso(), "source": "relay_service",
                                       "event": "relay_started", "pid": os.getpid(),
                                       "baseline_process_count": len(previous), "severity": "info"})
            self._write_state(state)
            while not self.stop_path.exists():
                deadline = time.monotonic() + self.service.poll_interval
                while time.monotonic() < deadline and not self.stop_path.exists():
                    time.sleep(min(0.1, max(0.0, deadline - time.monotonic())))
                if self.stop_path.exists():
                    break
                current = self._current_snapshot()
                for pid in current.keys() - previous.keys():
                    current[pid] = self.process_enricher.enrich_signature(current[pid])
                self.service.run_cycle(previous, current)
                previous = current
            self.service.append_jsonl({"timestamp": utc_now_iso(), "source": "relay_service",
                                       "event": "relay_stopped", "pid": os.getpid(), "severity": "info"})
            return 0
        finally:
            self.service.close()
            self._remove_runtime_state(include_lock=False)
            shutil.rmtree(self.lock_path, ignore_errors=True)


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Process monitoring relay service wrapper")
    parser.add_argument("--config", default=None)
    parser.add_argument("--runtime-dir", default=None)
    actions = parser.add_mutually_exclusive_group()
    actions.add_argument("--status", action="store_true")
    actions.add_argument("--start", action="store_true")
    actions.add_argument("--stop", action="store_true")
    actions.add_argument("--run", action="store_true", help=argparse.SUPPRESS)
    return parser


def main() -> int:
    parser = _build_parser()
    args = parser.parse_args()
    wrapper = RelayServiceWrapper(config_path=args.config, runtime_dir=args.runtime_dir)
    if args.run:
        return wrapper.run()
    if args.status:
        print(json.dumps(wrapper.status(), sort_keys=True))
        return 0
    if args.start:
        print(json.dumps(wrapper.start(), sort_keys=True))
        return 0
    if args.stop:
        print(json.dumps(wrapper.stop(), sort_keys=True))
        return 0
    parser.print_help()
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except KeyboardInterrupt:
        raise SystemExit(130)
