"""Diagnose Python virtual environment health."""

from __future__ import annotations

import os
import shutil
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class Issue:
    """A single diagnosed problem."""

    code: str
    severity: str  # "error", "warn", "info"
    message: str
    fix_hint: str = ""


@dataclass
class Diagnosis:
    """Full environment diagnosis result."""

    project_dir: Path
    venv_path: Path | None = None
    python_path: Path | None = None
    pip_path: Path | None = None
    poetry_available: bool = False
    poetry_lock_stale: bool = False
    conda_active: bool = False
    issues: list[Issue] = field(default_factory=list)

    @property
    def healthy(self) -> bool:
        return not any(i.severity == "error" for i in self.issues)

    @property
    def errors(self) -> list[Issue]:
        return [i for i in self.issues if i.severity == "error"]

    @property
    def warnings(self) -> list[Issue]:
        return [i for i in self.issues if i.severity == "warn"]


def find_venv(project_dir: Path) -> Path | None:
    """Locate virtual environment directory.

    Detects venvs even when Python/pip symlinks are broken by checking for
    the bin/ directory and pyvenv.cfg marker file.
    """
    candidates = ["venv", ".venv", "env", ".env"]
    for name in candidates:
        p = project_dir / name
        if not p.is_dir():
            continue
        # Check for pyvenv.cfg (definitive venv marker)
        if (p / "pyvenv.cfg").exists():
            return p
        # Check for bin/ or Scripts/ with python (including broken symlinks)
        bin_dir = p / "bin"
        scripts_dir = p / "Scripts"
        if bin_dir.is_dir() and any(bin_dir.glob("python*")):
            return p
        if scripts_dir.is_dir() and any(scripts_dir.glob("python*")):
            return p
    return None


def _check_symlink_target(path: Path) -> bool:
    """Check if a symlink points to an existing target."""
    if path.is_symlink():
        target = path.resolve()
        return target.exists()
    return path.exists()


