"""Main Code."""

import asyncio
import sys
from pathlib import Path
from typing import Annotated
from typing import Any

import httpx
import tomllib
from cyclopts import App
from cyclopts import Parameter
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion
from packaging.version import parse as parse_version

from .cache import PyPICache
from .cache import get_default_cache

# Create the Cyclopts app
app = App()


def _setup_cache(cache: bool, clear_cache: bool, short: bool) -> PyPICache:
    """Set up and configure cache instance."""
    cache_instance = get_default_cache()
    cache_instance.enabled = cache

    if clear_cache:
        cleared = cache_instance.clear()
        if not short:
            print(f"Cleared {cleared} cache entries")

    return cache_instance


def _show_cache_stats(cache_instance: PyPICache, cache_stats: bool, short: bool) -> None:
    """Show cache statistics if requested."""
    if cache_stats and not short:
        stats = cache_instance.get_stats()
        print("\nCache Statistics:")
        print(f"  Enabled: {stats['enabled']}")
        print(f"  Hits: {stats['hits']}")
        print(f"  Misses: {stats['misses']}")
        print(f"  Expired: {stats['expired']}")
        print(f"  Errors: {stats['errors']}")
        print(f"  Total entries: {stats['total_entries']}")
        print(f"  Cache directory: {stats['cache_dir']}")


async def _check_dependency_groups(
    data: dict,
    short: bool,
    compat_ok: bool,
    client: httpx.AsyncClient,
    cache_instance: PyPICache,
) -> bool:
    """Check all dependency groups and return True if updates needed."""
    needs_update = False
    dep_groups = data.get("dependency-groups", {})

    for group_name, dependencies in dep_groups.items():
        if not short:
            print(f"\nChecking dependency group [{group_name}]")

        if await process_dependencies(
            dependencies=dependencies,
            short=short,
            client=client,
            compat_ok=compat_ok,
            cache=cache_instance,
        ):
            needs_update = True

    return needs_update


@app.default
async def check(  # noqa: PLR0913
    pyproject_toml: Annotated[
        Path,
        Parameter(help="Full path to pyproject.toml file to check."),
    ] = Path("./pyproject.toml"),
    short: Annotated[
        bool,
        Parameter(help="Should only the shortest possible output be produced."),
    ] = False,
    groups: Annotated[
        bool,
        Parameter(help="Should dependencies in groups be checked."),
    ] = False,
    compat_ok: Annotated[
        bool,
        Parameter(help="Compatible versions are considered OK and won't cause an exit code of 1"),
    ] = False,
    cache: Annotated[
        bool,
        Parameter(help="Enable caching of PyPI API responses (default: False)."),
    ] = False,
    clear_cache: Annotated[
        bool,
        Parameter(help="Clear all cache entries before checking."),
    ] = False,
    cache_stats: Annotated[
        bool,
        Parameter(help="Show cache statistics after checking."),
    ] = False,
) -> None:
    """Check listed dependencies against latest versions available on Pypi.org."""
    if not short:
        print(f"Checking dependencies in {pyproject_toml.resolve()}")

    with pyproject_toml.open("rb") as definitions:
        data = tomllib.load(definitions)

    exit_code: int = 0

    # Handle cache options
    cache_instance = _setup_cache(cache, clear_cache, short)

    # Check main dependencies
    dependencies = data.get("project", {}).get("dependencies")

    async with httpx.AsyncClient() as client:
        if await process_dependencies(
            dependencies=dependencies,
            short=short,
            client=client,
            compat_ok=compat_ok,
            cache=cache_instance,
        ):
            exit_code = 1

        # Check dependency groups if requested
        if groups:
            if await _check_dependency_groups(data, short, compat_ok, client, cache_instance):
                exit_code = 1

    # Show cache statistics if requested
    _show_cache_stats(cache_instance, cache_stats, short)

    sys.exit(exit_code)


