from __future__ import annotations

import json
import socket
import subprocess
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Optional
from urllib.request import Request, urlopen


@dataclass
class TaskResult:
    returncode: int
    stdout: str = ''
    stderr: str = ''


@dataclass
class Task:
    name: str
    command: list[str] | str
    cwd: str | None = None
    shell: bool = False
    env: dict[str, str] | None = None


class TaskRunner:
    def run(self, task: Task, timeout: int | None = None) -> TaskResult:
        result = subprocess.run(task.command, cwd=task.cwd, shell=task.shell, env=task.env, capture_output=True, text=True, timeout=timeout)
        return TaskResult(result.returncode, result.stdout, result.stderr)


class BackgroundJobManager:
    def __init__(self) -> None:
        self.jobs: dict[str, threading.Thread] = {}
        self.flags: dict[str, threading.Event] = {}

    def start(self, name: str, target: Callable[[threading.Event], None], daemon: bool = True) -> threading.Thread:
        self.stop(name)
        flag = threading.Event()
        thread = threading.Thread(target=target, args=(flag,), name=name, daemon=daemon)
        self.jobs[name] = thread
        self.flags[name] = flag
        thread.start()
        return thread

    def stop(self, name: str) -> None:
        flag = self.flags.get(name)
        if flag:
            flag.set()
        self.jobs.pop(name, None)
        self.flags.pop(name, None)

    def active(self) -> list[str]:
        return [name for name, thread in self.jobs.items() if thread.is_alive()]


class ServiceController:
    def __init__(self) -> None:
        self.processes: dict[str, subprocess.Popen] = {}

    def start(self, name: str, command: list[str] | str, cwd: str | None = None, shell: bool = False) -> subprocess.Popen:
        proc = subprocess.Popen(command, cwd=cwd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
        self.processes[name] = proc
        return proc

    def stop(self, name: str) -> bool:
        proc = self.processes.get(name)
        if not proc:
            return False
        if proc.poll() is None:
            proc.terminate()
            try:
                proc.wait(timeout=3)
            except Exception:
                proc.kill()
        return True

    def restart(self, name: str, command: list[str] | str, cwd: str | None = None, shell: bool = False) -> subprocess.Popen:
        self.stop(name)
        return self.start(name, command, cwd=cwd, shell=shell)

    def status(self, name: str) -> str:
        proc = self.processes.get(name)
        if not proc:
            return 'offline'
        return 'online' if proc.poll() is None else 'offline'


class ProcessMonitor:
    def snapshot(self) -> list[dict[str, Any]]:
        try:
            import psutil  # type: ignore
        except Exception:
            return []
        rows = []
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info']):
            try:
                memory = getattr(proc.info.get('memory_info'), 'rss', 0)
                rows.append({'pid': proc.info['pid'], 'name': proc.info['name'], 'cpu': proc.info.get('cpu_percent', 0), 'memory': memory})
            except Exception:
                continue
        return rows


class PortChecker:
    def check(self, host: str, port: int, timeout: float = 0.5) -> bool:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(timeout)
            try:
                return sock.connect_ex((host, int(port))) == 0
            except Exception:
                return False


@dataclass
class HealthCheck:
    name: str
    check: Callable[[], tuple[bool, str]]


class HealthSuite:
    def __init__(self) -> None:
        self.checks: list[HealthCheck] = []

    def add(self, name: str, check: Callable[[], tuple[bool, str]]) -> HealthCheck:
        item = HealthCheck(name, check)
        self.checks.append(item)
        return item

    def http_check(self, url: str, timeout: int = 3) -> tuple[bool, str]:
        try:
            req = Request(url, headers={'User-Agent': 'NestUI/1.0'})
            with urlopen(req, timeout=timeout) as response:
                return 200 <= response.status < 400, f'HTTP {response.status}'
        except Exception as exc:
            return False, str(exc)

    def run(self) -> list[tuple[str, bool, str]]:
        return [(check.name, *check.check()) for check in self.checks]


class DependencyGraph:
    def __init__(self) -> None:
        self.edges: dict[str, set[str]] = {}

    def add_service(self, name: str, depends_on: list[str] | None = None) -> None:
        self.edges.setdefault(name, set()).update(depends_on or [])
        for dep in depends_on or []:
            self.edges.setdefault(dep, set())

    def rows(self) -> list[list[str]]:
        rows = []
        for name, deps in sorted(self.edges.items()):
            rows.append([name, ', '.join(sorted(deps)) or '-', str(len(deps))])
        return rows


@dataclass
class ScheduledJob:
    name: str
    every_seconds: int
    callback: Callable[[], None]
    last_run: float = 0.0


class Scheduler:
    def __init__(self) -> None:
        self.jobs: list[ScheduledJob] = []

    def every(self, name: str, seconds: int, callback: Callable[[], None]) -> ScheduledJob:
        job = ScheduledJob(name, max(1, int(seconds)), callback)
        self.jobs.append(job)
        return job

    def tick(self) -> list[str]:
        now = time.time()
        ran: list[str] = []
        for job in self.jobs:
            if now - job.last_run >= job.every_seconds:
                job.callback()
                job.last_run = now
                ran.append(job.name)
        return ran


class RecentProjects:
    def __init__(self, path: str | Path, max_items: int = 25) -> None:
        self.path = Path(path)
        self.max_items = max_items
        self.items: list[str] = []
        self.load()

    def load(self) -> list[str]:
        if self.path.exists():
            try:
                self.items = json.loads(self.path.read_text(encoding='utf-8'))
            except Exception:
                self.items = []
        return self.items

    def save(self) -> Path:
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(json.dumps(self.items[:self.max_items], indent=2), encoding='utf-8')
        return self.path

    def remember(self, project_path: str) -> None:
        project_path = str(project_path)
        if project_path in self.items:
            self.items.remove(project_path)
        self.items.insert(0, project_path)
        self.items[:] = self.items[:self.max_items]
