from __future__ import annotations

import json
import os
import socket
import subprocess
import urllib.request
from urllib.parse import urlsplit
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",
                "VITE_API_BASE_URL", "BACKEND_HOST", "BACKEND_PORT", "FRONTEND_PORT", "JARVIS_FINANCE_API_URL"}
_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)
    return env


def _healthcheck(url: str) -> bool:
    try:
        with urllib.request.urlopen(url.rstrip("/") + "/health", timeout=0.5) as response:
            return 200 <= int(response.status) < 300
    except Exception:
        return False


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 | None = None,
    frontend_url: str | None = None,
    backend_reachable: bool | None = None,
    frontend_reachable: bool | None = None,
) -> 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"
    configured_api = api_url or os.environ.get("JARVIS_FINANCE_API_URL") or os.environ.get("VITE_API_BASE_URL")
    parsed = urlsplit(configured_api) if configured_api else None
    backend_port = parsed.port if parsed and parsed.hostname else None
    configured_frontend = frontend_url or os.environ.get("JARVIS_FINANCE_FRONTEND_URL")
    frontend_parsed = urlsplit(configured_frontend) if configured_frontend else None
    frontend_port = frontend_parsed.port if frontend_parsed and frontend_parsed.hostname else None
    backend_running = backend_reachable if backend_reachable is not None else bool(configured_api and _healthcheck(configured_api))
    frontend_running = (
        frontend_reachable
        if frontend_reachable is not None
        else bool(configured_frontend and _healthcheck(configured_frontend.rstrip("/").removesuffix("/api")))
    )
    if frontend_reachable is None and not frontend_running and frontend_port:
        frontend_running = _port_open(frontend_port)
    return {
        "purpose": "system_ops_status_v1",
        "status": "ok",
        "api_url": configured_api,
        "runtime_db_available": db_path.exists(),
        "runtime_outside_repo": repo not in runtime.parents and runtime != repo,
        "backend": {"status": "running" if backend_running else "offline", "port": backend_port},
        "frontend": {"status": "running" if frontend_running else "offline", "port": frontend_port},
        "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
