from __future__ import annotations

import os
from pathlib import Path
from typing import Any

try:
    import psutil
except Exception:  # pragma: no cover - handled via status payload
    psutil = None


_RUNTIME_KINDS = ("gui_backend", "gold_trader", "pipeline_live_trade")
_REQUIRED_EXPLICIT_PARAMS = ("post_close_cooldown_sec",)


def _normalize_cmdline(cmdline: Any) -> list[str]:
    if isinstance(cmdline, (list, tuple)):
        return [str(part) for part in cmdline if str(part or "").strip()]
    text = str(cmdline or "").strip()
    return [text] if text else []


def _command_text(cmdline: list[str]) -> str:
    return " ".join(cmdline).strip()


def _is_runtime_executable(cmdline: list[str]) -> bool:
    """Only classify processes whose executable is Python or our frozen EXE."""
    if not cmdline:
        return False
    exe = str(cmdline[0]).lower()
    # Python interpreters
    if exe.endswith("python.exe") or exe.endswith("python3.exe") or exe.endswith("python"):
        return True
    if "python" in exe and (exe.endswith(".exe") or "/python" in exe):
        return True
    # Our frozen EXE
    if "goldautotrade" in exe:
        return True
    return False


def _classify_runtime_process(cmdline: list[str]) -> str | None:
    if not _is_runtime_executable(cmdline):
        return None
    command = _command_text(cmdline)
    if "gui_backend.py" in command:
        return "gui_backend"
    if "bridge/gold_trader.py" in command:
        return "gold_trader"
    if "src/pipeline.py" in command and "--runtime-intent" in cmdline and "live_trade" in cmdline:
        return "pipeline_live_trade"
    # Frozen mode: sys.executable --auto --daemon-pipeline
    if "--daemon-pipeline" in cmdline:
        return "pipeline_live_trade"
    return None


def _candidate_script_paths(cmdline: list[str], cwd: str) -> list[Path]:
    candidates: list[Path] = []
    base_cwd = Path(cwd).resolve() if cwd else None
    for part in cmdline:
        text = str(part or "").strip()
        if not text.endswith((".py", ".pyc")):
            continue
        path = Path(text)
        if path.is_absolute():
            candidates.append(path.resolve())
            continue
        if base_cwd is not None:
            candidates.append((base_cwd / path).resolve())
    return candidates


def _infer_code_root(kind: str, cmdline: list[str], cwd: str) -> str:
    for script_path in _candidate_script_paths(cmdline, cwd):
        try:
            if kind == "gui_backend" and script_path.name == "gui_backend.py":
                return str(script_path.parent)
            if kind == "gold_trader" and script_path.parts[-2:] == ("bridge", "gold_trader.py"):
                return str(script_path.parent.parent)
            if kind == "pipeline_live_trade" and script_path.parts[-2:] == ("src", "pipeline.py"):
                return str(script_path.parent.parent)
        except Exception:
            continue
    # Frozen mode fallback for pipeline: use cwd or APP_DIR env
    if kind == "pipeline_live_trade":
        app_dir = os.environ.get("LOBSTERQUANT_APP_DIR", "").strip()
        if app_dir:
            return str(Path(app_dir).resolve())
    return str(Path(cwd).resolve()) if cwd else ""


def _build_issue(level: str, code: str, message: str, **extra: Any) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "level": str(level or "WARNING").upper(),
        "code": str(code or "unknown"),
        "message": str(message or "").strip(),
    }
    for key, value in extra.items():
        if value not in (None, "", [], {}):
            payload[key] = value
    return payload


