#!/usr/bin/python

# Copyright 2003-2023 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2


import os
import shutil
import sys

import gentoolkit.pprinter as pp
from portage.emaint.main import TaskHandler
from portage.emaint.modules.binhost import binhost


class CleanUp:
    """Performs all cleaning actions to distfiles or package directories.

    @param controller: a progress output/user interaction controller function
                                       which returns a Boolean to control file deletion
                                       or bypassing/ignoring
    """

    def __init__(self, controller, quiet):
        self.controller = controller
        self.quiet = quiet

    def clean_dist(self, clean_dict, vcs):
        """Calculate size of each entry for display, prompt user if needed,
        delete files if approved and return the total size of files that
        have been deleted.

        @param clean_dict: dictionary of {'display name':[list of files]}

        @rtype: int
        @return: total size that was cleaned
        """
        clean_size = 0
        # clean all entries one by one; sorting helps reading
        for key in sorted(clean_dict):
            clean_size += self._clean_files(clean_dict[key], key)
        # return total size of deleted or to delete files
        clean_size += self._clean_vcs_src(vcs)
        return clean_size

    def clean_pkgs(self, clean_dict, pkgdir):
        """Calculate size of each entry for display, prompt user if needed,
        delete files if approved and return the total size of files that
        have been deleted.

        @param clean_dict:  dictionary of  {'display name':[list of files]}
        @param metadata: package index of type portage.getbinpkg.PackageIndex()
        @param pkgdir: path to the package directory to be cleaned

        @rtype: int
        @return: total size that was cleaned
        """
        clean_size = 0
        # clean all entries one by one; sorting helps reading
        for location, loc_clean_dict in clean_dict.items():
            print(" * Location", pp.path(location))
            for key in sorted(loc_clean_dict):
                clean_size += self._clean_binary_package(loc_clean_dict[key], key)

        #  run 'emaint --fix' here
        if clean_size:
            file = os.path.join(pkgdir, "Packages")
            size1 = os.stat(file).st_size
            TaskHandler(show_progress_bar=self.quiet).run_tasks(
                [binhost.BinhostHandler], "fix"
            )
            size = size1 - os.stat(file).st_size
            self.controller(size, "Packages Index", file, "Index")
            clean_size += size
        # return total size of deleted or to delete files
        return clean_size

    def pretend_clean(self, clean_dict, vcs={}, file_type="file"):
        """Shortcut function that calculates total space savings
        for the files in clean_dict.

        @param clean_dict: dictionary of {'display name':[list of files]}
        @rtype: integer
        @return: total size that would be cleaned
        """
        clean_size = 0
        # tally all entries one by one; sorting helps reading
        if vcs:
            clean_size += self._clean_vcs_src(vcs, pretend=True)
        if file_type == "file":
            for key in sorted(clean_dict):
                key_size = self._get_size(clean_dict[key])
                self.controller(key_size, key, clean_dict[key], file_type)
                clean_size += key_size
        else:
            # binary package
            for location, loc_clean_dict in clean_dict.items():
                print(" * Location", pp.path(location))
                for key in sorted(loc_clean_dict):
                    binpkg, debugpack = loc_clean_dict[key]
                    key_size = self._get_size(
                        [binpkg, debugpack] if debugpack else [binpkg]
                    )
                    self.controller(key_size, key, loc_clean_dict[key], file_type)
                    clean_size += key_size

        return clean_size

    def _get_size(self, key):
        """Determine the total size for an entry (may be several files)."""
        key_size = 0
        for file_ in key:
            # print file_
            # get total size for an entry (may be several files, and
            # links don't count
            # ...get its statinfo
            try:
                statinfo = os.stat(file_)
                if statinfo.st_nlink == 1:
                    key_size += statinfo.st_size
            except OSError as er:
                print(pp.error("Could not get stat info for:" + file_), file=sys.stderr)
                print(pp.error("Error: %s" % str(er)), file=sys.stderr)
        return key_size

    def _get_size_valid_symlink(self, file_):
        """
        Get a file size, attempting to remove broken symlinks when
        possible (attempting to remove any broken symlinks results in
        this function returning 0).
        """
        try:
            statinfo = os.stat(file_)
            if statinfo.st_nlink == 1:
                # Hack to disambiguate a file of empty size from the other
                # case where this function returns 0 at the end for a dead
                # symlink.
                return statinfo.st_size or 1
        except OSError as er:
            if os.path.exists(os.readlink(file_)):
                print(pp.error("Could not get stat info for:" + file_), file=sys.stderr)
                print(pp.error("Error: %s" % str(er)), file=sys.stderr)
            else:
                try:
                    os.remove(file_)
                    print(
                        pp.error("Removed broken symbolic link " + file_),
                        file=sys.stderr,
                    )
                except OSError as er:
                    print(
                        pp.error("Error deleting broken symbolic link " + file_),
                        file=sys.stderr,
                    )
                    print(pp.error("Error: %s" % str(er)), file=sys.stderr)
        return 0

    def _clean_binary_package(self, files: tuple[str, str | None], key: str):
        # collect files
        rm_files = []
        rm_size = 0

        if sz := self._get_size_valid_symlink(files[0]):
            rm_files.append(files[0])
            rm_size += sz

        if files[1] and (sz := self._get_size_valid_symlink(files[1])):
            rm_files.append(files[1])
            rm_size += sz

        # optionally prompt for removal
        clean_size = 0
        if self.controller(rm_size, key, files, "binary package"):
            for file_ in rm_files:
                # ... try to delete it.
                try:
                    statinfo = os.stat(file_)
                    os.unlink(file_)
                    # only count size if successfully deleted and not a link
                    if statinfo.st_nlink == 1:
                        clean_size += statinfo.st_size
                        try:
                            os.rmdir(os.path.dirname(file_))
                        except OSError:
                            pass
                except OSError as er:
                    print(pp.error("Could not delete " + file_), file=sys.stderr)
                    print(pp.error("Error: %s" % str(er)), file=sys.stderr)
        return clean_size

    def _clean_files(self, files, key):
        """File removal function."""
        clean_size = 0
        for file_ in files:
            # print file_, type(file_)
            # ...get its statinfo
            try:
                statinfo = os.stat(file_)
            except OSError as er:
                if not os.path.exists(os.readlink(file_)):
                    try:
                        os.remove(file_)
                        print(
                            pp.error("Removed broken symbolic link " + file_),
                            file=sys.stderr,
                        )
                        break
                    except OSError as er:
                        print(
                            pp.error("Error deleting broken symbolic link " + file_),
                            file=sys.stderr,
                        )
                        print(pp.error("Error: %s" % str(er)), file=sys.stderr)
                        break
                else:
                    print(
                        pp.error("Could not get stat info for:" + file_),
                        file=sys.stderr,
                    )
                    print(pp.error("Error: %s" % str(er)), file=sys.stderr)
            if self.controller(statinfo.st_size, key, file_, "file"):
                # ... try to delete it.
                try:
                    os.unlink(file_)
                    # only count size if successfully deleted and not a link
                    if statinfo.st_nlink == 1:
                        clean_size += statinfo.st_size
                        try:
                            os.rmdir(os.path.dirname(file_))
                        except OSError:
                            pass
                except OSError as er:
                    print(pp.error("Could not delete " + file_), file=sys.stderr)
                    print(pp.error("Error: %s" % str(er)), file=sys.stderr)
        return clean_size

    def _clean_vcs_src(self, deprecated_vcs, pretend=False):
        clean_size = 0
        for checkout in deprecated_vcs:
            csize = 0
            for path, dirs, files in os.walk(checkout):
                for f in files:
                    fp = os.path.join(path, f)
                    try:
                        statinfo = os.stat(fp)
                    except OSError as er:
                        print(
                            pp.error("Could not get stat info for:" + fp),
                            file=sys.stderr,
                        )
                        print(pp.error("Error: %s" % str(er)), file=sys.stderr)
                    clean_size += statinfo.st_size
                    csize += statinfo.st_size

            try:
                self.controller(csize, checkout, checkout, "checkout")
                if not pretend:
                    shutil.rmtree(checkout)
            except OSError as er:
                print(pp.error("Could not delete " + checkout), file=sys.stderr)
                print(pp.error("Error: %s" % str(er)), file=sys.stderr)
        return clean_size
