# gophian -- tools to help with Debianizing Go software
# Copyright (C) 2024-2025 Maytham Alsudany <maytha8thedev@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from pathlib import Path
import re
import subprocess
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

import click
from looseversion import LooseVersion

from gophian.error import GophianError
from gophian.git import Git
from gophian.trace import trace
from gophian.vcs import package_from_import_path

if TYPE_CHECKING:
    from gophian.session import Session

GO_MOD_MODULE_REGEX = re.compile(r"^module (\S+)$")
GO_MOD_GO_REGEX = re.compile(r"^go (\d[\d\.]*)$")
GO_MOD_REQUIREMENT_REGEX = re.compile(r"^(\S+) (\S+)(?: //.*)?$")
GO_MOD_NONVERSIONED_REGEX = re.compile(r"^(\d[\d\.]*)?-(\d{14})-([a-z\d]{12})$")

GO_ERROR_NO_MATCHING_FILES = re.compile(
    r"^([^:]+):(\d+):\d+: pattern [^:]+: no matching files found$"
)

IGNORE_LIST = (Path(__file__).parent / "ignore.txt").read_text().splitlines()


class GoPackage(Git):
    def __init__(self, session: "Session", package: str, repo: str) -> None:
        self.package = package
        self.module = package
        self.path = session.gopath / "src" / package
        self.session = session
        self.repo = repo

        super().__init__(self.path, quiet=True, check=False)

        tags_dict: Dict[str, str] = {}
        all_tags = Git.remote_tags(repo, quiet=True)
        for tag in all_tags:
            if match := re.search(r"^v?(\d[\d\.]+)$", tag, re.MULTILINE):
                tags_dict[match.group(1)] = match.group(0)

        versions = [LooseVersion(tag) for tag in tags_dict.keys()]
        versions.sort()

        if len(versions) > 0:
            version = versions[-1]
            self.commitish = tags_dict[str(version)]
            self.versioned = True
            self.debianized_version = str(version)
            if not self.path.exists():
                self._clone(repo, branch=self.commitish, depth=1)
        else:
            if not self.path.exists():
                self._clone(repo, depth=1)
            iso_date, commit_hash = self.get(["log", "-1", "--format=%cI %H"]).split(
                " "
            )
            date = datetime.fromisoformat(iso_date).strftime("%Y%m%d")
            self.commitish = commit_hash
            self.versioned = False
            self.debianized_version = f"0.0~git{date}.{commit_hash[:7]}"

        self.deps: Dict[str, GoVersion] = {}
        for filename in self.path.rglob("**/go.mod"):
            with filename.open("r") as file:
                inside_require = False
                for i, line in enumerate(file):
                    line = line.strip()
                    if line == "" or line.startswith("//"):
                        continue
                    elif inside_require:
                        if match := GO_MOD_REQUIREMENT_REGEX.match(line):
                            indirect = line.endswith("// indirect")
                            if comment_index := line.find("//") != -1:
                                line = line[:comment_index].strip()
                            if not indirect:
                                try:
                                    dep, _ = package_from_import_path(
                                        self.session.requests_session, match.group(1)
                                    )
                                    self.deps[dep] = GoVersion(match.group(2))
                                except Exception as e:
                                    click.echo(
                                        click.style("Error: ", bold=True, fg="red")
                                        + str(e)
                                    )
                        elif line == ")":
                            inside_require = False
                        else:
                            raise GoModParseError(
                                i + 1, self.path, "Invalid 'require' directive. (84)"
                            )
                    elif line.startswith("require "):
                        line = line[8:]
                        if line.startswith("("):
                            indirect = line.endswith("// indirect")
                            if comment_index := line.find("//") != -1:
                                line = line[:comment_index].strip()
                            if line == "(":
                                inside_require = True
                            else:
                                if line.endswith(")"):
                                    line = line[1:-1]
                                    if match := GO_MOD_REQUIREMENT_REGEX.match(line):
                                        if not indirect:
                                            try:
                                                dep, _ = package_from_import_path(
                                                    self.session.requests_session,
                                                    match.group(1),
                                                )
                                                self.deps[dep] = GoVersion(
                                                    match.group(2)
                                                )
                                            except Exception as e:
                                                click.echo(
                                                    click.style(
                                                        "Error: ", bold=True, fg="red"
                                                    )
                                                    + str(e)
                                                )
                                    else:
                                        raise GoModParseError(
                                            i + 1,
                                            self.path,
                                            "Invalid 'require' directive. (105)",
                                        )
                                else:
                                    if match := GO_MOD_REQUIREMENT_REGEX.match(line):
                                        if not indirect:
                                            try:
                                                dep, _ = package_from_import_path(
                                                    self.session.requests_session,
                                                    match.group(1),
                                                )
                                                self.deps[dep] = GoVersion(
                                                    match.group(2)
                                                )
                                            except Exception as e:
                                                click.echo(
                                                    click.style(
                                                        "Error: ", bold=True, fg="red"
                                                    )
                                                    + str(e)
                                                )
                                    else:
                                        raise GoModParseError(
                                            i + 1,
                                            self.path,
                                            "Invalid 'require' directive. (117)",
                                        )
                    elif match := GO_MOD_MODULE_REGEX.match(line):
                        self.module = match.group(1)
                    elif match := GO_MOD_GO_REGEX.match(line):
                        self.go_version = LooseVersion(match.group(1))

    def find_dependencies(self, test: bool = False) -> List[Tuple[str, str]]:
        all_imports: List[str] = []

        out = subprocess.run(
            [
                "env",
                f"GOPATH={self.session.gopath}",
                "GO111MODULE=off",
                "go",
                "list",
                "-f",
                "{{.TestImports}}" if test else "{{.Imports}}",
                f"{self.package}/...",
            ],
            cwd=(self.session.gopath / "src" / self.package),
            capture_output=True,
        )
        # Ignore errors from go, as they're about missing dependencies in
        # GOPATH (and we don't care about this).
        # Except when an error regarding a go:embed directive affects output.
        redo = False
        for line in out.stderr.splitlines():
            if match := GO_ERROR_NO_MATCHING_FILES.match(line.decode("utf-8")):
                redo = True
                affected_file = match.group(1)
                affected_lineno = int(match.group(2))
                path = self.session.gopath / "src" / self.package / affected_file
                s = path.read_text().splitlines()
                s[affected_lineno - 1] = "// embed removed"
                path.write_text("\n".join(s))
        if redo:
            trace(
                self.package
                + ": Error with go:embed detected, removing problematic directives."
            )
            return self.find_dependencies(test)

        for pkg in out.stdout.splitlines():
            dep_string = pkg.strip().decode()[1:-1]
            all_imports += dep_string.split(" ")

        def filter_deps(dep: str):
            return (
                dep != self.package
                and dep != self.module
                and not dep.startswith(self.package + "/")
                and not dep.startswith(self.module + "/")
                and dep not in self.session.stdlib
                and dep != ""
            )

        lib_imports: List[str] = remove_duplicates(
            list(filter(filter_deps, all_imports))
        )

        deps: List[Tuple[str, str]] = []

        for imp in lib_imports:
            if any(imp.startswith(dep) for dep in deps + IGNORE_LIST):
                continue
            try:
                deps.append(
                    package_from_import_path(self.session.requests_session, imp)
                )
            except Exception as e:
                click.echo(click.style("Error: ", bold=True, fg="red") + str(e))
                continue

        return deps


class GoModParseError(GophianError):
    def __init__(self, line, path, msg) -> None:
        self.line = line
        super().__init__(f"{path}:{line} {msg}")


T = Any


def remove_duplicates(li: List[T]) -> List[T]:
    return list(dict.fromkeys(li))


class GoVersion(LooseVersion):
    def __init__(self, vstring: str) -> None:
        self.commit = False
        if vstring.startswith("v"):
            vstring = vstring[1:]
        if match := GO_MOD_NONVERSIONED_REGEX.match(vstring):
            self.commit = True
            [semver, date, commit] = match.groups()
            if semver == "0.0.0":
                semver = "0.0"
            date = date[:8]
            commit = commit[:7]
            vstring = f"{semver}~git{date}.{commit}"
        super().__init__(vstring)
