"""Outdated dependency detection.

Parses requirements.txt and pyproject.toml to identify installed
dependencies and checks how outdated they are by comparing against
the latest versions available on PyPI.
"""

from __future__ import annotations

import json
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any

try:
    import httpx
except ImportError:
    httpx = None  # type: ignore[assignment]


@dataclass
class DependencyInfo:
    """Information about a single dependency."""

    name: str
    installed_version: str
    latest_version: str
    is_outdated: bool
    major_behind: int = 0
    minor_behind: int = 0
    patch_behind: int = 0
    source_file: str = ""

    @property
    def severity(self) -> str:
        """How severe the outdatedness is."""
        if not self.is_outdated:
            return "current"
        if self.major_behind > 0:
            return "critical"  # Major version behind
        if self.minor_behind >= 5:
            return "high"
        if self.minor_behind > 0:
            return "medium"
        return "low"  # Only patch behind

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "installed_version": self.installed_version,
            "latest_version": self.latest_version,
            "is_outdated": self.is_outdated,
            "severity": self.severity,
            "major_behind": self.major_behind,
            "minor_behind": self.minor_behind,
            "patch_behind": self.patch_behind,
            "source_file": self.source_file,
        }


def _parse_version(version: str) -> tuple[int, int, int]:
    """Parse a version string into (major, minor, patch)."""
    # Strip pre-release/build metadata
    version = re.split(r"[-+]", version)[0]
    parts = version.split(".")
    try:
        major = int(parts[0]) if len(parts) > 0 else 0
        minor = int(parts[1]) if len(parts) > 1 else 0
        patch = int(re.sub(r"[^\d]", "", parts[2])) if len(parts) > 2 else 0
    except (ValueError, IndexError):
        return (0, 0, 0)
    return (major, minor, patch)


def _version_diff(
    installed: str, latest: str
) -> tuple[int, int, int]:
    """Calculate how far behind installed is from latest."""
    inst = _parse_version(installed)
    lat = _parse_version(latest)
    return (
        max(0, lat[0] - inst[0]),
        max(0, lat[1] - inst[1]) if lat[0] == inst[0] else 0,
        max(0, lat[2] - inst[2]) if lat[0] == inst[0] and lat[1] == inst[1] else 0,
    )


