"""Scraperサブプロセスの起動・停止・稼働判定を担う。"""
from __future__ import annotations

import json
import logging
import os
import signal
import subprocess
import time
from pathlib import Path
from threading import Lock
from typing import Any, Dict, Optional

from .registry import ScraperDef

logger = logging.getLogger("scraper-api.process")

_running: Dict[str, subprocess.Popen] = {}
_locks: Dict[str, Lock] = {}


def _lock_for(name: str) -> Lock:
    if name not in _locks:
        _locks[name] = Lock()
    return _locks[name]


def _read_pid(scraper: ScraperDef) -> Optional[int]:
    pid_path = scraper.path(scraper.pid_file)
    if not pid_path.exists():
        return None
    try:
        return int(pid_path.read_text().strip())
    except (ValueError, OSError):
        return None


def is_running(scraper: ScraperDef) -> bool:
    proc = _running.get(scraper.name)
    if proc is not None:
        return proc.poll() is None

    pid = _read_pid(scraper)
    if pid is None:
        return False
    try:
        os.kill(pid, 0)
        return True
    except (ProcessLookupError, PermissionError, OSError):
        return False


def start(scraper: ScraperDef, body: Dict[str, Any]) -> int:
    lock = _lock_for(scraper.name)
    if not lock.acquire(blocking=False):
        raise RuntimeError("already_starting")
    try:
        if is_running(scraper):
            raise RuntimeError("already_running")

        if scraper.config_writer:
            scraper.config_writer(body, scraper.path(scraper.config_file))

        otp_path = scraper.path(scraper.otp_file)
        if otp_path.exists():
            try:
                otp_path.unlink()
            except OSError:
                pass

        env = {k: os.environ[k] for k in scraper.env_passthrough if k in os.environ}
        env.setdefault("PYTHONUNBUFFERED", "1")

        argv = ["python3", scraper.script]
        if scraper.argv_builder:
            argv.extend(scraper.argv_builder(body))

        script_dir = Path(scraper.dir)
        script_dir.mkdir(parents=True, exist_ok=True)

        proc = subprocess.Popen(
            argv,
            cwd=scraper.dir,
            env=env,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )
        _running[scraper.name] = proc

        pid_path = scraper.path(scraper.pid_file)
        pid_path.parent.mkdir(parents=True, exist_ok=True)
        pid_path.write_text(str(proc.pid))
        logger.info("started %s pid=%d argv=%s", scraper.name, proc.pid, argv)
        return proc.pid
    finally:
        lock.release()


def stop(scraper: ScraperDef) -> bool:
    proc = _running.get(scraper.name)
    pid: Optional[int] = None

    if proc is not None and proc.poll() is None:
        pid = proc.pid
    else:
        pid = _read_pid(scraper)
        if pid is not None:
            try:
                os.kill(pid, 0)
            except (ProcessLookupError, PermissionError, OSError):
                pid = None

    if pid is None:
        return False

    _signal_tree(pid, signal.SIGTERM)
    time.sleep(0.5)

    try:
        subprocess.run(["pkill", "-9", "-f", "chromium"], check=False,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except FileNotFoundError:
        pass

    _signal_tree(pid, signal.SIGKILL)

    _running.pop(scraper.name, None)
    logger.info("stopped %s pid=%d", scraper.name, pid)
    return True


def _signal_tree(pid: int, sig: int) -> None:
    try:
        os.killpg(os.getpgid(pid), sig)
    except (ProcessLookupError, PermissionError, OSError):
        try:
            os.kill(pid, sig)
        except (ProcessLookupError, PermissionError, OSError):
            pass


def reset_stale_status(scraper: ScraperDef) -> None:
    """FastAPI起動時に stale な 'running' / 'waiting_otp' を 'stopped' に補正。"""
    status_path = scraper.path(scraper.status_file)
    if not status_path.exists():
        return
    try:
        data = json.loads(status_path.read_text(encoding="utf-8"))
    except (ValueError, OSError):
        return
    if data.get("status") in ("running", "waiting_otp") and not is_running(scraper):
        data["status"] = "stopped"
        try:
            status_path.write_text(
                json.dumps(data, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            logger.info("reset stale status for %s", scraper.name)
        except OSError:
            pass
