import logging
import re
from copy import (
    deepcopy,
)

from pydantic import (
    BaseModel,
    ValidationError,
)

from labels.artifact.relationship import (
    Relationship,
)
from labels.file.location import (
    Location,
)
from labels.file.location_read_closer import (
    LocationReadCloser,
)
from labels.file.resolver import (
    Resolver,
)
from labels.model.core import (
    Language,
    Package,
    PackageType,
)
from labels.pkg.cataloger.generic.parser import (
    Environment,
)
from labels.pkg.cataloger.java.model import (
    JavaArchive,
    JavaPomProject,
)
from labels.pkg.cataloger.java.package import (
    package_url,
)
from labels.utils.strings import format_exception

LOGGER = logging.getLogger(__name__)
# Constants
QUOTE = r'["\']'
NL = r"(\n?\s*)?"
TEXT = r'[^"\']+'
RE_LINE_COMMENT: re.Pattern[str] = re.compile(r"^.*" rf"{NL}//" r".*$")  # noqa: ISC001


def is_comment(line: str) -> bool:
    return (
        line.strip().startswith("//")
        or line.strip().startswith("/*")
        or line.strip().endswith("*/")
    )


class LockFileDependency(BaseModel):
    group: str
    name: str
    version: str
    line: int | None = None


def create_java_archive(
    dependency: LockFileDependency,
    name: str,
    version: str,
) -> JavaArchive:
    return JavaArchive(
        pom_project=JavaPomProject(
            group_id=dependency.group,
            name=name,
            artifact_id=name,
            version=version,
        ),
    )


def create_package(
    name: str,
    version: str,
    location: Location,
    archive: JavaArchive,
) -> Package:
    return Package(
        name=name,
        version=version,
        locations=[location],
        language=Language.JAVA,
        type=PackageType.JavaPkg,
        metadata=archive,
        p_url=package_url(name, version, archive),
        licenses=[],
    )


def avoid_cmt(line: str, *, is_block_cmt: bool) -> tuple[str, bool]:
    if RE_LINE_COMMENT.match(line):
        line = line.split("//", 1)[0]
    if is_block_cmt:
        if "*/" in line:
            is_block_cmt = False
            line = line.split("*/", 1).pop()
        else:
            return "", is_block_cmt
    if "/*" in line:
        line_cmt_open = line.split("/*", 1)[0]
        if "*/" in line:
            line = line_cmt_open + line.split("*/", 1).pop()
        else:
            line = line_cmt_open
            is_block_cmt = True
    return line, is_block_cmt


def get_line_number(content: str, match_start: int) -> int:
    return content[:match_start].count("\n") + 2


def extract_gradle_configs(content: str) -> set[str]:
    config_pattern = re.compile(r"configurations\s*\{([^}]+)\}", re.DOTALL)
    custom_config_pattern = re.compile(r"\s*(\w+)\s*")
    configs = {
        "runtimeOnly",
        "api",
        "compile",
        "compileOnly",
        "implementation",
    }

    config_blocks = config_pattern.findall(content)
    for block in config_blocks:
        custom_configs = custom_config_pattern.findall(block)
        configs.update(custom_configs)

    return configs