def _run_quiet(cmd: list[str], cwd: Path | None = None, timeout: int = 10) -> tuple[int, str]:
    """Run command and return (returncode, stdout)."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            cwd=cwd,
        )
        return result.returncode, result.stdout.strip()
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return -1, ""


def diagnose(project_dir: Path | str | None = None) -> Diagnosis:
    """Run full environment diagnosis on a project directory."""
    if project_dir is None:
        project_dir = Path.cwd()
    project_dir = Path(project_dir).resolve()

    diag = Diagnosis(project_dir=project_dir)

    # --- Detect conda ---
    if os.environ.get("CONDA_DEFAULT_ENV"):
        diag.conda_active = True
        diag.issues.append(Issue(
            code="CONDA_ACTIVE",
            severity="info",
            message=f"Conda environment active: {os.environ['CONDA_DEFAULT_ENV']}",
        ))

    # --- Detect Poetry ---
    diag.poetry_available = shutil.which("poetry") is not None

    # --- Locate venv ---
    venv_path = find_venv(project_dir)
    diag.venv_path = venv_path

    if venv_path is None:
        diag.issues.append(Issue(
            code="NO_VENV",
            severity="warn",
            message="No virtual environment found (checked: venv, .venv, env, .env)",
            fix_hint="python3 -m venv venv",
        ))
    else:
        # --- Check Python binary in venv ---
        bin_dir = venv_path / "bin"
        if not bin_dir.exists():
            bin_dir = venv_path / "Scripts"  # Windows

        python_bin = bin_dir / "python"
        if sys.platform == "win32":
            python_bin = bin_dir / "python.exe"

        diag.python_path = python_bin

        if python_bin.is_symlink() and not python_bin.exists():
            # Symlink exists but target is gone
            target = os.readlink(python_bin)
            diag.issues.append(Issue(
                code="VENV_BROKEN_PYTHON",
                severity="error",
                message=f"Python symlink broken: {python_bin} -> {target}",
                fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
            ))
        elif not python_bin.exists():
            diag.issues.append(Issue(
                code="VENV_NO_PYTHON",
                severity="error",
                message=f"Python binary missing: {python_bin}",
                fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
            ))
        elif not _check_symlink_target(python_bin):
            target = python_bin.resolve() if python_bin.is_symlink() else python_bin
            diag.issues.append(Issue(
                code="VENV_BROKEN_PYTHON",
                severity="error",
                message=f"Python symlink broken: {python_bin} -> {target}",
                fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
            ))
        else:
            rc, ver = _run_quiet([str(python_bin), "--version"])
            if rc != 0:
                diag.issues.append(Issue(
                    code="VENV_PYTHON_FAIL",
                    severity="error",
                    message=f"Python in venv not executable: {python_bin}",
                    fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
                ))
            else:
                diag.issues.append(Issue(
                    code="VENV_PYTHON_OK",
                    severity="info",
                    message=f"venv Python: {ver}",
                ))

        # --- Check pip binary in venv ---
        pip_bin = bin_dir / "pip"
        if sys.platform == "win32":
            pip_bin = bin_dir / "pip.exe"

        diag.pip_path = pip_bin

        if not pip_bin.exists():
            diag.issues.append(Issue(
                code="VENV_NO_PIP",
                severity="error",
                message=f"pip binary missing: {pip_bin}",
                fix_hint=f"{python_bin} -m ensurepip --upgrade",
            ))
        elif not _check_symlink_target(pip_bin):
            target = pip_bin.resolve() if pip_bin.is_symlink() else pip_bin
            diag.issues.append(Issue(
                code="VENV_BROKEN_PIP",
                severity="error",
                message=f"pip symlink broken: {pip_bin} -> {target}",
                fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
            ))
        else:
            rc, ver = _run_quiet([str(pip_bin), "--version"])
            if rc != 0:
                # Try via python -m pip
                rc2, ver2 = _run_quiet([str(python_bin), "-m", "pip", "--version"])
                if rc2 == 0:
                    diag.issues.append(Issue(
                        code="VENV_PIP_SHIM_BROKEN",
                        severity="error",
                        message=f"pip shim broken but 'python -m pip' works",
                        fix_hint=f"{python_bin} -m pip install --upgrade pip",
                    ))
                else:
                    diag.issues.append(Issue(
                        code="VENV_PIP_FAIL",
                        severity="error",
                        message=f"pip not functional in venv",
                        fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
                    ))
            else:
                diag.issues.append(Issue(
                    code="VENV_PIP_OK",
                    severity="info",
                    message=f"venv pip: {ver.split(',')[0] if ',' in ver else ver}",
                ))

        # --- Check pyvenv.cfg ---
        cfg = venv_path / "pyvenv.cfg"
        if cfg.exists():
            text = cfg.read_text()
            for line in text.splitlines():
                if line.startswith("home"):
                    home_dir = line.split("=", 1)[1].strip()
                    if not Path(home_dir).exists():
                        diag.issues.append(Issue(
                            code="VENV_STALE_HOME",
                            severity="error",
                            message=f"pyvenv.cfg home dir missing: {home_dir}",
                            fix_hint=f"rm -rf {venv_path} && python3 -m venv {venv_path}",
                        ))
                    break

    # --- Check pyproject.toml ---
    pyproject = project_dir / "pyproject.toml"
    if not pyproject.exists():
        diag.issues.append(Issue(
            code="NO_PYPROJECT",
            severity="warn",
            message="No pyproject.toml found",
        ))

    # --- Check poetry.lock staleness ---
    lock_file = project_dir / "poetry.lock"
    if diag.poetry_available and pyproject.exists() and lock_file.exists():
        rc, out = _run_quiet(["poetry", "check", "--lock"], cwd=project_dir)
        if rc != 0:
            diag.poetry_lock_stale = True
            diag.issues.append(Issue(
                code="POETRY_LOCK_STALE",
                severity="error",
                message="poetry.lock out of sync with pyproject.toml",
                fix_hint="poetry lock",
            ))
        else:
            diag.issues.append(Issue(
                code="POETRY_LOCK_OK",
                severity="info",
                message="poetry.lock is in sync",
            ))
    elif diag.poetry_available and pyproject.exists() and not lock_file.exists():
        diag.issues.append(Issue(
            code="POETRY_NO_LOCK",
            severity="warn",
            message="poetry.lock not found",
            fix_hint="poetry lock",
        ))

    # --- Check VIRTUAL_ENV env var mismatch ---
    active_venv = os.environ.get("VIRTUAL_ENV")
    if active_venv and venv_path:
        active_resolved = Path(active_venv).resolve()
        venv_resolved = venv_path.resolve()
        if active_resolved != venv_resolved:
            diag.issues.append(Issue(
                code="VENV_MISMATCH",
                severity="warn",
                message=f"Active VIRTUAL_ENV ({active_resolved}) differs from project venv ({venv_resolved})",
                fix_hint=f"deactivate && source {venv_path}/bin/activate",
            ))
    elif active_venv and not venv_path:
        diag.issues.append(Issue(
            code="VENV_EXTERNAL",
            severity="info",
            message=f"Using external venv: {active_venv}",
        ))

    return diag
