# 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/>.

import re
from typing import Optional, Tuple, Union

import click
import requests
from looseversion import LooseVersion

from gophian.error import GophianError
from gophian.new import NewPackage
from gophian.salsa import salsa_repo_exists
from gophian.trace import trace


DEB_EXT = re.compile(r"([\+~](debian|dfsg|ds|deb)(\.)?(\d+)?)?-([A-Za-z0-9\+\.~]+)$")


class DebianPackage:
    """
    Class representing a Debian source package (can have more than one binary
    package).
    """

    def __init__(self, session: requests.Session, source: str) -> None:
        self.session = session
        self.source = source
        pass

    def is_in_package_tracker(self) -> Optional[str]:
        """
        Check if the Debian package can be found at tracker.debian.org.

        Returns URL of package tracker if found.
        """
        res = self.session.head(
            "https://tracker.debian.org/pkg/" + self.source, timeout=60
        )
        if res.status_code != 404:
            return "https://tracker.debian.org/pkg/" + self.source
        else:
            return None

    def is_in_new_queue(self) -> Optional[str]:
        """
        Check if the Debian package is in the NEW queue.

        Returns NewPackage object if found.
        """
        new_package = NewPackage.get(self.session, self.source)
        if new_package:
            return DEB_EXT.sub("", new_package.version)

    def unstable_version(self) -> Optional[Tuple[str, str]]:
        """
        Check if the Debian package is currently in unstable.
        Useful to check if a package has been removed.
        """
        res = self.session.get(
            "https://sources.debian.org/api/src/" + self.source + "/", timeout=60
        )
        try:
            data = res.json()
        except requests.JSONDecodeError:
            print(res.status_code, res.text)
            raise JSONDecodeError(
                f"Failed to decode JSON from sources.d.o response (package: {self.source})"
            )
        # sources api returns status code 200 but body {error:404}
        if res.status_code == 404 or ("error" in data and data["error"] == 404):
            return
        in_unstable = "sid" in data["versions"][0]["suites"]
        in_experimental = "experimental" in data["versions"][0]["suites"]
        if in_unstable or in_experimental:
            return DEB_EXT.sub("", data["versions"][0]["version"]), "experimental" if (
                in_experimental and not in_unstable
            ) else "unstable"


class DebianGolangPackages:
    """
    Class representing the list of all Debian Go packages.
    """

    def __init__(self, session: requests.Session):
        self.session = session

        def filter_dbgsym(package):
            return not package["binary"].endswith("-dbgsym")

        trace("Fetching list of Go packages in Debian")
        self.packages = list(
            filter(
                filter_dbgsym,
                self.session.get(
                    "https://api.ftp-master.debian.org/binary/by_metadata/Go-Import-Path",
                    timeout=60,
                ).json(),
            )
        )

    def is_packaged(self, go_package: str) -> Union[str, None]:
        """
        Check if the given Go package has been packaged for Debian.
        """
        for deb_package in self.packages:
            importpaths = list(
                map(lambda x: x.strip(), deb_package["metadata_value"].split(","))
            )
            if (
                len(
                    list(
                        filter(
                            lambda i: i == go_package
                            or i.startswith(deb_package["metadata_value"] + "/"),
                            importpaths,
                        )
                    )
                )
                > 0
            ):
                return deb_package["source"]
        return None

    def library_is_packaged(
        self, go_package: str
    ) -> Optional[Tuple[str, LooseVersion, str]]:
        """
        Check if the given Go library has been packaged for Debian.
        """
        trace("Checking if package is in Debian: " + go_package)
        for deb_package_data in self.packages:
            if not deb_package_data["binary"].endswith("-dev"):
                continue
            if deb_package_data[
                "metadata_value"
            ] == go_package or go_package.startswith(
                deb_package_data["metadata_value"] + "/"
            ):
                deb_package = DebianPackage(self.session, deb_package_data["source"])
                trace("Found package in Debian: " + deb_package.source)
                if result := deb_package.unstable_version():
                    trace("Package is in unstable: " + deb_package.source)
                    return (
                        deb_package_data["binary"],
                        LooseVersion(result[0]),
                        result[1],
                    )
                elif version := deb_package.is_in_new_queue():
                    trace("Package is in NEW: " + deb_package.source)
                    return deb_package_data["binary"], LooseVersion(version), "NEW"
        return None

    def _check_for_package(self, package_name: str, quiet: bool = False) -> bool:
        if debian_package := self.is_packaged(package_name):
            if not quiet:
                click.secho(
                    f"Already packaged for Debian: {debian_package}", fg="yellow"
                )
                package = DebianPackage(self.session, debian_package)
                if package.is_in_package_tracker():
                    if result := package.unstable_version():
                        click.echo(
                            "Latest version in Debian repos is "
                            + result[0]
                            + f" ({result[1]})"
                        )
                        click.echo("https://tracker.debian.org/pkg/" + debian_package)
                    else:
                        click.echo("Package was removed from Debian unstable")
                        if salsa_repo_exists(
                            self.session, "go-team/packages/" + package.source
                        ):
                            click.echo(
                                "The Salsa repo for the package still exists though:"
                            )
                            click.echo(
                                "https://salsa.debian.org/go-team/packages/"
                                + package.source
                            )
                elif new_package := NewPackage.get(self.session, debian_package):
                    click.echo("Waiting in NEW queue: " + new_package.url)
                else:
                    click.echo(
                        "Could not find the package on the Package Tracker, nor in the NEW queue"
                    )
            return True
        else:
            return False


class JSONDecodeError(GophianError):
    pass
