#
# Copyright (c) 2021-2026 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# 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 <http://www.gnu.org/licenses/>.
#
from collections.abc import Iterator
from pathlib import Path

from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging
from ahriman.core.utils import check_output, package_like
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths


class Task(LazyLogging):
    """
    base package build task

    Attributes:
        archbuild_flags(list[str]): command flags for archbuild command
        build_command(list[str]): build command
        include_debug_packages(bool): whether to include debug packages or not
        make_flags(str): MAKEFLAGS variable for makepkg command
        makechrootpkg_flags(list[str]): command flags for makechrootpkg command
        makepkg_flags(list[str]): command flags for makepkg command
        package(Package): package definitions
        paths(RepositoryPaths): repository paths instance
        repository_id(RepositoryId): repository unique identifier
        uid(int): uid of the repository owner user
    """

    def __init__(self, package: Package, configuration: Configuration, repository_id: RepositoryId,
                 paths: RepositoryPaths) -> None:
        """
        Args:
            package(Package): package definitions
            configuration(Configuration): configuration instance
            repository_id(RepositoryId): repository unique identifier
            paths(RepositoryPaths): repository paths instance
        """
        self.package = package
        self.paths = paths
        self.uid, _ = paths.root_owner
        self.repository_id = repository_id

        self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
        self.build_command = configuration.getlist("build", "devtools_wrapper")
        self._legacy_build_command = configuration.getlist("build", "build_command", fallback=[])
        self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True)
        # even though this option is declared as list, there is no need to read it as list,
        # because it will be converted back to the string anyway
        self.make_flags = configuration.get("build", "make_flags", fallback=None)
        self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
        self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])

    def _package_archives(self, sources_dir: Path, source_files: list[Path]) -> list[Path]:
        """
        extract package archives from the directory

        Args:
            sources_dir(Path): path to where sources are
            source_files(list[Path]): list of files which were initially in the directory

        Returns:
            list[Path]: list of file paths which looks like freshly generated archives
        """
        def files() -> Iterator[Path]:
            for filepath in sources_dir.iterdir():
                if filepath in source_files:
                    continue  # skip files which were already there
                if filepath.suffix == ".log":
                    continue  # skip log files
                if not package_like(filepath):
                    continue  # path doesn't look like a package
                yield filepath

        # debug packages are always formed as package.base-debug
        # see /usr/share/makepkg/util/pkgbuild.sh for more details
        debug_package_prefix = f"{self.package.base}-debug-"
        return [
            package
            for package in files()
            if self.include_debug_packages or not package.name.startswith(debug_package_prefix)
        ]

    def build(self, sources_dir: Path, *, dry_run: bool = False, **kwargs: str | None) -> list[Path]:
        """
        run package build

        Args:
            sources_dir(Path): path to where sources are
            dry_run(bool, optional): do not perform build itself (Default value = False)
            **kwargs(str | None): environment variables to be passed to build processes

        Returns:
            list[Path]: paths of produced packages
        """
        command = self._legacy_build_command[:]
        if not command:
            command = self.build_command + ["-r", self.repository_id.name, "-a", self.repository_id.architecture, "--"]

        command.extend(["-r", str(self.paths.chroot)] + self.archbuild_flags)  # archbuild flags
        command.extend(["--", "-D", str(self.paths.archive)] + self.makechrootpkg_flags)  # makechrootpkg flags
        command.extend(["--"] + self.makepkg_flags)  # makepkg flags
        if dry_run:
            command.extend(["--nobuild"])

        self.logger.info("using %s for %s", command, self.package.base)

        environment: dict[str, str] = {
            key: value
            for key, value in kwargs.items()
            if value is not None
        }
        if self.make_flags is not None:
            environment["MAKEFLAGS"] = self.make_flags
        self.logger.info("using environment variables %s", environment)

        source_files = list(sources_dir.iterdir())
        check_output(
            *command,
            exception=BuildError.from_process(self.package.base),
            cwd=sources_dir,
            logger=self.logger,
            user=self.uid,
            environment=environment,
        )

        return self._package_archives(sources_dir, source_files)

    def init(self, sources_dir: Path, patches: list[PkgbuildPatch], local_version: str | None) -> str | None:
        """
        fetch package from git

        Args:
            sources_dir(Path): local path to fetch
            patches(list[PkgbuildPatch]): list of patches for the package
            local_version(str | None): local version of the package. If set and equal to current version, it will
                automatically bump pkgrel

        Returns:
            str | None: current commit sha if available
        """
        last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths)
        if self.package.is_vcs:  # if package is VCS, then make sure to update PKGBUILD to the latest version
            self.build(sources_dir, dry_run=True)

        if local_version is None:  # there is no local package or pkgrel increment is disabled
            return last_commit_sha

        # load fresh package
        loaded_package = Package.from_build(sources_dir, self.repository_id.architecture, None)
        if (pkgrel := loaded_package.next_pkgrel(local_version)) is not None:
            self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
            patch = PkgbuildPatch("pkgrel", pkgrel)
            patch.write(sources_dir / "PKGBUILD")

        return last_commit_sha