def build_regex_with_configs(configs: set[str]) -> dict[str, re.Pattern[str]]:
    config_pattern = "|".join(configs)

    return {
        "RE_GRADLE_A": re.compile(
            rf"^{NL}(?:{config_pattern}){NL}[(]?{NL}"
            rf"group{NL}:{NL}{QUOTE}(?P<group>{TEXT}){QUOTE}{NL},"
            rf"{NL}name{NL}:{NL}{QUOTE}(?P<name>{TEXT}){QUOTE}{NL}"
            rf"(?:,{NL}version{NL}:{NL}{QUOTE}(?P<version>{TEXT}){QUOTE}{NL})"
            rf"?.*$",
        ),
        "RE_GRADLE_B": re.compile(
            rf"^.*{NL}(?:{config_pattern}){NL}[(]?{NL}{QUOTE}(?P<statement>{TEXT}){QUOTE}",
        ),
        "RE_GRADLE_C": re.compile(
            rf"{NL}(?:{config_pattern}){NL}\("
            rf"{NL}{QUOTE}(?P<statement>{TEXT}){QUOTE}{NL}\)"
            rf"{NL}{{({NL})version{NL}{{({NL})strictly{NL}\({NL}"
            rf"{QUOTE}(?P<version>{TEXT}){QUOTE}{NL}\){NL}}}{NL}}}",
            re.DOTALL,
        ),
        "BLOCK": re.compile(
            rf"{NL}(?:{config_pattern}){NL}\("
            rf"{NL}{QUOTE}(?P<statement>{TEXT}){QUOTE}{NL}\)"
            rf"{NL}\{{(.*?version{NL}\{{.*?\}}){NL}\}}",
            re.DOTALL,
        ),
        "VERSION": re.compile(
            rf"version{NL}{{({NL})strictly{NL}\("
            rf"{NL}{QUOTE}(?P<version>{TEXT}){QUOTE}{NL}\){NL}}}",
            re.DOTALL,
        ),
    }


def get_block_deps(content: str, regexes: dict[str, re.Pattern[str]]) -> list[LockFileDependency]:
    dependencies = []

    for block in regexes["BLOCK"].finditer(content):
        product = block.group("statement")
        hit = regexes["VERSION"].search(block.group())
        if not hit:
            continue

        version = hit.group("version")
        if version == "":
            continue

        line_no = get_line_number(content, block.start())

        dependencies.append(
            LockFileDependency(
                group=product.split(":")[0],
                name=product,
                version=version,
                line=line_no,
            ),
        )
    return dependencies


def parse_dependencies(reader: LocationReadCloser) -> list[LockFileDependency]:
    content = reader.read_closer.read()
    configs = extract_gradle_configs(content)
    maven_regexes = build_regex_with_configs(configs)
    dependencies: list[LockFileDependency] = get_block_deps(content, maven_regexes)

    is_block_cmt = False
    for line_no, raw_line in enumerate(content.splitlines(), start=1):
        line, is_block_cmt = avoid_cmt(raw_line, is_block_cmt=is_block_cmt)

        if match := maven_regexes["RE_GRADLE_A"].match(line):
            group = match.group("group")
            product = group + ":" + match.group("name")
            version = match.group("version") or ""
        elif match := maven_regexes["RE_GRADLE_B"].match(line):
            statement = match.group("statement")
            product, version = (
                statement.rsplit(":", maxsplit=1) if statement.count(":") >= 2 else (statement, "")
            )
            group = product.split(":")[0]
        else:
            continue

        # Assuming a wildcard in Maven if the version is not found can
        # result in issues.
        # https://gitlab.com/fluidattacks/universe/-/issues/5635
        if version == "" or re.match(r"\${.*}", version):
            continue

        dependencies.append(
            LockFileDependency(
                group=group,
                name=product,
                version=version,
                line=line_no,
            ),
        )

    return dependencies


def create_packages(
    dependencies: list[LockFileDependency],
    reader_location: Location,
) -> list[Package]:
    packages: list[Package] = []

    for dependency in dependencies:
        name = dependency.name
        version = dependency.version

        if not name or not version:
            continue

        location = deepcopy(reader_location)
        if location.coordinates:
            location.coordinates.line = dependency.line

        archive = create_java_archive(dependency, name, version)

        try:
            package = create_package(name, version, location, archive)
            packages.append(package)
        except ValidationError as ex:
            LOGGER.warning(
                "Malformed package. Required fields are missing or data types are incorrect.",
                extra={
                    "extra": {
                        "exception": format_exception(str(ex)),
                        "location": location.path(),
                    },
                },
            )

    return packages


def parse_gradle(
    _resolver: Resolver | None,
    __: Environment | None,
    reader: LocationReadCloser,
) -> tuple[list[Package], list[Relationship]]:
    dependencies = parse_dependencies(reader)
    packages = create_packages(dependencies, reader.location)
    return packages, []
