from collections.abc import Generator
from contextlib import suppress
from typing import NamedTuple

import requirements

from labels.model.file import LocationReadCloser
from labels.model.package import Package
from labels.model.package_manager import PackageManager
from labels.model.relationship import Relationship
from labels.model.release import Environment
from labels.model.resolver import Resolver
from labels.parsers.cataloger.python.package_builder import new_python_package
from labels.parsers.cataloger.utils import get_enriched_location

OPERATOR_ORDER = {"==": 1, "===": 1, "~=": 1, ">=": 2, ">": 2, "<": 3, "<=": 3}


class PythonRequirementsData(NamedTuple):
    name: str
    version: str
    is_dev: bool


def parse_requirements_txt(
    _resolver: Resolver | None,
    _environment: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    file_content = _safe_parse_requirements_txt(reader)
    if not file_content:
        return [], []

    packages = _collect_packages(file_content, reader)

    return packages, []


def _safe_parse_requirements_txt(reader: LocationReadCloser) -> str | None:
    try:
        return reader.read_closer.read()
    except UnicodeDecodeError:
        return None


def _collect_packages(file_content: str, reader: LocationReadCloser) -> list[Package]:
    packages: list[Package] = []
    deps_found = False
    for line_number, line in _split_lines_requirements(file_content):
        parsed_dependency_data = _get_parsed_dependency(line)

        if not parsed_dependency_data:
            if not deps_found and line_number > 3:
                return packages
            continue

        deps_found = True
        new_location = get_enriched_location(
            reader.location,
            line=line_number,
            is_dev=parsed_dependency_data.is_dev,
            is_transitive=False,
            package_manager=PackageManager.PIP,
        )

        package = new_python_package(
            name=parsed_dependency_data.name,
            version=parsed_dependency_data.version,
            location=new_location,
        )
        if package:  # pragma: no branch
            packages.append(package)

    return packages


def _split_lines_requirements(
    content: str,
) -> Generator[tuple[int, str], None, None]:
    last_line = ""
    line_number = 1
    for index, raw_line in enumerate(content.splitlines(), 1):
        if not last_line:
            line_number = index
        line = _trim_requirements_txt_line(raw_line)
        if last_line != "":
            line = last_line + line
            last_line = ""
        if line.endswith("\\"):
            last_line += line.rstrip("\\")
            continue
        if not line:
            continue

        if any(
            (
                line.startswith("-e"),
                line.startswith("-r"),
                line.startswith("--requirements"),
            ),
        ):
            continue

        yield line_number, line


def _trim_requirements_txt_line(line: str) -> str:
    line = line.strip()

    return _remove_trailing_comment(line)


def _remove_trailing_comment(line: str) -> str:
    parts = line.split("#", 1)
    if len(parts) < 2:
        # there aren't any comments
        return line
    return parts[0]


def _get_parsed_dependency(line: str) -> PythonRequirementsData | None:
    with suppress(Exception):
        parsed_dep = next(iter(requirements.parse(line)))
        if not parsed_dep.specs or not (version := _get_dep_version_range(parsed_dep.specs)):
            return None
        is_dev: bool = False
        if parsed_dep.extras and any("dev" in extra.lower() for extra in parsed_dep.extras):
            is_dev = True
        return PythonRequirementsData(name=str(parsed_dep.name), version=version, is_dev=is_dev)
    return None


def _operator_sort_key(spec: tuple[str, str]) -> int:
    return OPERATOR_ORDER.get(spec[0], 1)


def _get_dep_version_range(dep_specs: list[tuple[str, str]]) -> str | None:
    version_range = ""
    ordered_specs = sorted(dep_specs, key=_operator_sort_key)
    for operator, version in ordered_specs:
        if operator not in OPERATOR_ORDER:
            return None

        if operator in {"==", "~="}:
            version_range = version
            break
        version_range += f"{operator}{version} "
    return version_range.rstrip()
