from __future__ import annotations

import json
import os
import socket
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from jarvis_finance.config.settings import find_repo_root, load_settings

ALLOWED_ACTIONS: dict[str, str] = {
    "backend": "scripts/restart_backend.sh",
    "frontend": "scripts/restart_frontend.sh",
    "dashboard": "scripts/restart_dashboard.sh",
}
_ALLOWED_SCRIPT_NAMES = {"restart_backend.sh", "restart_frontend.sh", "restart_dashboard.sh", "restart_vue_dashboard.sh"}
_ALLOWED_ENV = {"PATH", "HOME", "USER", "SHELL", "LANG", "LC_ALL", "JARVIS_FINANCE_RUNTIME_DIR"}
_SAFE_OPS_KEYS = {
    "action",
    "component",
    "status",
    "started_at",
    "finished_at",
    "message",
    "error",
    "return_code",
    "worker_started",
    "log_available",
}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _port_open(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(0.3)
        return sock.connect_ex(("127.0.0.1", int(port))) == 0


def _runtime_dir(repo_root: Path | None = None) -> Path:
    return load_settings(repo_root=repo_root or find_repo_root()).runtime_paths.base_dir


def _safe_env(runtime_dir: Path) -> dict[str, str]:
    env = {k: v for k, v in os.environ.items() if k in _ALLOWED_ENV}
    env["JARVIS_FINANCE_RUNTIME_DIR"] = str(runtime_dir)
    env["VITE_API_BASE_URL"] = "http://100.85.29.67:8000"
    env["BACKEND_HOST"] = "0.0.0.0"
    env["BACKEND_PORT"] = "8000"
    env["FRONTEND_PORT"] = "5173"
    return env


def _write_ops_log(runtime_dir: Path, entry: dict[str, Any]) -> None:
    log_dir = runtime_dir / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    safe_entry = _sanitize_ops_entry(entry)
    with (log_dir / "ops_actions.jsonl").open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(safe_entry, ensure_ascii=False, sort_keys=True) + "\n")


def _sanitize_ops_entry(entry: dict[str, Any]) -> dict[str, Any]:
    return {key: value for key, value in entry.items() if key in _SAFE_OPS_KEYS}


def _last_ops_action(runtime_dir: Path) -> dict[str, Any] | None:
    path = runtime_dir / "logs" / "ops_actions.jsonl"
    if not path.exists():
        return None
    try:
        lines = [line for line in path.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip()]
        return _sanitize_ops_entry(json.loads(lines[-1])) if lines else None
    except Exception:
        return None


def system_status(*, runtime_dir: Path | None = None, repo_root: Path | None = None, api_url: str = "http://100.85.29.67:8000") -> dict[str, Any]:
    repo = (repo_root or find_repo_root()).resolve()
    runtime = (runtime_dir or _runtime_dir(repo)).resolve()
    db_path = runtime / "data" / "finance.sqlite3"
    return {
        "purpose": "system_ops_status_v1",
        "status": "ok",
        "api_url": api_url,
        "runtime_db_available": db_path.exists(),
        "runtime_outside_repo": repo not in runtime.parents and runtime != repo,
        "backend": {"status": "running" if _port_open(8000) else "offline", "port": 8000},
        "frontend": {"status": "running" if _port_open(5173) else "offline", "port": 5173},
        "last_restart": _last_ops_action(runtime),
    }


def restart_system_component(action: str, *, runtime_dir: Path | None = None, repo_root: Path | None = None, timeout: int = 30) -> dict[str, Any]:
    if action not in ALLOWED_ACTIONS:
        raise ValueError("unsupported_system_action")
    repo = (repo_root or find_repo_root()).resolve()
    runtime = (runtime_dir or _runtime_dir(repo)).resolve()
    script_rel = ALLOWED_ACTIONS[action]
    script_path = (repo / script_rel).resolve()
    started_at = _now()
    base = {
        "action": f"restart_{action}",
        "component": action,
        "started_at": started_at,
    }
    if repo not in script_path.parents or script_path.name not in _ALLOWED_SCRIPT_NAMES:
        raise ValueError("script_not_allowed")
    if not script_path.exists():
        result = {**base, "status": "error", "message": "Restart-Script fehlt.", "error": "script_missing", "finished_at": _now()}
        _write_ops_log(runtime, result)
        return result
    # Restart requests are served by the backend that may itself be stopped by
    # the restart script. Running those scripts synchronously makes the browser
    # see a network error before the HTTP response is flushed. Always schedule a
    # detached worker with a tiny delay so remote/Tailscale clients receive a
    # deterministic response, then let the shell script stop/start/healthcheck.
    log_dir = runtime / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    worker_log = log_dir / f"restart_{action}.log"
    worker_command = f"sleep 1; exec {str(script_path)!r}"
    with worker_log.open("ab") as log_fh:
        worker = subprocess.Popen(
            ["/usr/bin/env", "bash", "-lc", worker_command],
            cwd=str(repo),
            env=_safe_env(runtime),
            stdout=log_fh,
            stderr=subprocess.STDOUT,
            start_new_session=True,
        )
    result = {
        **base,
        "status": "scheduled",
        "finished_at": _now(),
        "message": f"{action.title()}-Restart wurde gestartet. Bitte in wenigen Sekunden erneut prüfen.",
        "return_code": None,
        "worker_started": worker.pid > 0,
        "log_available": True,
    }
    _write_ops_log(runtime, result)
    return result