def inspect_runtime_processes(project_root: Path, *, current_pid: int | None = None) -> dict[str, Any]:
    counts = {kind: 0 for kind in _RUNTIME_KINDS}
    processes: list[dict[str, Any]] = []
    issues: list[dict[str, Any]] = []
    observed_roots: set[str] = set()
    resolved_root = str(Path(project_root).resolve())

    if psutil is None:
        return {
            "status": "ERROR",
            "project_root": resolved_root,
            "process_counts": counts,
            "processes": [],
            "code_roots": [],
            "issues": [
                _build_issue(
                    "ERROR",
                    "psutil_unavailable",
                    "psutil not available; cannot inspect runtime process topology",
                )
            ],
            "message": "psutil not available; cannot inspect runtime process topology",
        }

    for proc in psutil.process_iter(["pid", "ppid", "cmdline", "cwd", "name"]):
        try:
            info = proc.info
            pid = int(info.get("pid") or 0)
            if current_pid is not None and pid == int(current_pid):
                continue
            cmdline = _normalize_cmdline(info.get("cmdline"))
            kind = _classify_runtime_process(cmdline)
            if kind is None:
                continue
            ppid = int(info.get("ppid") or 0)
            cwd = str(info.get("cwd") or "")
            code_root = _infer_code_root(kind, cmdline, cwd)
            if code_root:
                observed_roots.add(code_root)
            counts[kind] += 1
            processes.append(
                {
                    "pid": pid,
                    "ppid": ppid,
                    "kind": kind,
                    "cwd": cwd,
                    "code_root": code_root,
                    "command": _command_text(cmdline),
                    "orphan": ppid == 1,
                }
            )
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            continue

    for kind, count in counts.items():
        if count > 1:
            issues.append(
                _build_issue(
                    "ERROR",
                    f"duplicate_{kind}",
                    f"Detected {count} concurrent {kind} processes",
                    count=count,
                    kind=kind,
                )
            )

    for process in processes:
        if process["orphan"] and process["kind"] in {"gold_trader", "pipeline_live_trade"}:
            issues.append(
                _build_issue(
                    "WARNING",
                    f"orphan_{process['kind']}",
                    f"{process['kind']} PID {process['pid']} is orphaned (ppid=1)",
                    pid=process["pid"],
                    kind=process["kind"],
                    code_root=process.get("code_root", ""),
                )
            )

    comparable_roots = {root for root in observed_roots if root}
    if len(comparable_roots) > 1:
        issues.append(
            _build_issue(
                "ERROR",
                "mixed_code_roots",
                "Detected runtime processes started from multiple code roots",
                code_roots=sorted(comparable_roots),
            )
        )

    message = ""
    if issues:
        message = issues[0]["message"]
    elif any(counts.values()):
        message = "Runtime process topology looks healthy"

    status = "OK"
    if any(issue["level"] == "ERROR" for issue in issues):
        status = "ERROR"
    elif issues:
        status = "WARNING"

    return {
        "status": status,
        "project_root": resolved_root,
        "process_counts": counts,
        "processes": processes,
        "code_roots": sorted(comparable_roots),
        "issues": issues,
        "message": message,
    }


def inspect_runtime_config(project_root: Path) -> dict[str, Any]:
    resolved_root = str(Path(project_root).resolve())
    try:
        import config_manager
    except Exception as exc:
        return {
            "status": "ERROR",
            "project_root": resolved_root,
            "issues": [
                _build_issue(
                    "ERROR",
                    "config_manager_import_failed",
                    f"Failed to import config_manager: {exc}",
                )
            ],
            "message": f"Failed to import config_manager: {exc}",
        }

    app_dir = Path(config_manager.get_app_dir()).resolve()
    config_dir = Path(config_manager.get_config_dir()).resolve()
    data_dir = Path(config_manager.get_data_dir()).resolve()
    logs_dir = Path(config_manager.get_logs_dir()).resolve()

    required_params: list[dict[str, Any]] = []
    issues: list[dict[str, Any]] = []
    for param_name in _REQUIRED_EXPLICIT_PARAMS:
        value, source = config_manager.get_effective_param_with_source(param_name, None)
        explicit = source not in {"schema_default", "caller_default"}
        param_payload = {
            "name": param_name,
            "value": value,
            "source": source,
            "explicit": explicit,
        }
        required_params.append(param_payload)
        if not explicit:
            issues.append(
                _build_issue(
                    "WARNING",
                    f"missing_explicit_{param_name}",
                    f"{param_name} is still using {source}; set it explicitly in the active runtime config",
                    param=param_name,
                    source=source,
                    value=value,
                )
            )

    message = issues[0]["message"] if issues else "Runtime config looks healthy"
    status = "WARNING" if issues else "OK"

    return {
        "status": status,
        "project_root": resolved_root,
        "app_dir": str(app_dir),
        "config_dir": str(config_dir),
        "data_dir": str(data_dir),
        "logs_dir": str(logs_dir),
        "running_from_project_root": app_dir == Path(project_root).resolve(),
        "required_params": required_params,
        "issues": issues,
        "message": message,
    }