async def process_dependencies(
    dependencies: list[Any],
    short: bool,
    compat_ok: bool,
    client: httpx.AsyncClient,
    cache: PyPICache | None = None,
) -> bool:
    """Check all dependencies for updates concurrently.

    Return True if incompatible update is available or if compat_ok is False on any update available.
    """
    if not dependencies:
        dependencies = []

    remote_deps = [dep for dep in dependencies if not is_local_dependency(dependency=dep)]

    results = await asyncio.gather(
        *[
            check_dependency(dependency=dep, short=short, compat_ok=compat_ok, client=client, cache=cache)
            for dep in remote_deps
        ]
    )

    flag_update = False
    for output, flag in results:
        if output is not None:
            print(output)
        flag_update |= flag

    return flag_update


def parse_dependency(dependency: str) -> tuple[str, str, str | None]:
    """Parse a dependency string into (package_name, version_constraint, environment_marker)."""
    try:
        req = Requirement(dependency)
        return (req.name, str(req.specifier) if req.specifier else "", str(req.marker) if req.marker else None)
    except InvalidRequirement as e:
        raise ValueError(f"Invalid dependency format '{dependency}'") from e


def is_version_compatible(current_spec: str, latest_version: str) -> bool:
    """Check if the latest version satisfies the current version constraints."""
    if not current_spec:
        return True  # No constraints means any version is acceptable

    try:
        specifier = SpecifierSet(current_spec)
        return parse_version(latest_version) in specifier
    except InvalidVersion:
        return False


def is_latest_version(current_spec: str, latest_version: str) -> bool:
    """Check if the latest version has been specified."""
    if not current_spec:
        return True  # No constraints means any version is acceptable

    try:
        specifier = SpecifierSet(current_spec)
        latest_specified = False
        for spec in specifier:
            if latest_version == spec.version:
                latest_specified = True
        return latest_specified
    except InvalidVersion:
        return False


async def check_dependency(
    dependency: str,
    short: bool,
    compat_ok: bool,
    client: httpx.AsyncClient,
    cache: PyPICache | None = None,
) -> tuple[str | None, bool]:
    """Check if a dependency needs an update.

    Returns (output_line, needs_update). output_line is None when there is no output to show.
    needs_update is True when a newer version is available and outside version spec, or compat_ok
    is False and a newer compatible version exists.
    """
    try:
        package_name, version_spec, _ = parse_dependency(dependency)

        if not version_spec:
            return (None, False)

        latest_version = await get_latest_pypi_version(package_name=package_name, client=client, cache=cache)

        is_compatible = is_version_compatible(version_spec, latest_version)
        is_latest = is_latest_version(current_spec=version_spec, latest_version=latest_version)

        if not is_compatible:
            return (f"\u274c {package_name}: {version_spec} \u2192 Latest: {latest_version}", True)

        if not is_latest:
            flag_update_available = not compat_ok
            return (
                f"\u26a0\ufe0f {package_name}: {version_spec} \u2192 Latest: {latest_version}",
                flag_update_available,
            )

        output = f"\u2705\ufe0f {package_name}{version_spec} is up to date" if not short else None
        return (output, False)

    except ValueError as e:
        return (f"❌ Error checking {dependency}: {e!r}", False)
    except (httpx.HTTPStatusError, httpx.RequestError) as e:
        return (f"❌ Network error for {package_name}: {e!r}", False)


def is_local_dependency(dependency: str) -> bool:
    """Check if dependency is a local path or URL."""
    if not dependency:
        return True

    return (
        " @" in dependency
        or ("/" in dependency and "://" not in dependency)
        or any(dependency.startswith(prefix) for prefix in ["file://", "https://", "http://", "git+https://"])
    )


async def get_latest_pypi_version(package_name: str, client: httpx.AsyncClient, cache: PyPICache | None = None) -> str:
    """Get latest version number of a package on Pypi.org."""
    # Try to get from cache first
    if cache and cache.enabled:
        cached_data = cache.get(package_name)
        if cached_data and "version" in cached_data:
            return str(cached_data["version"])

    # Fetch from PyPI
    response = await client.get(f"https://pypi.org/pypi/{package_name}/json")
    response.raise_for_status()
    response_data = response.json()
    latest_version = str(response_data["info"]["version"])

    # Cache the result
    if cache and cache.enabled:
        cache_data = {
            "version": latest_version,
            "package_info": response_data["info"],
        }
        cache.set(package_name, cache_data)

    return latest_version


def main() -> None:
    """Run actual processing."""
    app()
