import re
from collections.abc import Iterable

from gemfileparser import Dependency, GemfileParser

from labels.model.file import Location, 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.ruby.package_builder import new_gem_package
from labels.parsers.cataloger.ruby.utils import format_ruby_version_constraints
from labels.parsers.cataloger.utils import get_enriched_location

GEMFILE_DEP: re.Pattern[str] = re.compile(
    r'^\s*(?P<gem>gem [\'"].*?[\'"],?( [\'"][><~=]{0,2}\s?[\d\.]+[\'"],?){0,2})',
)
NOT_PROD_DEP: re.Pattern[str] = re.compile(
    r":group => \[?[:\w\-, ]*(:development|:test)",
)
NOT_PROD_GROUP: re.Pattern[str] = re.compile(r"(\s*)group :(test|development)")
GEM_LOCK_DEP: re.Pattern[str] = re.compile(
    r"^\s{4}(?P<gem>[^\s]*)\s\([^\d]*(?P<version>.*)\)$",
)


def parse_gemfile(
    _resolver: Resolver | None,
    _environment: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    packages = _collect_packages(reader.read_closer.read(), reader)

    return packages, []


def _collect_packages(file_content: str, reader: LocationReadCloser) -> list[Package]:
    packages: list[Package] = []
    in_group_block = False
    end_line = ""

    for line_number, line in enumerate(file_content.splitlines(), 1):
        if in_group_block:
            if line == end_line:
                in_group_block = False
            elif (
                (matched := GEMFILE_DEP.search(line))
                and matched
                and (dependency := _create_dependency(line_number, reader.location, matched, line))
            ):
                packages.append(dependency)
            continue

        if match_group := NOT_PROD_GROUP.search(line):
            in_group_block = True
            end_line = f"{match_group.group(1)}end"
            continue

        if (
            (matched := GEMFILE_DEP.search(line))
            and matched
            and (dependency := _create_dependency(line_number, reader.location, matched, line))
        ):
            packages.append(dependency)

    return packages


def _create_dependency(
    line_number: int,
    location: Location,
    matched: re.Match[str],
    line: str,
) -> Package | None:
    gem_info = GemfileParser.preprocess(matched.group("gem"))[3:]
    product, version = _parse_line(gem_info)

    is_dev = _is_dev_dependency(line)

    new_location = get_enriched_location(
        location,
        line=line_number,
        is_dev=is_dev,
        is_transitive=False,
        package_manager=PackageManager.BUNDLER,
    )

    return new_gem_package(product, version, new_location)


def _parse_line(in_line: str) -> tuple[str, str]:
    line: list[str] = []
    line = in_line.split(",")

    column_list: list[str] = []
    for column in line:
        stripped_column = (
            column.replace("'", "")
            .replace('"', "")
            .replace("%q<", "")
            .replace("(", "")
            .replace(")", "")
            .replace("[", "")
            .replace("]", "")
            .strip()
        )
        column_list.append(stripped_column)

    deps = _match_dep_criteria(column_list)
    product: str = deps.name
    version: str = format_ruby_version_constraints(deps.requirement)
    return product, version


def _match_dep_criteria(
    column_list: Iterable[str],
) -> Dependency:
    dep = Dependency()
    for column in column_list:
        for criteria, criteria_regex in GemfileParser.gemfile_regexes.items():
            match = criteria_regex.match(column)
            if match:
                if criteria == "requirement":
                    dep.requirement.append(match.group(criteria))
                else:
                    setattr(dep, criteria, match.group(criteria))
                break
    return dep


def _is_dev_dependency(line: str) -> bool:
    return bool(NOT_PROD_DEP.search(line))