class DependencyAgeAnalyzer:
    """Detect outdated dependencies in a Python project."""

    def analyze_directory(
        self,
        directory: str | Path,
    ) -> list[DependencyInfo]:
        """Analyze dependencies from requirements files and pyproject.toml.

        Attempts to:
        1. Parse requirements.txt / pyproject.toml for declared deps
        2. Check installed versions via pip
        3. Query PyPI for latest versions
        """
        directory = Path(directory)
        declared_deps = self._parse_declared_deps(directory)

        if not declared_deps:
            # Fall back to pip list
            declared_deps = self._get_pip_packages()

        results: list[DependencyInfo] = []
        for name, version, source in declared_deps:
            latest = self._get_latest_version(name)
            if not latest or not version:
                results.append(
                    DependencyInfo(
                        name=name,
                        installed_version=version or "unknown",
                        latest_version=latest or "unknown",
                        is_outdated=False,
                        source_file=source,
                    )
                )
                continue

            major_behind, minor_behind, patch_behind = _version_diff(version, latest)
            is_outdated = (major_behind + minor_behind + patch_behind) > 0

            results.append(
                DependencyInfo(
                    name=name,
                    installed_version=version,
                    latest_version=latest,
                    is_outdated=is_outdated,
                    major_behind=major_behind,
                    minor_behind=minor_behind,
                    patch_behind=patch_behind,
                    source_file=source,
                )
            )

        return results

    def _parse_declared_deps(
        self, directory: Path
    ) -> list[tuple[str, str, str]]:
        """Parse dependencies from project files.

        Returns list of (name, version, source_file).
        """
        deps: list[tuple[str, str, str]] = []

        # Check requirements.txt
        for req_file in ["requirements.txt", "requirements-dev.txt", "requirements.lock"]:
            req_path = directory / req_file
            if req_path.exists():
                deps.extend(self._parse_requirements_txt(req_path))

        # Check pyproject.toml
        pyproject = directory / "pyproject.toml"
        if pyproject.exists():
            deps.extend(self._parse_pyproject_toml(pyproject))

        return deps

    def _parse_requirements_txt(
        self, path: Path
    ) -> list[tuple[str, str, str]]:
        """Parse a requirements.txt file."""
        deps: list[tuple[str, str, str]] = []
        try:
            lines = path.read_text(encoding="utf-8").splitlines()
        except OSError:
            return deps

        for line in lines:
            line = line.strip()
            if not line or line.startswith("#") or line.startswith("-"):
                continue
            # Parse "package==1.2.3", "package>=1.2.3", "package~=1.2.3"
            match = re.match(r"^([a-zA-Z0-9_.-]+)\s*([=~<>!]+)\s*([^\s,;#]+)", line)
            if match:
                name, _, version = match.groups()
                deps.append((name.lower(), version, str(path)))
            else:
                # Just a package name without version
                match = re.match(r"^([a-zA-Z0-9_.-]+)", line)
                if match:
                    deps.append((match.group(1).lower(), "", str(path)))

        return deps

    def _parse_pyproject_toml(
        self, path: Path
    ) -> list[tuple[str, str, str]]:
        """Parse dependencies from pyproject.toml (basic parser, no toml lib needed)."""
        deps: list[tuple[str, str, str]] = []
        try:
            content = path.read_text(encoding="utf-8")
        except OSError:
            return deps

        # Simple regex extraction of dependencies array
        # Match lines like: "click>=8.0", "rich>=13.0"
        in_deps = False
        for line in content.splitlines():
            stripped = line.strip()
            if stripped.startswith("dependencies") and "=" in stripped:
                in_deps = True
                continue
            if in_deps:
                if stripped == "]":
                    in_deps = False
                    continue
                # Extract "package>=version" from quoted string
                match = re.search(
                    r'"([a-zA-Z0-9_.-]+)\s*([=~<>!]+)\s*([^"]+)"', stripped
                )
                if match:
                    name, _, version = match.groups()
                    deps.append((name.lower(), version.strip(), str(path)))

        return deps

    def _get_pip_packages(self) -> list[tuple[str, str, str]]:
        """Get installed packages from pip."""
        try:
            result = subprocess.run(
                [sys.executable, "-m", "pip", "list", "--format=json"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                packages = json.loads(result.stdout)
                return [
                    (p["name"].lower(), p["version"], "pip")
                    for p in packages
                ]
        except (subprocess.TimeoutExpired, json.JSONDecodeError, KeyError):
            pass
        return []

    def _get_latest_version(self, package_name: str) -> str | None:
        """Query PyPI for the latest version of a package."""
        if httpx is None:
            return None
        try:
            resp = httpx.get(
                f"https://pypi.org/pypi/{package_name}/json",
                timeout=10.0,
                follow_redirects=True,
            )
            if resp.status_code == 200:
                data = resp.json()
                return data.get("info", {}).get("version")
        except (httpx.HTTPError, KeyError, ValueError):
            pass
        return None

    def summary(self, deps: list[DependencyInfo]) -> dict[str, Any]:
        """Generate dependency age summary."""
        outdated = [d for d in deps if d.is_outdated]
        by_severity: dict[str, int] = {}
        for d in outdated:
            by_severity[d.severity] = by_severity.get(d.severity, 0) + 1

        return {
            "total_dependencies": len(deps),
            "outdated_count": len(outdated),
            "current_count": len(deps) - len(outdated),
            "critical_outdated": by_severity.get("critical", 0),
            "high_outdated": by_severity.get("high", 0),
            "medium_outdated": by_severity.get("medium", 0),
            "low_outdated": by_severity.get("low", 0),
        }
