"""Parser for pyproject.toml files.

Extracts dependencies from [project.dependencies] and all
[project.optional-dependencies] groups, returning normalized PEP 508
requirement strings.
"""

from __future__ import annotations

import logging
import os
import sys
from pathlib import Path
from typing import Any

from packaging.requirements import InvalidRequirement, Requirement

logger = logging.getLogger(__name__)


def _toml_loads(text: str, source_path: Path) -> dict[str, Any]:
    """Parse *text* as TOML and return the top-level mapping.

    Uses ``tomllib`` from the standard library on Python 3.11+ and falls back
    to the ``tomli`` backport on Python 3.10.

    Args:
        text:        Raw TOML text to parse.
        source_path: File path used in error messages only.

    Returns:
        A dictionary representing the top-level TOML document.

    Raises:
        ValueError: If the text is not valid TOML.
    """
    if sys.version_info >= (3, 11):
        import tomllib  # noqa: PLC0415

        try:
            return tomllib.loads(text)
        except tomllib.TOMLDecodeError as exc:
            raise ValueError(f"TOML syntax error in {source_path}: {exc}") from exc
    else:
        try:
            import tomli  # type: ignore[import-untyped]  # noqa: PLC0415
        except ImportError as exc:  # pragma: no cover
            raise ImportError(
                "tomli>=2.0 is required on Python < 3.11. Install it with: pip install tomli"
            ) from exc

        try:
            result: dict[str, Any] = tomli.loads(text)  # type: ignore[attr-defined]
            return result  # type: ignore[return-value]
        except Exception as exc:
            raise ValueError(f"TOML syntax error in {source_path}: {exc}") from exc


def parse_pyproject(source: str | os.PathLike[str]) -> list[str]:
    """Parse a pyproject.toml file and return a list of requirement strings.

    Extracts all entries from ``[project.dependencies]`` and every group
    listed under ``[project.optional-dependencies]``.  Duplicate entries
    across groups are *not* de-duplicated — the caller is responsible for
    that if needed.

    Args:
        source: Path to the ``pyproject.toml`` file (str or Path-like).

    Returns:
        A list of normalised PEP 508 requirement strings, e.g.
        ``["httpx>=0.27.0", "tomli>=2.0; python_version < '3.11'"]``.

    Raises:
        FileNotFoundError: If the file does not exist.
        ValueError: If the ``[project]`` section is absent, the
            ``[project.dependencies]`` key is absent, the TOML is
            syntactically invalid, or a dependency string is not a valid
            PEP 508 requirement.
    """
    path = Path(source).resolve()

    if not path.exists():
        raise FileNotFoundError(f"pyproject.toml not found: {path}")

    logger.debug("Parsing pyproject.toml: %s", path)

    raw_bytes = path.read_bytes()
    data: dict[str, Any] = _toml_loads(raw_bytes.decode("utf-8"), path)

    # Validate presence of [project] table
    if "project" not in data:
        raise ValueError(
            f"[project] table is missing from {path}. "
            "This file does not appear to be a PEP 517/518 pyproject.toml."
        )

    project: dict[str, Any] = data["project"]

    # Validate presence of [project.dependencies]
    if "dependencies" not in project:
        raise ValueError(
            f"[project.dependencies] is missing from {path}. "
            "Add a 'dependencies' list under the [project] table."
        )

    raw_deps: list[str] = project["dependencies"]

    requirements: list[str] = []

    # Parse core dependencies
    for dep in raw_deps:
        requirements.append(_validate_requirement(dep, path))

    logger.debug("Found %d core dependencies in %s", len(raw_deps), path)

    # Parse optional dependency groups
    optional: dict[str, list[str]] = project.get("optional-dependencies", {})
    for group_name, group_deps in optional.items():
        logger.debug(
            "Including optional-dependencies group %r (%d entries) from %s",
            group_name,
            len(group_deps),
            path,
        )
        for dep in group_deps:
            requirements.append(_validate_requirement(dep, path))

    return requirements


def _validate_requirement(dep: str, path: Path) -> str:
    """Validate *dep* as a PEP 508 requirement string and return its normalised form.

    Args:
        dep:  Raw dependency string from the TOML file.
        path: Source file path — included in error messages.

    Returns:
        The normalised requirement string produced by
        :class:`packaging.requirements.Requirement`.

    Raises:
        ValueError: If *dep* is not a valid PEP 508 requirement.
    """
    try:
        req = Requirement(dep)
    except InvalidRequirement as exc:
        raise ValueError(f"Invalid PEP 508 requirement {dep!r} in {path}: {exc}") from exc
    return str(req)
