import glob
import json
import os
import re
import shutil
import subprocess
import tarfile
import time
import traceback
from datetime import datetime
from pathlib import Path
from pwd import getpwnam
from threading import Thread
from typing import List, Set, Type, Tuple, Dict, Iterable, Optional, Collection, Generator, Any

from dateutil.parser import parse as parse_date

from bauh import __app_name__
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \
    TransactionResult, SoftwareAction, SettingsView, SettingsController
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher, TaskManager
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageStatus, \
    CustomSoftwareAction, PackageSuggestion
from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \
    ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextInputType, \
    FileChooserComponent, TextComponent
from bauh.api.exception import NoInternetException
from bauh.api.paths import TEMP_DIR
from bauh.commons import system
from bauh.commons.boot import CreateConfigFile
from bauh.commons.category import CategoriesDownloader
from bauh.commons.html import bold
from bauh.commons.suggestions import sort_by_priority
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, SimpleProcess
from bauh.commons.util import datetime_as_milis
from bauh.commons.view_utils import new_select
from bauh.gems.arch import aur, pacman, message, confirmation, disk, git, \
    gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, \
    get_icon_path, database, mirrors, sorting, cpu_manager, UPDATES_IGNORED_FILE, \
    ARCH_CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS, rebuild_detector, makepkg, sshell, get_repo_icon_path
from bauh.gems.arch.aur import AURClient
from bauh.gems.arch.config import get_build_dir, ArchConfigManager
from bauh.gems.arch.confirmation import confirm_missing_deps
from bauh.gems.arch.dependencies import DependenciesAnalyser
from bauh.gems.arch.download import MultithreadedDownloadService, ArchDownloadException
from bauh.gems.arch.exceptions import PackageNotFoundException, PackageInHoldException
from bauh.gems.arch.mapper import AURDataMapper
from bauh.gems.arch.model import ArchPackage
from bauh.gems.arch.output import TransactionStatusHandler
from bauh.gems.arch.pacman import RE_DEP_OPERATORS
from bauh.gems.arch.proc_util import write_as_user
from bauh.gems.arch.suggestions import RepositorySuggestionsDownloader
from bauh.gems.arch.updates import UpdatesSummarizer
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, RefreshMirrors, \
    SyncDatabases

URL_GIT = 'https://aur.archlinux.org/{}.git'
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
URL_ARCH_NEWS_FEED = 'https://archlinux.org/feeds/news/'

RE_SPLIT_VERSION = re.compile(r'([=><]+)')

SOURCE_FIELDS = ('source', 'source_x86_64')
RE_PRE_DOWNLOAD_WL_PROTOCOLS = re.compile(r'^(.+::)?(https?|ftp)://.+')
RE_PRE_DOWNLOAD_BL_EXT = re.compile(r'.+\.(git|gpg)$')
RE_PKGBUILD_PKGNAME = re.compile(r'pkgname\s*=.+')
RE_CONFLICT_DETECTED = re.compile(r'\n::\s*(.+)\s+are in conflict\s*.')
RE_DEPENDENCY_BREAKAGE = re.compile(r'\n?::\s+installing\s+(.+\s\(.+\))\sbreaks\sdependency\s\'(.+)\'\srequired\sby\s(.+)\s*', flags=re.IGNORECASE)
RE_PKG_ENDS_WITH_BIN = re.compile(r'.+[\-_]bin$')
RE_ARCH_NEWS_ITEM = re.compile(r'<item>(.*?)</item>', flags=re.DOTALL)
RE_ARCH_NEWS_TITLE = re.compile(r'<title><!\[CDATA\[(.*?)]]></title>|<title>(.*?)</title>', flags=re.DOTALL)
RE_ARCH_NEWS_LINK = re.compile(r'<link>(.*?)</link>', flags=re.DOTALL)


class TransactionContext:

    def __init__(self, aur_supported: bool, name: str = None, base: str = None, maintainer: str = None, watcher: ProcessWatcher = None,
                 handler: ProcessHandler = None, dependency: bool = None, skip_opt_deps: bool = False, root_password: Optional[str] = None,
                 build_dir: str = None, project_dir: str = None, change_progress: bool = False, arch_config: dict = None,
                 install_files: Set[str] = None, repository: str = None, pkg: ArchPackage = None,
                 remote_repo_map: Dict[str, str] = None, provided_map: Dict[str, Set[str]] = None,
                 remote_provided_map: Dict[str, Set[str]] = None, aur_idx: Set[str] = None,
                 missing_deps: List[Tuple[str, str]] = None, installed: Set[str] = None, removed: Dict[str, SoftwarePackage] = None,
                 disk_loader: DiskCacheLoader = None, disk_cache_updater: Thread = None,
                 new_pkg: bool = False, custom_pkgbuild_path: str = None,
                 pkgs_to_build: Set[str] = None, last_modified: Optional[int] = None,
                 commit: Optional[str] = None, update_aur_index: bool = False):
        self.aur_supported = aur_supported
        self.name = name
        self.base = base
        self.maintainer = maintainer
        self.watcher = watcher
        self.handler = handler
        self.dependency = dependency
        self.skip_opt_deps = skip_opt_deps
        self.build_dir = build_dir
        self.project_dir = project_dir
        self.root_password = root_password
        self.change_progress = change_progress
        self.repository = repository
        self.config = arch_config
        self.install_files = install_files
        self.pkg = pkg
        self.provided_map = provided_map
        self.remote_repo_map = remote_repo_map
        self.remote_provided_map = remote_provided_map
        self.aur_idx = aur_idx
        self.missing_deps = missing_deps
        self.installed = installed
        self.removed = removed
        self.disk_loader = disk_loader
        self.disk_cache_updater = disk_cache_updater
        self.pkgbuild_edited = False
        self.new_pkg = new_pkg
        self.custom_pkgbuild_path = custom_pkgbuild_path
        self.pkgs_to_build = pkgs_to_build
        self.previous_change_progress = change_progress
        self.last_modified = last_modified
        self.commit = commit
        self.update_aur_index = update_aur_index

    @classmethod
    def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: Optional[str], handler: ProcessHandler, aur_supported: Optional[bool] = None) -> "TransactionContext":
        return cls(name=pkg.name, base=pkg.get_base_name(), maintainer=pkg.maintainer, repository=pkg.repository,
                   arch_config=arch_config, watcher=handler.watcher, handler=handler, skip_opt_deps=True,
                   change_progress=True, root_password=root_password, dependency=False,
                   installed=set(), removed={}, new_pkg=not pkg.installed, last_modified=pkg.last_modified,
                   aur_supported=aur_supported if aur_supported is not None else (pkg.repository == 'aur' or aur.is_supported(arch_config)))

    def get_base_name(self):
        return self.base if self.base else self.name

    def get_project_dir(self):
        return self.project_dir or '.'

    def clone_base(self):
        return TransactionContext(watcher=self.watcher, handler=self.handler, root_password=self.root_password,
                                  arch_config=self.config, installed=set(), removed={}, aur_supported=self.aur_supported)

    def gen_dep_context(self, name: str, repository: str):
        dep_context = self.clone_base()
        dep_context.name = name
        dep_context.repository = repository
        dep_context.dependency = True
        dep_context.change_progress = False
        dep_context.installed = set()
        dep_context.removed = {}
        return dep_context

    def has_install_files(self) -> bool:
        return bool(self.install_files)

    def get_packages_paths(self) -> Set[str]:
        return self.install_files if self.install_files else {self.name}

    def get_package_names(self) -> Set[str]:
        return self.pkgs_to_build if (self.pkgs_to_build and self.install_files) else {self.name}

    def get_version(self) -> str:
        return self.pkg.version if self.pkg else None

    def get_aur_idx(self, aur_client: AURClient) -> Set[str]:
        if self.aur_idx is None:
            if self.aur_supported:
                self.aur_idx = aur_client.read_index()
            else:
                self.aur_idx = set()

        return self.aur_idx

    def get_provided_map(self) -> Dict[str, Set[str]]:
        if self.provided_map is None:
            self.provided_map = pacman.map_provided()

        return self.provided_map

    def get_remote_provided_map(self) -> Dict[str, Set[str]]:
        if self.remote_provided_map is None:
            self.remote_provided_map = pacman.map_provided(remote=True)

        return self.remote_provided_map

    def get_remote_repo_map(self) -> Dict[str, str]:
        if self.remote_repo_map is None:
            self.remote_repo_map = pacman.map_repositories()

        return self.remote_repo_map

    def disable_progress_if_changing(self):
        if self.change_progress:
            self.previous_change_progress = True
            self.change_progress = False

    def restabilish_progress(self):
        if self.previous_change_progress is not None:
            self.change_progress = self.previous_change_progress

        self.previous_change_progress = self.change_progress


class ArchManager(SoftwareManager, SettingsController):

    def __init__(self, context: ApplicationContext, disk_cache_updater: Optional[ArchDiskCacheUpdater] = None):
        super(ArchManager, self).__init__(context=context)
        self.aur_cache = context.cache_factory.new()
        # context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO
        self.configman = ArchConfigManager()
        self.aur_mapper = AURDataMapper(http_client=context.http_client, i18n=context.i18n, logger=context.logger)
        self.i18n = context.i18n
        self.aur_client = AURClient(http_client=context.http_client, logger=context.logger, x86_64=context.is_system_x86_64())
        self.dcache_updater = None
        self.logger = context.logger
        self.enabled = True
        self.arch_distro = context.distro == 'arch'
        self.categories = {}
        self.deps_analyser = DependenciesAnalyser(self.aur_client, self.i18n, self.logger)
        self.http_client = context.http_client
        self._custom_actions: Optional[Dict[str, CustomSoftwareAction]] = None
        self.index_aur = None
        self.re_file_conflict = re.compile(r'[\w\d\-_.]+:')
        self.disk_cache_updater = disk_cache_updater
        self.pkgbuilder_user: Optional[str] = f'{__app_name__}-aur' if context.root_user else None
        self._suggestions_downloader: Optional[RepositorySuggestionsDownloader] = None

    def refresh_mirrors(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            return False

        available_countries = pacman.list_mirror_countries()
        current_countries = pacman.get_current_mirror_countries()

        if not available_countries:
            self.logger.warning("No country available")
            countries = current_countries
        else:
            country_opts = [InputOption(label=self.i18n['arch.custom_action.refresh_mirrors.location.all'], value='all',
                                        tooltip=self.i18n['arch.custom_action.refresh_mirrors.location.all.tip'])]
            mapped_opts = [InputOption(label=' '.join((w.capitalize() for w in self.i18n[' '.join(c.split('_'))].split(' '))),
                                       value=c) for c in available_countries]
            mapped_opts.sort(key=lambda o: o.label)

            if len(current_countries) == 1 and current_countries[0] == 'all':
                default_opts = {country_opts[0]}
            else:
                default_opts = {o for o in mapped_opts if o.value in current_countries}
                country_opts.extend(default_opts)

            country_opts.extend((o for o in mapped_opts if o not in default_opts))

            select = MultipleSelectComponent(options=country_opts,
                                             default_options=default_opts,
                                             max_per_line=3,
                                             label=self.i18n['arch.custom_action.refresh_mirrors.select_label'])

            if watcher.request_confirmation(title=self.i18n['arch.custom_action.refresh_mirrors'],
                                            body=None,
                                            components=[select],
                                            confirmation_label=self.i18n['continue'].capitalize(),
                                            deny_label=self.i18n["cancel"].capitalize()):
                countries = select.get_selected_values()

                if 'all' in countries or len(countries) == len(available_countries):
                    countries = ['all']
            else:
                watcher.print("Aborted by the user")
                return False

        watcher.change_substatus(self.i18n['arch.custom_action.refresh_mirrors.status.updating'])

        if current_countries == countries:
            success, output = handler.handle_simple(pacman.refresh_mirrors(root_password))
        else:
            success, output = handler.handle_simple(pacman.update_mirrors(root_password, countries))

        if not success:
            watcher.show_message(title=self.i18n["action.failed"].capitalize(),
                                 body=self.i18n['arch.custom_action.refresh_mirrors.failed'],
                                 type_=MessageType.ERROR)
            return False

        sort_limit = self.configman.get_config()['mirrors_sort_limit']

        if sort_limit is not None and isinstance(sort_limit, int) and sort_limit >= 0:
            watcher.change_substatus(self.i18n['arch.custom_action.refresh_mirrors.status.sorting'])
            handler.handle_simple(pacman.sort_fastest_mirrors(root_password, sort_limit))

        mirrors.register_sync(self.logger)

        watcher.change_substatus(self.i18n['arch.sync_databases.substatus'])
        return self.sync_databases(root_password=root_password, watcher=watcher)

    def sync_databases(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            return False

        success, output = handler.handle_simple(pacman.sync_databases(root_password, force=True))

        if not success:
            watcher.show_message(title=self.i18n["action.failed"].capitalize(),
                                 body=self.i18n['arch.custom_action.refresh_mirrors.failed'],
                                 type_=MessageType.ERROR)
            return False

        database.register_sync(self.logger)
        return True

    def _fill_repos_search_results(self, query: str, output: dict):
        ti = time.time()
        output['repositories'] = pacman.search(query)
        tf = time.time()
        self.logger.info("Repositories search took {0:.2f} seconds".format(tf - ti))

    def _fill_aur_search_results(self, query: str, output: dict):
        ti = time.time()
        api_res = self.aur_client.search(query)

        pkgs_found = None
        if api_res and api_res.get('results'):
            pkgs_found = api_res['results']
        else:
            tii = time.time()
            if self.index_aur:
                self.index_aur.join()

            aur_index = self.aur_client.read_local_index()
            if aur_index:
                self.logger.info("Querying through the local AUR index")
                to_query = set()
                for norm_name, real_name in aur_index.items():
                    if query in norm_name:
                        to_query.add(real_name)

                    if len(to_query) == 25:
                        break

                pkgs_found = self.aur_client.get_info(to_query)

            tif = time.time()
            self.logger.info("Query through local AUR index took {0:.2f} seconds".format(tif - tii))

        if pkgs_found:
            for pkg in pkgs_found:
                output['aur'][pkg['Name']] = pkg

        tf = time.time()
        self.logger.info("AUR search took {0:.2f} seconds".format(tf - ti))

    def __fill_search_installed_and_matched(self, query: str, res: dict):
        matches = set()
        installed = pacman.list_installed_names()
        res['installed'] = installed
        res['installed_matches'] = matches

        if installed and ' ' not in query:  # already filling some matches only based on the query
            matches.update((name for name in installed if query in name))

    def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult:
        if is_url:
            return SearchResult.empty()

        arch_config = self.configman.get_config()
        repos_supported, aur_supported = arch_config['repositories'], aur.is_supported(arch_config)

        if not any([repos_supported, aur_supported]):
            return SearchResult.empty()

        res = SearchResult.empty()

        search_output, search_threads = {'aur': {}, 'repositories': {}}, []
        t = Thread(target=self.__fill_search_installed_and_matched, args=(words, search_output), daemon=True)
        t.start()
        search_threads.append(t)

        if aur_supported:
            taur = Thread(target=self._fill_aur_search_results, args=(words, search_output), daemon=True)
            taur.start()
            search_threads.append(taur)

        if repos_supported:
            trepo = Thread(target=self._fill_repos_search_results, args=(words, search_output), daemon=True)
            trepo.start()
            search_threads.append(trepo)

        for t in search_threads:
            t.join()

        for name in {*search_output['repositories'].keys(), *search_output['aur'].keys()}:
            if name in search_output['installed']:
                search_output['installed_matches'].add(name)

        if search_output['installed_matches']:
            installed = self.read_installed(disk_loader=disk_loader, names=search_output['installed_matches']).installed

            for pkg in installed:
                if pkg.repository != 'aur':
                    if repos_supported:
                        res.installed.append(pkg)
                        if pkg.name in search_output['repositories']:
                            del search_output['repositories'][pkg.name]
                elif aur_supported:
                    res.installed.append(pkg)
                    if pkg.name in search_output['aur']:
                        del search_output['aur'][pkg.name]

        if search_output['repositories']:
            for pkgname, data in search_output['repositories'].items():
                res.new.append(ArchPackage(name=pkgname, i18n=self.i18n, **data))

        if search_output['aur']:
            for pkgname, apidata in search_output['aur'].items():
                res.new.append(self.aur_mapper.map_api_data(apidata, None, self.categories))

        res.update_total()
        return res

    def _fill_aur_pkgs_offline(self, aur_pkgs: dict, arch_config: dict, output: List[ArchPackage], disk_loader: Optional[DiskCacheLoader]):
        self.logger.info("Reading cached data from installed AUR packages")

        editable_pkgbuilds = self._read_editable_pkgbuilds() if arch_config['edit_aur_pkgbuild'] is not False else None
        for name, data in aur_pkgs.items():
            pkg = ArchPackage(name=name, version=data.get('version'),
                              latest_version=data.get('version'), description=data.get('description'),
                              installed=True, repository='aur', i18n=self.i18n)

            pkg.categories = self.categories.get(pkg.name)
            pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None

            if disk_loader:
                disk_loader.fill(pkg)

            pkg.status = PackageStatus.READY
            output.append(pkg)

    def _fill_aur_pkgs(self, aur_pkgs: dict, output: List[ArchPackage], disk_loader: Optional[DiskCacheLoader], internet_available: bool,
                       arch_config: dict, rebuild_check: Optional[Thread], rebuild_ignored: Optional[Thread], rebuild_output: Optional[Dict[str, Set[str]]]):

        if not internet_available:
            self._fill_aur_pkgs_offline(aur_pkgs=aur_pkgs, arch_config=arch_config,
                                        output=output, disk_loader=disk_loader)
            return

        pkgsinfo = self.aur_client.get_info(aur_pkgs.keys())

        if pkgsinfo is None:
            self._fill_aur_pkgs_offline(aur_pkgs=aur_pkgs, arch_config=arch_config, output=output, disk_loader=disk_loader)
        elif not pkgsinfo:
            self.logger.warning("No data found for the supposed installed AUR packages returned from AUR API's info endpoint")
        else:
            editable_pkgbuilds = self._read_editable_pkgbuilds() if arch_config['edit_aur_pkgbuild'] is not False else None

            ignore_rebuild_check = None
            if rebuild_ignored and rebuild_output is not None:
                rebuild_ignored.join()
                ignore_rebuild_check = rebuild_output['ignored']

            to_rebuild = None
            if rebuild_check and rebuild_output is not None:
                self.logger.info("Waiting for rebuild-detector")
                rebuild_check.join()
                to_rebuild = rebuild_output['to_rebuild']

            for pkgdata in pkgsinfo:
                pkg = self.aur_mapper.map_api_data(pkgdata, aur_pkgs, self.categories)
                pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None

                if pkg.installed:
                    if disk_loader:
                        disk_loader.fill(pkg, sync=True)

                    pkg.update = self._check_aur_package_update(pkg=pkg,
                                                                installed_data=aur_pkgs.get(pkg.name, {}),
                                                                api_data=pkgdata)
                    pkg.aur_update = pkg.update  # used in 'set_rebuild_check'

                    if ignore_rebuild_check is not None:
                        pkg.allow_rebuild = pkg.name not in ignore_rebuild_check

                    if to_rebuild and not pkg.update and pkg.name in to_rebuild:
                        pkg.require_rebuild = True

                    pkg.update_state()

                pkg.status = PackageStatus.READY
                output.append(pkg)

    def _check_aur_package_update(self, pkg: ArchPackage, installed_data: dict, api_data: dict) -> bool:
        if pkg.last_modified is None:  # if last_modified is not available, then the install_date will be used instead
            install_date = installed_data.get('install_date')

            if install_date:
                try:
                    pkg.install_date = datetime_as_milis(parse_date(install_date))
                except ValueError:
                    self.logger.error(f"Could not parse 'install_date' ({install_date}) from AUR package '{pkg.name}'")
            else:
                self.logger.error(f"AUR package '{pkg.name}' install_date was not retrieved")

        return self.aur_mapper.check_update(pkg=pkg, last_modified=api_data['LastModified'])

    def _fill_repo_updates(self, updates: dict):
        updates.update(pacman.list_repository_updates())

    def _fill_repo_pkgs(self, repo_pkgs: dict, pkgs: list, aur_index: Optional[Set[str]], disk_loader: DiskCacheLoader):
        updates = {}

        thread_updates = Thread(target=self._fill_repo_updates, args=(updates,), daemon=True)
        thread_updates.start()

        repo_map = pacman.map_repositories(repo_pkgs)

        thread_updates.join()
        self.logger.info("Repository updates found" if updates else "No repository updates found")

        for name, data in repo_pkgs.items():
            pkgversion = data.get('version')
            pkgrepo = repo_map.get(name)
            pkg = ArchPackage(name=name,
                              version=pkgversion,
                              latest_version=pkgversion,
                              description=data.get('description'),
                              maintainer=pkgrepo,
                              i18n=self.i18n,
                              installed=True,
                              repository=pkgrepo,
                              categories=self.categories.get(name, []))

            if updates:
                update_version = updates.get(pkg.name)

                if update_version:
                    pkg.latest_version = update_version
                    pkg.update = True

            if disk_loader:
                disk_loader.fill(pkg, sync=True)

            if pkg.repository == 'aur':
                pkg.repository = None

                if aur_index and pkg.name not in aur_index:
                    removed_cat = self.i18n['arch.category.remove_from_aur']

                    if removed_cat not in pkg.categories:
                        pkg.categories.append(removed_cat)

            pkgs.append(pkg)

    def _wait_for_disk_cache(self):
        if self.disk_cache_updater and self.disk_cache_updater.is_alive():
            self.logger.info("Waiting for disk cache to be ready")
            self.disk_cache_updater.join()
            self.logger.info("Disk cache ready")

    def __fill_packages_to_rebuild(self, output: Dict[str, Set[str]], ignore_binaries: bool):
        if rebuild_detector.is_installed():
            self.logger.info("rebuild-detector: checking")
            to_rebuild = rebuild_detector.list_required_rebuild()

            if to_rebuild and ignore_binaries:
                to_rebuild = {p for p in to_rebuild if not RE_PKG_ENDS_WITH_BIN.match(p)}

            output['to_rebuild'].update(to_rebuild)
            self.logger.info("rebuild-detector: {} packages require rebuild".format(len(to_rebuild)))

    def __fill_ignored_by_rebuild_detector(self, output: Dict[str, Set[str]]):
        output['ignored'].update(rebuild_detector.list_ignored())

    def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None, names: Iterable[str] = None, wait_disk_cache: bool = True) -> SearchResult:
        self.aur_client.clean_caches()
        arch_config = self.configman.get_config()

        aur_supported, repos_supported = aur.is_supported(arch_config), arch_config['repositories']

        if not aur_supported and not repos_supported:
            return SearchResult.empty()

        rebuild_output, rebuild_check, rebuild_ignored = None, None, None
        if aur_supported and arch_config['aur_rebuild_detector']:
            rebuild_output = {'to_rebuild': set(), 'ignored': set()}
            rebuild_check = Thread(target=self.__fill_packages_to_rebuild,
                                   args=(rebuild_output, arch_config['aur_rebuild_detector_no_bin']),
                                   daemon=True)
            rebuild_check.start()

            rebuild_ignored = Thread(target=self.__fill_ignored_by_rebuild_detector, args=(rebuild_output, ), daemon=True)
            rebuild_ignored.start()

        installed = pacman.map_packages(names=names)

        aur_pkgs, repo_pkgs, aur_index = None, None, None

        if repos_supported:
            repo_pkgs = installed['signed']

        if installed['not_signed']:
            if aur_supported:
                if self.index_aur:
                    self.index_aur.join()

                aur_index = self.aur_client.read_index()

                for pkg in {*installed['not_signed']}:
                    if pkg not in aur_index:
                        if repos_supported:
                            repo_pkgs[pkg] = installed['not_signed'][pkg]

                        if aur_supported and installed['not_signed']:
                            del installed['not_signed'][pkg]

                aur_pkgs = installed['not_signed']
            elif repos_supported:
                repo_pkgs.update(installed['not_signed'])

        pkgs = []
        if repo_pkgs or aur_pkgs:
            if wait_disk_cache:
                self._wait_for_disk_cache()

            map_threads = []

            if aur_pkgs:
                t = Thread(target=self._fill_aur_pkgs, args=(aur_pkgs, pkgs, disk_loader, internet_available, arch_config, rebuild_check, rebuild_ignored, rebuild_output), daemon=True)
                t.start()
                map_threads.append(t)

            if repo_pkgs:
                t = Thread(target=self._fill_repo_pkgs, args=(repo_pkgs, pkgs, aur_index, disk_loader), daemon=True)
                t.start()
                map_threads.append(t)

            for t in map_threads:
                t.join()

        if pkgs:
            ignored = self._fill_ignored_updates(set())

            if ignored:
                for p in pkgs:
                    if p.name in ignored:
                        p.update_ignored = True

        return SearchResult(pkgs, None, len(pkgs))

    def _downgrade_aur_pkg(self, context: TransactionContext) -> bool:
        if not self.add_package_builder_user(context.handler):
            return False

        if context.commit:
            self.logger.info("Package '{}' current commit {}".format(context.name, context.commit))
        else:
            self.logger.warning("Package '{}' has no commit associated with it. Downgrading will only compare versions.".format(context.name))

        context.build_dir = f'{get_build_dir(context.config, self.pkgbuilder_user)}/build_{int(time.time())}'

        try:
            if not os.path.exists(context.build_dir):
                build_dir, build_dir_error = sshell.mkdir(dir_path=context.build_dir, custom_user=self.pkgbuilder_user)

                if not build_dir:
                    context.watcher.print(build_dir_error)
                else:
                    context.handler.watcher.change_progress(10)
                    base_name = context.get_base_name()
                    context.watcher.change_substatus(self.i18n['arch.clone'].format(bold(context.name)))
                    clone_dir = f'{context.build_dir}/{base_name}'
                    cloned, _ = context.handler.handle_simple(git.clone(url=URL_GIT.format(base_name),
                                                                        target_dir=clone_dir,
                                                                        custom_user=self.pkgbuilder_user))
                    context.watcher.change_progress(30)

                    if cloned:
                        context.watcher.change_substatus(self.i18n['arch.downgrade.reading_commits'])
                        context.project_dir = clone_dir
                        srcinfo_path = f'{clone_dir}/.SRCINFO'

                        logs = git.list_commits(clone_dir)
                        context.watcher.change_progress(40)

                        if not logs or len(logs) == 1:
                            context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
                                                         body=self.i18n['arch.downgrade.impossible'].format(context.name),
                                                         type_=MessageType.ERROR)
                            return False

                        if context.commit:
                            target_commit, target_commit_timestamp = None, None
                            for idx, log in enumerate(logs):
                                if context.commit == log[0] and idx + 1 < len(logs):
                                    target_commit = logs[idx + 1][0]
                                    target_commit_timestamp = logs[idx + 1][1]
                                    break

                            if not target_commit:
                                self.logger.warning("Could not find '{}' target commit to revert to".format(context.name))
                            else:
                                context.watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
                                checkout_proc = new_subprocess(['git', 'checkout', target_commit], cwd=clone_dir, custom_user=self.pkgbuilder_user)
                                if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)):
                                    context.watcher.print("Could not rollback to current version's commit")
                                    return False

                                context.watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
                                context.last_modified = target_commit_timestamp
                                context.commit = target_commit
                                return self._build(context)

                        # trying to downgrade by version comparison
                        commit_found, commit_date = None, None
                        srcfields = {'pkgver', 'pkgrel', 'epoch'}

                        for idx in range(1, len(logs)):
                            commit, date = logs[idx][0], logs[idx][1]
                            with open(srcinfo_path) as f:
                                pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name, fields=srcfields)

                            reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_dir, custom_user=self.pkgbuilder_user)
                            if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)):
                                context.handler.watcher.print('Could not downgrade anymore. Aborting...')
                                return False

                            epoch, version, release = pkgsrc.get('epoch'), pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')

                            if epoch:
                                current_version = '{}:{}-{}'.format(epoch, version, release)
                            else:
                                current_version = '{}-{}'.format(version, release)

                            if commit_found:
                                context.watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
                                checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_dir, custom_user=self.pkgbuilder_user)
                                if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)):
                                    context.watcher.print("Could not rollback to current version's commit")
                                    return False

                                reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_dir, custom_user=self.pkgbuilder_user)
                                if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)):
                                    context.watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found))
                                    return False

                                break
                            elif current_version == context.get_version():
                                commit_found, commit_date = commit, date

                        context.watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
                        context.last_modified = commit_date
                        context.commit = commit_found
                        return self._build(context)
        finally:
            if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']:
                context.handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', context.build_dir])))

        return False

    def _downgrade_repo_pkg(self, context: TransactionContext):
        context.watcher.change_substatus(self.i18n['arch.downgrade.searching_stored'])
        if not os.path.isdir('/var/cache/pacman/pkg'):
            context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
                                         body=self.i18n['arch.downgrade.repo_pkg.no_versions'],
                                         type_=MessageType.ERROR)
            return False

        available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(context.name))

        if not available_files:
            context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
                                         body=self.i18n['arch.downgrade.repo_pkg.no_versions'],
                                         type_=MessageType.ERROR)
            return False

        reg = re.compile(r'{}-([\w.\-]+)-(x86_64|any|i686).pkg'.format(context.name))

        versions, version_files = [], {}
        for file_path in available_files:
            found = reg.findall(os.path.basename(file_path))

            if found:
                ver = found[0][0]
                if ver not in versions and ver < context.get_version():
                    versions.append(ver)
                    version_files[ver] = file_path

        context.watcher.change_progress(40)
        if not versions:
            context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
                                         body=self.i18n['arch.downgrade.repo_pkg.no_versions'],
                                         type_=MessageType.ERROR)
            return False

        versions.sort(reverse=True)

        context.watcher.change_progress(50)

        context.install_files = version_files[versions[0]]  # TODO verify
        if not self._handle_missing_deps(context=context):
            return False

        context.watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
        context.watcher.change_progress(60)

        return self._install(context)

    def downgrade(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        if not self.check_action_allowed(pkg, watcher):
            return False

        self.aur_client.clean_caches()

        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            return False

        arch_config = self.configman.get_config()
        aur_supported = pkg.repository == 'aur' or aur.is_supported(arch_config)
        context = TransactionContext(name=pkg.name, base=pkg.get_base_name(), skip_opt_deps=True,
                                     change_progress=True, dependency=False, repository=pkg.repository, pkg=pkg,
                                     arch_config=arch_config, watcher=watcher, handler=handler, root_password=root_password,
                                     installed=set(), removed={},
                                     aur_supported=aur_supported,
                                     commit=pkg.commit)

        self._sync_databases(arch_config=context.config, aur_supported=aur_supported,
                             root_password=root_password, handler=handler)

        watcher.change_progress(5)

        if pkg.repository == 'aur':
            return self._downgrade_aur_pkg(context)
        else:
            return self._downgrade_repo_pkg(context)

    def clean_cache_for(self, pkg: ArchPackage):
        if os.path.exists(pkg.get_disk_cache_path()):
            shutil.rmtree(pkg.get_disk_cache_path())

    def _is_database_locked(self, handler: ProcessHandler, root_password: Optional[str]) -> bool:
        if os.path.exists('/var/lib/pacman/db.lck'):
            handler.watcher.print('pacman database is locked')
            msg = '<p>{}</p><p>{}</p><br/>'.format(self.i18n['arch.action.db_locked.body.l1'],
                                                   self.i18n['arch.action.db_locked.body.l2'])
            if handler.watcher.request_confirmation(title=self.i18n['arch.action.db_locked.title'].capitalize(),
                                                    body=msg,
                                                    confirmation_label=self.i18n['arch.action.db_locked.confirmation'].capitalize(),
                                                    deny_label=self.i18n['cancel'].capitalize()):

                try:
                    if not handler.handle_simple(SimpleProcess(['rm', '-rf', '/var/lib/pacman/db.lck'], root_password=root_password)):
                        handler.watcher.show_message(title=self.i18n['error'].capitalize(),
                                                     body=self.i18n['arch.action.db_locked.error'],
                                                     type_=MessageType.ERROR)
                        return True
                except Exception:
                    self.logger.error("An error occurred while removing the pacman database lock")
                    traceback.print_exc()
                    handler.watcher.show_message(title=self.i18n['error'].capitalize(),
                                                 body=self.i18n['arch.action.db_locked.error'],
                                                 type_=MessageType.ERROR)
                    return True
            else:
                handler.watcher.print('Action cancelled by the user. Aborting...')
                return True

        return False

    def _map_conflicting_file(self, output: str) -> List[MultipleSelectComponent]:
        error_idx = None
        lines = output.split('\n')
        for idx, l in enumerate(lines):
            if l and l.strip().lower().startswith('error: failed to commit transaction (conflicting files)'):
                error_idx = idx
                break

        files = []

        if error_idx and error_idx + 1 < len(lines):
            for idx in range(error_idx + 1, len(lines)):
                line = lines[idx].strip()

                if line and self.re_file_conflict.match(line):
                    files.append(InputOption(label=line, value=idx, read_only=True))

        return [MultipleSelectComponent(options=files, default_options={*files}, label='')]

    def _map_dependencies_breakage(self, output: str) -> List[ViewComponent]:
        errors = RE_DEPENDENCY_BREAKAGE.findall(output)

        if errors:
            opts = []

            for idx, err in enumerate(errors):
                opts.append(InputOption(label=self.i18n['arch.upgrade.error.dep_breakage.item'].format(*err), value=idx, read_only=True))

            return [MultipleSelectComponent(label='',
                                            options=opts,
                                            default_options={*opts})]
        else:
            return [TextComponent(output)]

    def list_related(self, pkgs: Collection[str], all_pkgs: Collection[str], data: Dict[str, dict], related: Set[str], provided_map: Dict[str, Set[str]]) -> Set[str]:
        related.update(pkgs)

        deps = set()

        for pkg in pkgs:
            pkg_deps = data[pkg]['d']

            if pkg_deps:
                deps.update({d for d in pkg_deps if d not in related})

        if deps:
            if not provided_map:
                for p in all_pkgs:
                    for provided in data[p].get('p', {p}):
                        sources = provided_map.get(provided, set())
                        provided_map[provided] = sources
                        sources.add(p)

            added_sources = set()
            for dep in deps:
                sources = provided_map.get(dep)

                if sources:
                    for source in sources:
                        if source not in related:
                            related.add(source)
                            added_sources.add(source)

            if added_sources:
                self.list_related(added_sources, all_pkgs, data, related, provided_map)

        return related

    def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str]], handler: ProcessHandler, root_password: Optional[str],
                           multithread_download: bool, pkgs_data: Dict[str, dict], overwrite_files: bool = False,
                           status_handler: TransactionStatusHandler = None, sizes: Dict[str, int] = None, download: bool = True,
                           check_syncfirst: bool = True, skip_dependency_checks: bool = False) -> bool:
        self.logger.info("Total packages to upgrade: {}".format(len(to_upgrade)))

        to_sync_first = None
        if check_syncfirst:
            to_sync_first = [p for p in pacman.get_packages_to_sync_first() if p.endswith('-keyring') and p in to_upgrade]
            self.logger.info("Packages detected to upgrade firstly: {}".format(len(to_sync_first)))

            if to_sync_first:
                self.logger.info("Upgrading keyrings marked as 'SyncFirst'")
                if not self._upgrade_repo_pkgs(to_upgrade=to_sync_first,
                                               to_remove=None,
                                               handler=handler,
                                               root_password=root_password,
                                               sizes=sizes,
                                               download=True,
                                               multithread_download=multithread_download,
                                               pkgs_data=pkgs_data,
                                               check_syncfirst=False):
                    return False

        to_upgrade_remaining = [p for p in to_upgrade if p not in to_sync_first] if to_sync_first else to_upgrade
        self.logger.info("Packages remaining to upgrade: {}".format(len(to_upgrade_remaining)))

        # pre-downloading all packages before removing any
        if download and to_upgrade_remaining:
            try:
                downloaded = self._download_packages(pkgnames=to_upgrade_remaining,
                                                     handler=handler,
                                                     root_password=root_password,
                                                     sizes=sizes,
                                                     multithreaded=multithread_download)

                if downloaded < len(to_upgrade_remaining):
                    self._show_upgrade_download_failed(handler.watcher)
                    return False

            except ArchDownloadException:
                self._show_upgrade_download_failed(handler.watcher)
                return False

        if to_remove and not self._remove_transaction_packages(to_remove, handler, root_password):
            return False

        if not to_upgrade_remaining:
            return True

        try:
            if status_handler:
                output_handler = status_handler
            else:
                output_handler = TransactionStatusHandler(handler.watcher, self.i18n, {*to_upgrade_remaining}, self.logger, downloading=len(to_upgrade_remaining))
                output_handler.start()

            self.logger.info("Upgrading {} repository packages: {}".format(len(to_upgrade_remaining), ', '.join(to_upgrade_remaining)))
            success, upgrade_output = handler.handle_simple(pacman.upgrade_several(pkgnames=to_upgrade_remaining,
                                                                                   root_password=root_password,
                                                                                   overwrite_conflicting_files=overwrite_files,
                                                                                   skip_dependency_checks=skip_dependency_checks),
                                                            output_handler=output_handler.handle)
            handler.watcher.change_substatus('')

            if success:
                output_handler.stop_working()
                output_handler.join()
                handler.watcher.print("Repository packages successfully upgraded")
                handler.watcher.change_substatus(self.i18n['arch.upgrade.caching_pkgs_data'])
                repo_map = pacman.map_repositories(to_upgrade_remaining)

                pkg_map = {}
                for name in to_upgrade_remaining:
                    repo = repo_map.get(name)
                    pkg_map[name] = ArchPackage(name=name,
                                                repository=repo,
                                                maintainer=repo,
                                                categories=self.categories.get(name))

                disk.write_several(pkgs=pkg_map, overwrite=True, maintainer=None)
                return True
            elif 'conflicting files' in upgrade_output:
                if not handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(),
                                                            body=self.i18n['arch.upgrade.error.conflicting_files'] + ':',
                                                            deny_label=self.i18n['arch.upgrade.conflicting_files.proceed'],
                                                            confirmation_label=self.i18n['arch.upgrade.conflicting_files.stop'],
                                                            components=self._map_conflicting_file(upgrade_output)):

                    return self._upgrade_repo_pkgs(to_upgrade=to_upgrade_remaining,
                                                   handler=handler,
                                                   root_password=root_password,
                                                   overwrite_files=True,
                                                   status_handler=output_handler,
                                                   multithread_download=multithread_download,
                                                   download=False,
                                                   check_syncfirst=False,
                                                   pkgs_data=pkgs_data,
                                                   to_remove=None,
                                                   sizes=sizes,
                                                   skip_dependency_checks=skip_dependency_checks)
                else:
                    output_handler.stop_working()
                    output_handler.join()
                    handler.watcher.print("Aborted by the user")
                    return False
            elif ' breaks dependency ' in upgrade_output:
                if not handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(),
                                                            body=self.i18n['arch.upgrade.error.dep_breakage'] + ':',
                                                            deny_label=self.i18n['arch.upgrade.error.dep_breakage.proceed'],
                                                            confirmation_label=self.i18n['arch.upgrade.error.dep_breakage.stop'],
                                                            components=self._map_dependencies_breakage(upgrade_output)):
                    return self._upgrade_repo_pkgs(to_upgrade=to_upgrade_remaining,
                                                   handler=handler,
                                                   root_password=root_password,
                                                   overwrite_files=overwrite_files,
                                                   status_handler=output_handler,
                                                   multithread_download=multithread_download,
                                                   download=False,
                                                   check_syncfirst=False,
                                                   pkgs_data=pkgs_data,
                                                   to_remove=None,
                                                   sizes=sizes,
                                                   skip_dependency_checks=True)
                else:
                    output_handler.stop_working()
                    output_handler.join()
                    handler.watcher.print("Aborted by the user")
                    return False
            else:
                output_handler.stop_working()
                output_handler.join()
                self.logger.error("'pacman' returned an unexpected response or error phrase after upgrading the repository packages")
                return False
        except Exception:
            handler.watcher.change_substatus('')
            handler.watcher.print("An error occurred while upgrading repository packages")
            self.logger.error("An error occurred while upgrading repository packages")
            traceback.print_exc()
            return False

    def _remove_transaction_packages(self, to_remove: Set[str], handler: ProcessHandler, root_password: Optional[str]) -> bool:
        output_handler = TransactionStatusHandler(watcher=handler.watcher,
                                                  i18n=self.i18n,
                                                  names=set(),
                                                  logger=self.logger,
                                                  pkgs_to_remove=len(to_remove))
        output_handler.start()
        try:
            success, _ = handler.handle_simple(pacman.remove_several(pkgnames=to_remove,
                                                                     root_password=root_password,
                                                                     skip_checks=True),
                                               output_handler=output_handler.handle)

            if not success:
                self.logger.error("Could not remove packages: {}".format(', '.join(to_remove)))
                output_handler.stop_working()
                output_handler.join()
                return False

            return True
        except Exception:
            self.logger.error("An error occurred while removing packages: {}".format(', '.join(to_remove)))
            traceback.print_exc()
            output_handler.stop_working()
            output_handler.join()
            return False

    def _show_upgrade_download_failed(self, watcher: ProcessWatcher):
        watcher.show_message(title=self.i18n['error'].capitalize(),
                             body=self.i18n['arch.upgrade.mthreaddownload.fail'],
                             type_=MessageType.ERROR)

    def upgrade(self, requirements: UpgradeRequirements, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        self.aur_client.clean_caches()
        watcher.change_status("{}...".format(self.i18n['manage_window.status.upgrading']))

        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            watcher.change_substatus('')
            return False

        aur_pkgs, repo_pkgs, pkg_sizes = [], [], {}

        for req in (*requirements.to_install, *requirements.to_upgrade):
            if req.pkg.repository == 'aur':
                aur_pkgs.append(req.pkg)
            else:
                repo_pkgs.append(req.pkg)

            pkg_sizes[req.pkg.name] = req.required_size

        arch_config = self.configman.get_config()
        aur_supported = bool(aur_pkgs) or aur.is_supported(arch_config)

        self._sync_databases(arch_config=arch_config, aur_supported=aur_supported,
                             root_password=root_password, handler=handler)

        if repo_pkgs and self.check_action_allowed(repo_pkgs[0], watcher):
            if not self._upgrade_repo_pkgs(to_upgrade=[p.name for p in repo_pkgs],
                                           to_remove={r.pkg.name for r in requirements.to_remove} if requirements.to_remove else None,
                                           handler=handler,
                                           root_password=root_password,
                                           multithread_download=self._multithreaded_download_enabled(arch_config),
                                           pkgs_data=requirements.context['data'],
                                           sizes=pkg_sizes):
                return False

        elif requirements.to_remove and not self._remove_transaction_packages({r.pkg.name for r in requirements.to_remove}, handler, root_password):
            return False

        if aur_pkgs and self.check_action_allowed(aur_pkgs[0], watcher) and self.add_package_builder_user(handler):
            watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs']))

            self.logger.info("Retrieving the 'last_modified' field for each package to upgrade")
            pkgs_api_data = self.aur_client.get_info({p.name for p in aur_pkgs})

            if not pkgs_api_data:
                self.logger.warning("Could not retrieve the 'last_modified' fields from the AUR API during the upgrade process")

            any_upgraded = False
            for pkg in aur_pkgs:
                watcher.change_substatus("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], pkg.name, pkg.version))

                if pkgs_api_data:
                    apidata = [p for p in pkgs_api_data if p.get('Name') == pkg.name]

                    if not apidata:
                        self.logger.warning("AUR API data from package '{}' could not be found".format(pkg.name))
                    else:
                        self.aur_mapper.fill_last_modified(pkg=pkg, api_data=apidata[0])

                context = TransactionContext.gen_context_from(pkg=pkg, arch_config=arch_config,
                                                              root_password=root_password, handler=handler, aur_supported=True)
                context.change_progress = False

                try:
                    if not self.install(pkg=pkg, root_password=root_password, watcher=watcher, disk_loader=None, context=context).success:
                        if any_upgraded:
                            self._update_aur_index(watcher)

                        watcher.print(self.i18n['arch.upgrade.fail'].format('"{}"'.format(pkg.name)))
                        self.logger.error("Could not upgrade AUR package '{}'".format(pkg.name))
                        watcher.change_substatus('')
                        return False
                    else:
                        any_upgraded = True
                        watcher.print(self.i18n['arch.upgrade.success'].format('"{}"'.format(pkg.name)))
                except Exception:
                    if any_upgraded:
                        self._update_aur_index(watcher)

                    watcher.print(self.i18n['arch.upgrade.fail'].format('"{}"'.format(pkg.name)))
                    watcher.change_substatus('')
                    self.logger.error("An error occurred when upgrading AUR package '{}'".format(pkg.name))
                    traceback.print_exc()
                    return False

            if any_upgraded:
                self._update_aur_index(watcher)

        watcher.change_substatus('')
        return True

    def _uninstall_pkgs(self, pkgs: Collection[str], root_password: Optional[str],
                        handler: ProcessHandler, ignore_dependencies: bool = False,
                        replacers: Optional[Set[str]] = None) -> bool:

        status_handler = TransactionStatusHandler(watcher=handler.watcher,
                                                  i18n=self.i18n,
                                                  names={*pkgs},
                                                  logger=self.logger,
                                                  pkgs_to_remove=len(pkgs))

        cmd = ['pacman', '-R', *pkgs, '--noconfirm']

        if ignore_dependencies:
            cmd.append('-dd')

        if replacers:
            cmd.extend(f'--assume-installed={r}' for r in replacers)

        status_handler.start()
        all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=cmd,
                                                                 root_password=root_password,
                                                                 error_phrases={'error: failed to prepare transaction',
                                                                                'error: failed to commit transaction'},
                                                                 shell=True),
                                                   output_handler=status_handler.handle)
        status_handler.stop_working()
        status_handler.join()

        installed = pacman.list_installed_names()

        for p in pkgs:
            if p not in installed:
                cache_path = ArchPackage.disk_cache_path(p)
                if os.path.exists(cache_path):
                    shutil.rmtree(cache_path)

        return all_uninstalled

    def _confirm_removal(self, to_remove: Collection[str], required: Collection[str],
                         watcher: ProcessWatcher, data: Optional[Dict[str, Dict[str, str]]] = None) -> bool:

        required_data = data if data is not None else self._map_installed_data_for_removal(required)
        reqs = self._map_as_input_options(required, required_data, read_only=True)
        reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="")

        main_msg = self.i18n['arch.uninstall.required_by'].format(no=bold(str(len(required))),
                                                                  pkgs=', '.join(bold(n) for n in to_remove)) + '.'

        full_msg = f"<p>{main_msg}</p><p>{self.i18n['arch.uninstall.required_by.warn'] + '.'}</p>"

        if not watcher.request_confirmation(title=self.i18n['warning'].capitalize(),
                                            body=full_msg,
                                            components=[reqs_select],
                                            confirmation_label=self.i18n['proceed'].capitalize(),
                                            deny_label=self.i18n['cancel'].capitalize(),
                                            min_width=600,
                                            window_cancel=False):
            watcher.print("Aborted")
            return False

        return True

    def _map_as_input_options(self, names: Iterable[str], data: Optional[Dict[str, Dict[str, str]]],
                              read_only: bool = False) -> List[InputOption]:
        opts = []
        for p in names:
            pkgdata = data and data.get(p)
            pkgver, pkgdesc, pkgrepo = None, None, None

            if pkgdata:
                pkgver, pkgdesc, pkgrepo = (pkgdata.get(k) for k in ('version', 'description', 'repository'))

            opts.append(InputOption(label=f"{p}{f' ({pkgver})' if pkgver else ''}", value=p, read_only=read_only,
                                    icon_path=get_repo_icon_path() if pkgrepo == 'repo' else get_icon_path(),
                                    tooltip=pkgdesc))

        return opts

    def _confirm_unneeded_removal(self, unnecessary: Iterable[str], watcher: ProcessWatcher,
                                  data: Optional[Dict[str, Dict[str, str]]] = None) -> Optional[Set[str]]:
        unneeded_data = data if data is not None else self._map_installed_data_for_removal(unnecessary)
        reqs = self._map_as_input_options(unnecessary, unneeded_data)
        reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="")

        if not watcher.request_confirmation(title=self.i18n['arch.uninstall.unnecessary.l1'],
                                            body=f"<p>{self.i18n['arch.uninstall.unnecessary.l2'] + ':'}</p>",
                                            components=[reqs_select],
                                            deny_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(),
                                            confirmation_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(),
                                            window_cancel=False,
                                            min_width=500):
            return {*reqs_select.get_selected_values()}

    def _confirm_all_unneeded_removal(self, pkgs: Collection[str], context: TransactionContext,
                                      data: Optional[Dict[str, Dict[str, str]]] = None) -> bool:
        unnecessary_data = data if data is not None else self._map_installed_data_for_removal(pkgs)
        reqs = self._map_as_input_options(pkgs, unnecessary_data, read_only=True)
        reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="")

        if not context.watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(),
                                                    body=self.i18n['arch.uninstall.unnecessary.all'].format(bold(str(len(pkgs)))),
                                                    components=[reqs_select],
                                                    confirmation_label=self.i18n['proceed'].capitalize(),
                                                    deny_label=self.i18n['cancel'].capitalize(),
                                                    window_cancel=False,
                                                    min_width=500):
            context.watcher.print("Aborted")
            return False

        return True

    def _fill_aur_providers(self, names: str, output: Set[str]):
        for _, data in self.aur_client.gen_updates_data(names):
            providers = data.get('p')

            if providers:
                output.update(providers)

    def _map_actual_replacers(self, names: Set[str], context: TransactionContext) -> Optional[Set[str]]:
        if not names:
            return

        repo_replacers, aur_replacers = set(), set()

        for r in names:
            repo = context.remote_repo_map.get(r)

            if repo and repo != 'aur':
                repo_replacers.add(r)
            elif repo == 'aur' or (context.aur_idx and r in context.aur_idx):
                aur_replacers.add(r)

        actual_replacers = set()

        thread_fill_aur = None
        if aur_replacers:
            thread_fill_aur = Thread(target=self._fill_aur_providers, args=(aur_replacers, actual_replacers), daemon=True)
            thread_fill_aur.start()

        if repo_replacers:
            repo_replace_providers = pacman.map_provided(remote=True, pkgs=repo_replacers)

            if repo_replace_providers:
                actual_replacers.update(repo_replace_providers)

        if thread_fill_aur:
            thread_fill_aur.join()

        return actual_replacers

    def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False,
                   disk_loader: Optional[DiskCacheLoader] = None, skip_requirements: bool = False,
                   replacers: Optional[Set[str]] = None):

        self._update_progress(context, 10)

        actual_replacers = self._map_actual_replacers(replacers, context) if replacers else None

        net_available = self.context.internet_checker.is_available() if disk_loader else True

        hard_requirements = set()

        if not skip_requirements:
            for n in names:
                try:
                    pkg_reqs = pacman.list_hard_requirements(name=n, logger=self.logger, assume_installed=actual_replacers)

                    if pkg_reqs:
                        hard_requirements.update(pkg_reqs)

                except PackageInHoldException:
                    error_msg = self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(n))
                    context.watcher.show_message(title=self.i18n['error'].capitalize(), body=error_msg,
                                                 type_=MessageType.ERROR)
                    return False

        self._update_progress(context, 25)

        to_uninstall = set()
        to_uninstall.update(names)

        if hard_requirements:
            to_uninstall.update(hard_requirements)

            if not self._confirm_removal(to_remove=names, required=hard_requirements, watcher=context.watcher):
                return False

        if not skip_requirements and remove_unneeded:
            unnecessary_packages = pacman.list_post_uninstall_unneeded_packages(to_uninstall)
            self.logger.info("Checking unnecessary optdeps")

            if context.config['suggest_optdep_uninstall']:
                unnecessary_packages.update(self._list_opt_deps_with_no_hard_requirements(source_pkgs=to_uninstall))

            self.logger.info("Packages no longer needed found: {}".format(len(unnecessary_packages)))
        else:
            unnecessary_packages = None

        self._update_progress(context, 50)

        if disk_loader and to_uninstall:  # loading package instances in case the removal succeeds
            instances = self.read_installed(disk_loader=disk_loader,
                                            names={n for n in to_uninstall},
                                            internet_available=net_available).installed

            if len(instances) != len(to_uninstall):
                self.logger.warning("Not all packages to be uninstalled could be read")
        else:
            instances = None

        provided_by_uninstalled = pacman.map_provided(pkgs=to_uninstall)

        uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler,
                                           ignore_dependencies=skip_requirements, replacers=actual_replacers)

        if uninstalled:
            self._remove_uninstalled_from_context(provided_by_uninstalled, context)

            if disk_loader:  # loading package instances in case the removal succeeds
                if instances:
                    for p in instances:
                        context.removed[p.name] = p

            self._update_progress(context, 70)

            if unnecessary_packages:
                unnecessary_to_uninstall = self._confirm_unneeded_removal(unnecessary=unnecessary_packages,
                                                                          watcher=context.watcher)

                if unnecessary_to_uninstall:
                    context.watcher.change_substatus(self.i18n['arch.checking_unnecessary_deps'])
                    unnecessary_requirements = set()

                    for pkg in unnecessary_to_uninstall:
                        try:
                            pkg_reqs = pacman.list_hard_requirements(pkg)

                            if pkg_reqs:
                                unnecessary_requirements.update(pkg_reqs)

                        except PackageInHoldException:
                            context.watcher.show_message(title=self.i18n['warning'].capitalize(),
                                                         body=self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(p)),
                                                         type_=MessageType.WARNING)

                    all_unnecessary_to_uninstall = {*unnecessary_to_uninstall, *unnecessary_requirements}

                    if not unnecessary_requirements or self._confirm_all_unneeded_removal(all_unnecessary_to_uninstall,
                                                                                          context):
                        if disk_loader:  # loading package instances in case the uninstall succeeds
                            unnecessary_instances = self.read_installed(disk_loader=disk_loader,
                                                                        internet_available=net_available,
                                                                        names=all_unnecessary_to_uninstall).installed
                        else:
                            unnecessary_instances = None

                        unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password,
                                                                   context.handler, replacers=actual_replacers)

                        if unneded_uninstalled:
                            to_uninstall.update(all_unnecessary_to_uninstall)

                            if disk_loader and unnecessary_instances:  # loading package instances in case the uninstall succeeds
                                for p in unnecessary_instances:
                                    context.removed[p.name] = p
                            else:
                                self.logger.error("Could not uninstall some unnecessary packages")
                                context.watcher.print("Could not uninstall some unnecessary packages")

            self._update_progress(context, 90)

            if bool(context.config['clean_cached']):  # cleaning old versions
                context.watcher.change_substatus(self.i18n['arch.uninstall.clean_cached.substatus'])
                if os.path.isdir('/var/cache/pacman/pkg'):
                    for p in to_uninstall:
                        available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(p))

                        if available_files and not context.handler.handle_simple(SimpleProcess(cmd=['rm', '-rf', *available_files],
                                                                                 root_password=context.root_password)):
                            context.watcher.show_message(title=self.i18n['error'],
                                                         body=self.i18n['arch.uninstall.clean_cached.error'].format(bold(p)),
                                                         type_=MessageType.WARNING)

                self._revert_ignored_updates(to_uninstall)

            self._remove_from_editable_pkgbuilds(context.name)

        self._update_progress(context, 100)
        return uninstalled

    def _map_installed_data_for_removal(self, names: Iterable[str]) -> Optional[Dict[str, Dict[str, str]]]:
        data = pacman.map_packages(names)

        if data:
            remapped_data = {}
            for key, pkgs in data.items():
                repository = 'aur' if key == 'not_signed' else 'repo'
                for name, data in pkgs.items():
                    remapped_data[name] = {**data, 'repository': repository}

            return remapped_data

    def _remove_uninstalled_from_context(self, provided_by_uninstalled: Dict[str, Set[str]], context: TransactionContext):
        if context.provided_map and provided_by_uninstalled:  # updating the current provided context
            for name, provided in provided_by_uninstalled.items():
                if name in context.provided_map:
                    del context.provided_map[name]

                if provided:
                    for exp in provided:
                        exp_provided = context.provided_map.get(exp)

                        if exp_provided and name in exp_provided:
                            exp_provided.remove(name)

                            if not exp_provided:
                                del context.provided_map[exp]

    def uninstall(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult:
        self.aur_client.clean_caches()

        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            return TransactionResult.fail()

        removed = {}
        arch_config = self.configman.get_config()
        success = self._uninstall(TransactionContext(change_progress=True,
                                                     arch_config=arch_config,
                                                     watcher=watcher,
                                                     root_password=root_password,
                                                     handler=handler,
                                                     removed=removed,
                                                     aur_supported=pkg.repository == 'aur' or aur.is_supported(arch_config)),
                                  remove_unneeded=arch_config['suggest_unneeded_uninstall'],
                                  names={pkg.name},
                                  disk_loader=disk_loader)  # to be able to return all uninstalled packages
        if success:
            removed_list = []

            main_removed = removed.get(pkg.name)
            if main_removed:
                pkg.installed = False
                pkg.url_download = main_removed.url_download  # otherwise uninstalled AUR packages cannot be reinstalled on the same view
                removed_list.append(pkg)

            removed_list.extend((inst for name, inst in removed.items() if name != pkg.name))
            return TransactionResult(success=not pkg.installed, installed=None, removed=removed_list)
        else:
            return TransactionResult.fail()

    def get_managed_types(self) -> Set["type"]:
        return {ArchPackage}

    def _map_info_aur_installed(self, pkg: ArchPackage, pkgbuild_thread: Thread) -> Dict[str, Any]:
        info = pacman.get_info_dict(pkg.name)

        if info is not None:
            self._parse_dates_string_from_info(pkg.name, info)

            info['04_orphan'] = pkg.orphan
            info['04_out_of_date'] = pkg.out_of_date

            if pkg.commit:
                info['commit'] = pkg.commit

            if pkg.last_modified:
                err_msg = f"Could not parse AUR package '{pkg.name}' 'last_modified' field ({pkg.last_modified})"
                info['last_modified'] = self._parse_timestamp(ts=pkg.last_modified, error_msg=err_msg)

            info['14_installed_files'] = pacman.list_installed_files(pkg.name)

            pkgbuild_thread.join()

            if pkg.pkgbuild:
                info['13_pkg_build'] = pkg.pkgbuild

        return info

    def _map_info_aur_uninstalled(self, pkg: ArchPackage, pkgbuild_thread: Thread) -> Dict[str, Any]:
        info = {
            '01_id': pkg.id,
            '02_name': pkg.name,
            '03_description': pkg.description,
            '03_version': pkg.version,
            '04_orphan': pkg.orphan,
            '04_out_of_date': pkg.out_of_date,
            '04_popularity': pkg.popularity,
            '05_votes': pkg.votes,
            '06_package_base': pkg.package_base,
            '07_maintainer': pkg.maintainer,
            '10_url': pkg.url_download
        }

        if pkg.first_submitted:
            info['08_first_submitted'] = self._parse_timestamp(ts=pkg.first_submitted,
                                                               error_msg="Could not parse AUR package '{}' 'first_submitted' field ({})".format(
                                                                   pkg.name, pkg.first_submitted))

        if pkg.last_modified:
            info['09_last_modified'] = self._parse_timestamp(ts=pkg.last_modified,
                                                             error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(
                                                                 pkg.name, pkg.last_modified))

        srcinfo = self.aur_client.get_src_info(pkg.name)

        if srcinfo:
            arch_str = 'x86_64' if self.context.is_system_x86_64() else 'i686'
            for info_attr, src_attr in {'12_makedepends': 'makedepends',
                                        '13_dependson': 'depends',
                                        '14_optdepends': 'optdepends',
                                        'checkdepends': '15_checkdepends'}.items():
                if srcinfo.get(src_attr):
                    info[info_attr] = [*srcinfo[src_attr]]

                arch_attr = f"{src_attr}_{arch_str}"

                if srcinfo.get(arch_attr):
                    if not info.get(info_attr):
                        info[info_attr] = [*srcinfo[arch_attr]]
                    else:
                        info[info_attr].extend(srcinfo[arch_attr])

        pkgbuild_thread.join()

        if pkg.pkgbuild:
            info['00_pkg_build'] = pkg.pkgbuild
        else:
            info['11_pkg_build_url'] = pkg.get_pkg_build_url()

        return info

    def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict:
        fill_pkgbuild = Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True)
        fill_pkgbuild.start()

        if pkg.installed:
            info = self._map_info_aur_installed(pkg, fill_pkgbuild)
        else:
            info = self._map_info_aur_uninstalled(pkg, fill_pkgbuild)

        info["00_url"] = f"https://aur.archlinux.org/packages/{pkg.get_base_name()}"
        return info

    def _parse_dates_string_from_info(self, pkgname: str, info: dict):
        for date_attr in ('install date', 'build date'):
            en_date_str = info.get(date_attr)

            if en_date_str:
                try:
                    info[date_attr] = parse_date(en_date_str)
                except ValueError:
                    self.logger.error("Could not parse date attribute '{}' ({}) from package '{}'".format(date_attr, en_date_str, pkgname))

    def _parse_timestamp(self, ts: int, error_msg: str) -> datetime:
        if ts:
            try:
                return datetime.fromtimestamp(ts)
            except ValueError:
                if error_msg:
                    self.logger.error(error_msg)

    def _get_info_repo_pkg(self, pkg: ArchPackage) -> dict:
        info = pacman.get_info_dict(pkg.name, remote=not pkg.installed)

        if info is not None:
            self._parse_dates_string_from_info(pkg.name, info)

            if pkg.installed:
                info['installed files'] = pacman.list_installed_files(pkg.name)

        for attr in ('version', 'description'):
            val = info.get(attr)

            if val is not None:
                info[f'03_{attr}'] = val
                del info[attr]

        return info

    def get_info(self, pkg: ArchPackage) -> dict:
        if pkg.repository == 'aur':
            info = self._get_info_aur_pkg(pkg)
        else:
            info = self._get_info_repo_pkg(pkg)

        if pkg.is_application():
            info['04_exec'] = pkg.command

        return info

    def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory:

        if pkg.commit:
            self.logger.info("Package '{}' current commit {}".format(pkg.name, pkg.commit))
        else:
            self.logger.warning("Package '{}' has no commit associated with it. Current history status may not be correct.".format(pkg.name))

        arch_config = self.configman.get_config()
        temp_dir = f'{get_build_dir(arch_config, self.pkgbuilder_user)}/build_{int(time.time())}'

        try:
            Path(temp_dir).mkdir(parents=True)
            base_name = pkg.get_base_name()
            run_cmd('git clone ' + URL_GIT.format(base_name), print_error=False, cwd=temp_dir)

            clone_dir = f'{temp_dir}/{base_name}'

            srcinfo_path = f'{clone_dir}/.SRCINFO'

            if not os.path.exists(srcinfo_path):
                return PackageHistory.empyt(pkg)

            logs = git.list_commits(clone_dir)

            if logs:
                srcfields = {'epoch', 'pkgver', 'pkgrel'}
                history, status_idx = [], -1

                for idx, log in enumerate(logs):
                    commit, timestamp = log[0], log[1]

                    with open(srcinfo_path) as f:
                        pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=pkg.name, fields=srcfields)

                    epoch, version, release = pkgsrc.get('epoch'), pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')

                    pkgver = '{}:{}'.format(epoch, version) if epoch is not None else version
                    current_version = '{}-{}'.format(pkgver, release)

                    if status_idx < 0:
                        if pkg.commit:
                            status_idx = idx if pkg.commit == commit else -1
                        else:
                            status_idx = idx if current_version == pkg.version else -1

                    history.append({'1_version': pkgver, '2_release': release,
                                    '3_date': datetime.fromtimestamp(timestamp)})  # the number prefix is to ensure the rendering order

                    if idx + 1 < len(logs):
                        if not run_cmd('git reset --hard ' + logs[idx + 1][0], cwd=clone_dir):
                            break

                return PackageHistory(pkg=pkg, history=history, pkg_status_idx=status_idx)
        finally:
            if os.path.exists(temp_dir):
                shutil.rmtree(temp_dir)

    def _get_history_repo_pkg(self, pkg: ArchPackage) -> PackageHistory:
        data = PackageHistory(pkg=pkg, history=[], pkg_status_idx=-1)

        versions = [pkg.latest_version]
        version_files = {}  # maps the version and tar file

        if pkg.update:
            versions.append(pkg.version)

        if os.path.isdir('/var/cache/pacman/pkg'):
            available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(pkg.name))

            if available_files:
                reg = re.compile(r'{}-([\w.\-]+)-(x86_64|any|i686).pkg'.format(pkg.name))

                for file_path in available_files:
                    found = reg.findall(os.path.basename(file_path))

                    if found:
                        ver = found[0][0]
                        if ver not in versions:
                            versions.append(ver)

                        version_files[ver] = file_path

        versions.sort(reverse=True)
        extract_path = f'{TEMP_DIR}/arch/history'

        try:
            Path(extract_path).mkdir(parents=True, exist_ok=True)
        except Exception:
            self.logger.error("Could not create temp dir {} to extract previous versions data".format(extract_path))
            traceback.print_exc()
            return data

        try:
            for idx, v in enumerate(versions):
                cur_version = v.split('-')
                cur_data = {'1_version': ''.join(cur_version[0:-1]),
                            '2_release': cur_version[-1],
                            '3_date': ''}

                if pkg.version == v:
                    data.pkg_status_idx = idx

                version_file = version_files.get(v)

                if not version_file:
                    if v == pkg.version:
                        cur_data['3_date'] = pacman.get_build_date(pkg.name)
                else:
                    extracted_dir = '{}/{}'.format(extract_path, v)
                    Path(extracted_dir).mkdir(parents=True, exist_ok=True)

                    try:
                        filext = version_file.split('.')[-1]
                        run_cmd('tar -C {} -I {} -xvf {} .PKGINFO'.format(extracted_dir, 'zstd' if filext == 'zst' else filext, version_file))
                    except tarfile.ReadError:
                        if v == pkg.version:
                            cur_data['3_date'] = pacman.get_build_date(pkg.name)
                        else:
                            self.logger.error("Could not read file {}. Skipping version {}".format(version_file, v))
                            continue

                    info_file = '{}/.PKGINFO'.format(extracted_dir)
                    if os.path.isfile(info_file):
                        with open(info_file) as f:
                            for l in f.readlines():
                                if l and l.startswith('builddate'):
                                    cur_data['3_date'] = datetime.fromtimestamp(int(l.split('=')[1].strip()))
                                    break

                data.history.append(cur_data)
            return data

        finally:
            if os.path.exists(extract_path):
                try:
                    self.logger.info("Removing temporary history dir {}".format(extract_path))
                    shutil.rmtree(extract_path)
                except Exception:
                    self.logger.error("Could not remove temp path '{}'".format(extract_path))
                    raise

    def get_history(self, pkg: ArchPackage) -> PackageHistory:
        if pkg.repository == 'aur':
            return self._get_history_aur_pkg(pkg)
        else:
            return self._get_history_repo_pkg(pkg)

    def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: TransactionContext,
                                     skip_requirements: bool = False) -> bool:

        conflict_msg = f"{bold(pkg)} {self.i18n['and']} {bold(conflicting_pkg)}"
        msg_body = self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)

        if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], body=msg_body):
            context.watcher.print(self.i18n['action.cancelled'])
            return False
        else:
            context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflicting_pkg)))
            context.disable_progress_if_changing()

            if context.removed is None:
                context.removed = {}

            res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader,
                                  remove_unneeded=False, skip_requirements=skip_requirements, replacers={pkg})

            context.restabilish_progress()
            return res

    def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]]) -> Optional[Iterable[str]]:
        progress_increment = int(100 / len(deps))
        progress = 0
        self._update_progress(context, 1)

        repo_deps, repo_dep_names, aur_deps_context = [], None, []

        for dep in deps:
            context.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ({})'.format(dep[0], dep[1]))))

            if dep[1] == 'aur':
                dep_context = context.gen_dep_context(dep[0], dep[1])
                dep_src = self.aur_client.get_src_info(dep[0])
                dep_context.base = dep_src['pkgbase'] if dep_src['pkgbase'] else dep[0]
                aur_deps_context.append(dep_context)
            else:
                repo_deps.append(dep)

        if repo_deps:
            repo_dep_names = [d[0] for d in repo_deps]

            if context.dependency:
                context.watcher.change_substatus(self.i18n['arch.substatus.conflicts'])
            else:
                context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name)))

            all_provided = context.get_provided_map()

            for dep, data in pacman.map_conflicts_with(repo_dep_names, remote=True).items():
                if data and data['c']:
                    for c in data['c']:
                        source_conflict = all_provided.get(c)

                        if source_conflict:
                            conflict_pkg = [*source_conflict][0]

                            if dep != conflict_pkg:
                                if not self._request_conflict_resolution(dep, conflict_pkg, context,
                                                                         skip_requirements=data['r'] and conflict_pkg in data['r']):
                                    return {dep}

            downloaded = 0
            if self._multithreaded_download_enabled(context.config):
                try:
                    pkg_sizes = pacman.map_download_sizes(repo_dep_names)
                    downloaded = self._download_packages(repo_dep_names, context.handler, context.root_password, pkg_sizes, multithreaded=True)
                except ArchDownloadException:
                    return None

            status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, names={*repo_dep_names},
                                                      logger=self.logger, percentage=len(repo_deps) > 1, downloading=downloaded)
            status_handler.start()
            installed, _ = context.handler.handle_simple(pacman.install_as_process(pkgpaths=repo_dep_names,
                                                                                   root_password=context.root_password,
                                                                                   file=False,
                                                                                   as_deps=True),
                                                         output_handler=status_handler.handle)

            if installed:
                pkg_map = {d[0]: ArchPackage(name=d[0], repository=d[1], maintainer=d[1],
                                             categories=self.categories.get(d[0])) for d in repo_deps}
                disk.write_several(pkg_map, overwrite=True, maintainer=None)
                progress += len(repo_deps) * progress_increment
                self._update_progress(context, progress)
            else:
                return repo_dep_names

        if aur_deps_context:
            aur_deps_info = self.aur_client.get_info((c.base for c in aur_deps_context))
            aur_deps_data = None

            if aur_deps_info:
                aur_deps_data = {data['Name']: data for data in aur_deps_info}

            for aur_context in aur_deps_context:
                if aur_deps_data:
                    dep_data = aur_deps_data.get(aur_context.base)

                    if dep_data:
                        last_modified = dep_data.get('LastModified')

                        if last_modified and isinstance(last_modified, int):
                            aur_context.last_modified = last_modified
                        else:
                            self.logger.warning(f"No valid 'LastModified' field returned for AUR package "
                                                f"'{context.name}': {last_modified}")

                installed = self._install_from_aur(aur_context)

                if not installed:
                    return {aur_context.name}
                else:
                    progress += progress_increment
                    self._update_progress(context, progress)

        self._update_progress(context, 100)

    def _map_repos(self, pkgnames: Collection[str]) -> dict:
        pkg_repos = pacman.get_repositories(pkgnames)  # getting repositories set

        if len(pkgnames) != len(pkg_repos):  # checking if any dep not found in the distro repos are from AUR
            norepos = {p for p in pkgnames if p not in pkg_repos}

            aur_info = self.aur_client.get_info(norepos)

            if aur_info:
                for pkginfo in aur_info:
                    if pkginfo.get('Name') in norepos:
                        pkg_repos[pkginfo['Name']] = 'aur'

        return pkg_repos

    def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessWatcher) -> bool:
        # TODO: multi-threaded download client cannot be run as another user at the moment
        if not self.context.root_user and self.context.file_downloader.is_multithreaded():
            with open('{}/.SRCINFO'.format(project_dir)) as f:
                srcinfo = aur.map_srcinfo(string=f.read(), pkgname=pkgname)

            pre_download_files = []

            for attr in SOURCE_FIELDS:
                if srcinfo.get(attr):
                    if attr == 'source_x86_x64' and not self.context.is_system_x86_64():
                        continue
                    else:
                        for f in srcinfo[attr]:
                            if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f):
                                pre_download_files.append(f)

            if pre_download_files:
                for f in pre_download_files:
                    fdata = f.split('::')

                    args = {'watcher': watcher, 'cwd': project_dir}
                    if len(fdata) > 1:
                        args.update({'file_url': fdata[1], 'output_path': fdata[0]})
                    else:
                        args.update({'file_url': fdata[0], 'output_path': fdata[0].split('/')[-1]})

                    if not self.context.file_downloader.download(**args):
                        watcher.print('Could not download source file {}'.format(args['file_url']))
                        return False

        return True

    def _display_pkgbuild_for_editing(self, pkgname: str, watcher: ProcessWatcher, pkgbuild_path: str) -> bool:
        with open(pkgbuild_path) as f:
            pkgbuild = f.read()

        pkgbuild_input = TextInputComponent(label='', value=pkgbuild, type_=TextInputType.MULTIPLE_LINES,
                                            min_width=500, min_height=350)

        watcher.request_confirmation(title='PKGBUILD ({})'.format(pkgname),
                                     body='',
                                     components=[pkgbuild_input],
                                     confirmation_label=self.i18n['proceed'].capitalize(),
                                     deny_button=False)

        if pkgbuild_input.get_value() != pkgbuild:
            if not write_as_user(content=pkgbuild_input.get_value(), file_path=pkgbuild_path, user=self.pkgbuilder_user):
                return False

            return makepkg.update_srcinfo(project_dir='/'.join(pkgbuild_path.split('/')[0:-1]),
                                          custom_user=self.pkgbuilder_user)

        return False

    def _ask_for_pkgbuild_edition(self, pkgname: str, arch_config: dict, watcher: ProcessWatcher, pkgbuild_path: str) -> bool:
        if pkgbuild_path:
            if arch_config['edit_aur_pkgbuild'] is None:
                if not watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(),
                                                    body=self.i18n['arch.aur.action.edit_pkgbuild.body'].format(bold(pkgname)),
                                                    confirmation_label=self.i18n['no'].capitalize(),
                                                    deny_label=self.i18n['yes'].capitalize()):
                    return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path)
            elif arch_config['edit_aur_pkgbuild']:
                return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path)

        return False

    def _edit_pkgbuild_and_update_context(self, context: TransactionContext):
        if context.new_pkg or context.name in self._read_editable_pkgbuilds():
            if self._ask_for_pkgbuild_edition(pkgname=context.name,
                                              arch_config=context.config,
                                              watcher=context.watcher,
                                              pkgbuild_path=f'{context.project_dir}/PKGBUILD'):
                context.pkgbuild_edited = True
                srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(build_dir=context.project_dir, custom_user=self.pkgbuilder_user),
                                          pkgname=context.name)

                if srcinfo:
                    context.name = srcinfo['pkgname']
                    context.base = srcinfo['pkgbase']

                    if context.pkg:
                        for pkgattr, srcattr in {'name': 'pkgname',
                                                 'package_base': 'pkgbase',
                                                 'version': 'pkgversion',
                                                 'latest_version': 'pkgversion',
                                                 'license': 'license',
                                                 'description': 'pkgdesc'}.items():
                            setattr(context.pkg, pkgattr, srcinfo.get(srcattr, getattr(context.pkg, pkgattr)))

    def _read_srcinfo(self, context: TransactionContext) -> str:
        src_path = f'{context.project_dir}/.SRCINFO'

        if not os.path.exists(src_path):
            srcinfo = makepkg.gen_srcinfo(build_dir=context.project_dir,
                                          custom_pkgbuild_path=context.custom_pkgbuild_path,
                                          custom_user=self.pkgbuilder_user)

            write_as_user(content=srcinfo, file_path=src_path, user=self.pkgbuilder_user)
        else:
            with open(src_path) as f:
                srcinfo = f.read()

        return srcinfo

    def _build(self, context: TransactionContext) -> bool:
        self._edit_pkgbuild_and_update_context(context)
        self._pre_download_source(context.name, context.project_dir, context.watcher)
        self._update_progress(context, 50)

        context.custom_pkgbuild_path = self._gen_custom_pkgbuild_if_required(context)

        if not self._handle_aur_package_deps_and_keys(context):
            return False

        # building main package
        context.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(context.name)))
        optimize = bool(context.config['optimize']) and cpu_manager.supports_performance_mode()

        cpus_changed, cpu_prev_governors = False, None

        if optimize:
            cpus_changed, cpu_prev_governors = cpu_manager.set_all_cpus_to('performance', context.root_password,
                                                                           self.logger)

        pkgbuilt = False

        try:
            pkgbuilt, output = makepkg.build(pkgdir=context.project_dir,
                                             optimize=optimize,
                                             handler=context.handler,
                                             custom_pkgbuild=context.custom_pkgbuild_path,
                                             custom_user=self.pkgbuilder_user)
        finally:
            if cpus_changed and cpu_prev_governors:
                self.logger.info("Restoring CPU governors")
                cpu_manager.set_cpus(cpu_prev_governors, context.root_password, self.logger, {'performance'})

        self._update_progress(context, 65)

        if pkgbuilt:
            self.__fill_aur_output_files(context)

            self.logger.info(f"Reading '{context.name}' cloned repository current commit")
            commits = git.list_commits(context.project_dir, limit=1)

            if commits:
                context.commit = commits[0][0]

            else:
                self.logger.error(f"Could not read '{context.name}' cloned repository current commit")

            if self._install(context=context):
                self._save_pkgbuild(context)

                if context.update_aur_index:
                    self._update_aur_index(context.watcher)

                if context.dependency or context.skip_opt_deps:
                    return True

                context.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(context.name)))

                self._update_progress(context, 100)

                if self._install_optdeps(context):
                    return True

        return False

    def _update_aur_index(self, watcher: ProcessWatcher):
        if self.context.internet_checker.is_available():
            if watcher:
                watcher.change_substatus(self.i18n['arch.task.aur.index.status'])

            idx_updater = AURIndexUpdater(context=self.context, taskman=TaskManager())  # null task manager
            idx_updater.update_index()
        else:
            self.logger.warning("Could not update the AUR index: no internet connection detected")

    def __fill_aur_output_files(self, context: TransactionContext):
        self.logger.info("Determining output files of '{}'".format(context.name))
        context.watcher.change_substatus(self.i18n['arch.aur.build.list_output'])

        output_files = {f for f in makepkg.list_output_files(project_dir=context.project_dir,
                                                             custom_pkgbuild_path=context.custom_pkgbuild_path,
                                                             custom_user=self.pkgbuilder_user) if os.path.isfile(f)}

        if output_files:
            context.install_files = output_files
        else:
            gen_file = [fname for root, dirs, files in os.walk(context.build_dir) for fname in files if
                        re.match(r'^{}-.+\.tar\.(xz|zst)'.format(context.name), fname)]

            if not gen_file:
                context.watcher.print('Could not find the built package. Aborting...')
                return False

            file_to_install = gen_file[0]

            if len(gen_file) > 1:
                srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(build_dir=context.project_dir, custom_user=self.pkgbuilder_user),
                                          pkgname=context.name)
                pkgver = '-{}'.format(srcinfo['pkgver']) if srcinfo.get('pkgver') else ''
                pkgrel = '-{}'.format(srcinfo['pkgrel']) if srcinfo.get('pkgrel') else ''
                arch = '-{}'.format(srcinfo['arch']) if srcinfo.get('arch') else ''
                name_start = '{}{}{}{}'.format(context.name, pkgver, pkgrel, arch)

                perfect_match = [f for f in gen_file if f.startswith(name_start)]

                if perfect_match:
                    file_to_install = perfect_match[0]

            context.install_files = {'{}/{}'.format(context.project_dir, file_to_install)}

        context.watcher.change_substatus('')

    def _save_pkgbuild(self, context: TransactionContext):
        cache_path = ArchPackage.disk_cache_path(context.name)
        if not os.path.exists(cache_path):
            try:
                os.mkdir(cache_path)
            except Exception:
                print("Could not create cache directory '{}'".format(cache_path))
                traceback.print_exc()
                return

        src_pkgbuild = '{}/PKGBUILD'.format(context.project_dir)
        dest_pkgbuild = '{}/PKGBUILD'.format(cache_path)
        try:
            shutil.copy(src_pkgbuild, dest_pkgbuild)
        except Exception:
            context.watcher.print("Could not copy '{}' to '{}'".format(src_pkgbuild, dest_pkgbuild))
            traceback.print_exc()

    def _ask_and_install_missing_deps(self, context: TransactionContext, missing_deps: List[Tuple[str, str]]) -> bool:
        context.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(context.name)))

        if not confirm_missing_deps(missing_deps, context.watcher, self.i18n):
            context.watcher.print(self.i18n['action.cancelled'])
            return False

        old_progress_behavior = context.change_progress
        context.change_progress = False
        deps_not_installed = self._install_deps(context, missing_deps)
        context.change_progress = old_progress_behavior

        if deps_not_installed:
            message.show_deps_not_installed(context.watcher, context.name, deps_not_installed, self.i18n)
            return False

        context.installed.update({d[0] for d in missing_deps})

        return True

    def _list_missing_deps(self, context: TransactionContext) -> List[Tuple[str, str]]:
        context.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(context.name)))
        ti = time.time()

        if context.repository == 'aur':
            srcinfo = aur.map_srcinfo(string=self._read_srcinfo(context),
                                      pkgname=context.name if (not context.pkgs_to_build or len(context.pkgs_to_build) == 1) else None)

            if context.pkgs_to_build and len(context.pkgs_to_build) > 1:  # removing self dependencies from srcinfo
                for attr in ('depends', 'makedepends', 'optdepends'):
                    dep_list = srcinfo.get(attr)

                    if dep_list and isinstance(dep_list, list):
                        to_remove = set()
                        for dep in dep_list:
                            dep_name = RE_DEP_OPERATORS.split(dep.split(':')[0])[0].strip()

                            if dep_name and dep_name in context.pkgs_to_build:
                                to_remove.add(dep)

                        for dep in to_remove:
                            dep_list.remove(dep)

            pkgs_data = {context.name: self.aur_client.map_update_data(context.name, context.get_version(), srcinfo)}
        else:
            pkgs_data = pacman.map_updates_data(context.get_packages_paths(), files=bool(context.install_files))

        deps_data, alread_checked_deps = {}, set()

        missing_deps = self.deps_analyser.map_missing_deps(pkgs_data=pkgs_data,
                                                           provided_map=context.get_provided_map(),
                                                           aur_index=context.get_aur_idx(self.aur_client),
                                                           deps_checked=alread_checked_deps,
                                                           deps_data=deps_data,
                                                           sort=True,
                                                           remote_provided_map=context.get_remote_provided_map(),
                                                           remote_repo_map=context.get_remote_repo_map(),
                                                           automatch_providers=context.config['automatch_providers'],
                                                           prefer_repository_provider=context.config['prefer_repository_provider'],
                                                           watcher=context.watcher)

        tf = time.time()
        self.logger.info("Took {0:.2f} seconds to check for missing dependencies".format(tf - ti))
        return missing_deps

    def _handle_missing_deps(self, context: TransactionContext) -> bool:
        try:
            missing_deps = self._list_missing_deps(context)
        except PackageNotFoundException:
            return False
        except Exception:
            traceback.print_exc()
            return False

        if missing_deps is None:
            return False  # called off by the user

        if not missing_deps:
            return True
        elif not self._ask_and_install_missing_deps(context=context, missing_deps=missing_deps):
            return False  # called off by the user or something went wrong
        else:
            return True

    def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool:
        if not context.dependency:
            handled_deps = self._handle_missing_deps(context)
            if not handled_deps:
                return False

        check_res = makepkg.check(project_dir=context.project_dir,
                                  optimize=bool(context.config['optimize']),
                                  missing_deps=False,
                                  handler=context.handler,
                                  custom_pkgbuild=context.custom_pkgbuild_path,
                                  custom_user=self.pkgbuilder_user)

        if check_res:
            if check_res.get('gpg_key'):
                if context.watcher.request_confirmation(title=self.i18n['arch.install.aur.unknown_key.title'],
                                                        body=self.i18n['arch.install.aur.unknown_key.body'].format(bold(context.name), bold(check_res['gpg_key']))):
                    context.watcher.change_substatus(self.i18n['arch.aur.install.unknown_key.status'].format(bold(check_res['gpg_key'])))
                    self.logger.info("Importing GPG key {}".format(check_res['gpg_key']))

                    gpg_res = self.context.http_client.get(URL_GPG_SERVERS)
                    gpg_server = gpg_res.text.split('\n')[0] if gpg_res else None

                    if not context.handler.handle(gpg.receive_key(check_res['gpg_key'], gpg_server)):
                        self.logger.error("An error occurred while importing the GPG key {}".format(check_res['gpg_key']))
                        context.watcher.show_message(title=self.i18n['error'].capitalize(),
                                                     body=self.i18n['arch.aur.install.unknown_key.receive_error'].format(bold(check_res['gpg_key'])))

                        return False
                else:
                    context.watcher.print(self.i18n['action.cancelled'])
                    return False

            if check_res.get('validity_check'):
                body = "<p>{}</p><p>{}</p>".format(self.i18n['arch.aur.install.validity_check.body'].format(bold(context.name)),
                                                   self.i18n['arch.aur.install.validity_check.proceed'])
                return not context.watcher.request_confirmation(title=self.i18n['arch.aur.install.validity_check.title'].format('( checksum )'),
                                                                body=body,
                                                                confirmation_label=self.i18n['no'].capitalize(),
                                                                deny_label=self.i18n['yes'].capitalize())

        return True

    def _install_optdeps(self, context: TransactionContext) -> bool:
        odeps = pacman.map_optional_deps({context.name}, remote=False, not_installed=True).get(context.name)

        if not odeps:
            return True

        repo_mapping = self._map_repos(odeps.keys())

        if repo_mapping:
            final_optdeps = {dep: {'desc': odeps.get(dep), 'repository': repo_mapping.get(dep)} for dep, repository in repo_mapping.items() if repo_mapping.get(dep)}

            deps_to_install = confirmation.request_optional_deps(context.name, final_optdeps, context.watcher, self.i18n, context.config['suggest_optdep_select'])

            if not deps_to_install:
                return True
            else:
                deps_data = {}
                opt_repo_deps, aur_threads = [], []

                for dep in deps_to_install:
                    if repo_mapping[dep] == 'aur':
                        t = Thread(target=self.aur_client.fill_update_data, args=(deps_data, dep, None, None), daemon=True)
                        t.start()
                        aur_threads.append(t)
                    else:
                        opt_repo_deps.append(dep)

                if opt_repo_deps:
                    deps_data.update(pacman.map_updates_data(opt_repo_deps))

                for t in aur_threads:
                    t.join()

                provided_map = pacman.map_provided()
                remote_provided_map = pacman.map_provided(remote=True)
                remote_repo_map = pacman.map_repositories()
                aur_index = self.aur_client.read_index() if aur_threads else None
                subdeps_data = {}
                missing_deps = self.deps_analyser.map_missing_deps(pkgs_data=deps_data,
                                                                   provided_map=provided_map,
                                                                   aur_index=aur_index,
                                                                   deps_checked=set(),
                                                                   deps_data=subdeps_data,
                                                                   watcher=context.watcher,
                                                                   remote_provided_map=remote_provided_map,
                                                                   remote_repo_map=remote_repo_map,
                                                                   automatch_providers=context.config['automatch_providers'],
                                                                   prefer_repository_provider=context.config['prefer_repository_provider'],
                                                                   sort=False)

                if missing_deps is None:
                    return False  # called of by the user

                to_sort = []
                if missing_deps:
                    for dep in missing_deps:
                        # TODO handle multiple providers for a missing dep
                        if dep[0] not in deps_to_install and dep[1] != '__several__':
                            to_sort.append(dep[0])

                display_deps_dialog = bool(to_sort)  # it means there are subdeps to be installed so a new dialog should be displayed

                to_sort.extend(deps_data.keys())

                sorted_deps = sorting.sort(to_sort, {**deps_data, **subdeps_data}, provided_map)

                if display_deps_dialog and not confirm_missing_deps(sorted_deps, context.watcher, self.i18n):
                    context.watcher.print(self.i18n['action.cancelled'])
                    return True  # because the main package installation was successful

                old_progress_behavior = context.change_progress
                context.change_progress = True
                context.dependency = True
                deps_not_installed = self._install_deps(context, sorted_deps)
                context.change_progress = old_progress_behavior

                if deps_not_installed:
                    message.show_optdeps_not_installed(deps_not_installed, context.watcher, self.i18n)
                else:
                    context.installed.update({dep[0] for dep in sorted_deps})

        return True

    def _multithreaded_download_enabled(self, arch_config: dict) -> bool:
        return bool(arch_config['repositories_mthread_download']) \
            and self.context.file_downloader.is_multithreaded() \
            and pacman.is_mirrors_available()

    def _download_packages(self, pkgnames: List[str], handler: ProcessHandler, root_password: Optional[str], sizes: Dict[str, int] = None, multithreaded: bool = True) -> int:
        if multithreaded:
            download_service = MultithreadedDownloadService(file_downloader=self.context.file_downloader,
                                                            http_client=self.http_client,
                                                            logger=self.context.logger,
                                                            i18n=self.i18n)
            self.logger.info("Initializing multi-threaded download for {} repository package(s)".format(len(pkgnames)))
            return download_service.download_packages(pkgs=pkgnames,
                                                      handler=handler,
                                                      sizes=sizes,
                                                      root_password=root_password)
        else:
            self.logger.info("Downloading {} repository package(s)".format(len(pkgnames)))
            output_handler = TransactionStatusHandler(handler.watcher, self.i18n, pkgnames, self.logger)
            output_handler.start()
            try:
                success, _ = handler.handle_simple(pacman.download(root_password, *pkgnames), output_handler=output_handler.handle)

                if success:
                    return len(pkgnames)
                else:
                    raise ArchDownloadException()
            except Exception:
                traceback.print_exc()
                raise ArchDownloadException()

    def _install(self, context: TransactionContext) -> bool:
        pkgpaths = context.get_packages_paths()

        context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name)))
        self.logger.info("Checking for possible conflicts with '{}'".format(context.name))

        _, output = context.handler.handle_simple(pacman.install_as_process(pkgpaths=pkgpaths,
                                                                            root_password=context.root_password,
                                                                            pkgdir=context.project_dir or '.',
                                                                            file=bool(context.install_files),
                                                                            simulate=True),
                                                  notify_watcher=False)

        self._update_progress(context, 70)

        if 'unresolvable package conflicts detected' in output:
            self.logger.info("Conflicts detected for '{}'".format(context.name))
            conflict_msgs = RE_CONFLICT_DETECTED.findall(output)
            conflicting_apps = {n.strip() for m in conflict_msgs for n in m.split(' and ')}
            conflict_msg = ' {} '.format(self.i18n['and']).join([bold(c) for c in conflicting_apps])
            if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'],
                                                        body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)):
                context.watcher.print(self.i18n['action.cancelled'])
                return False
            else:  # uninstall conflicts
                self._update_progress(context, 75)
                names_to_install = context.get_package_names()
                to_uninstall = {conflict for conflict in conflicting_apps if conflict not in names_to_install}

                if to_uninstall:
                    self.logger.info("Preparing to uninstall conflicting packages: {}".format(to_uninstall))
                    context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'])

                    if context.removed is None:
                        context.removed = {}

                    context.disable_progress_if_changing()

                    if not self._uninstall(names=to_uninstall, context=context, remove_unneeded=False,
                                           disk_loader=context.disk_loader,
                                           replacers=names_to_install):

                        context.watcher.show_message(title=self.i18n['error'],
                                                     body=self.i18n['arch.uninstalling.conflict.fail'].format(', '.join((bold(p) for p in to_uninstall))),
                                                     type_=MessageType.ERROR)
                        return False
                    else:
                        context.restabilish_progress()

        else:
            self.logger.info("No conflict detected for '{}'".format(context.name))

        self._update_progress(context, 80)

        to_install = []

        if context.missing_deps:
            to_install.extend((d[0] for d in context.missing_deps if d[1] != 'aur'))

        to_install.extend(pkgpaths)

        downloaded = 0
        if self._multithreaded_download_enabled(context.config):
            to_download = [p for p in to_install if not p.startswith('/')]

            if to_download:
                try:
                    pkg_sizes = pacman.map_download_sizes(to_download)
                    downloaded = self._download_packages(to_download, context.handler, context.root_password, pkg_sizes, multithreaded=True)
                except ArchDownloadException:
                    return False

        if not context.dependency:
            status_handler = TransactionStatusHandler(context.watcher, self.i18n, to_install, self.logger,
                                                      percentage=len(to_install) > 1,
                                                      downloading=downloaded) if not context.dependency else None
            status_handler.start()
        else:
            status_handler = None

        installed_with_same_name = self.read_installed(disk_loader=context.disk_loader, internet_available=True, names=context.get_package_names()).installed
        context.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(context.name)))  #

        installed = self._handle_install_call(context=context, to_install=to_install, status_handler=status_handler)

        if status_handler:
            status_handler.stop_working()
            status_handler.join()

        self._update_progress(context, 95)

        if installed:
            context.installed.update(context.get_package_names())
            context.installed.update((p for p in to_install if not p.startswith('/')))

            if installed_with_same_name:
                for p in installed_with_same_name:
                    context.removed[p.name] = p

            context.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(context.name)))

            if not context.maintainer:
                if context.pkg and context.pkg.maintainer:
                    pkg_maintainer = context.pkg.maintainer
                elif context.repository == 'aur':
                    aur_infos = self.aur_client.get_info((context.name,))
                    pkg_maintainer = aur_infos[0].get('Maintainer') if aur_infos else None
                else:
                    pkg_maintainer = context.repository
            else:
                pkg_maintainer = context.maintainer

            cache_map = {context.name: ArchPackage(name=context.name,
                                                   repository=context.repository,
                                                   maintainer=pkg_maintainer,
                                                   last_modified=context.last_modified,
                                                   commit=context.commit,
                                                   categories=self.categories.get(context.name))}
            if context.missing_deps:
                aur_deps = {dep[0] for dep in context.missing_deps if dep[1] == 'aur'}

                if aur_deps:
                    aur_data = self.aur_client.get_info(aur_deps)

                    if aur_data:
                        aur_data = {info['Name']: info for info in aur_data}
                    else:
                        aur_data = {n: {} for n in aur_deps}
                else:
                    aur_data = None

                for dep in context.missing_deps:
                    cache_map[dep[0]] = ArchPackage(name=dep[0],
                                                    repository=dep[1],
                                                    maintainer=dep[1] if dep[1] != 'aur' else (aur_data[dep[0]].get('Maintainer') if aur_data else None),
                                                    categories=self.categories.get(context.name))

            disk.write_several(pkgs=cache_map, maintainer=None, overwrite=True)

            context.watcher.change_substatus('')
            self._update_progress(context, 100)

        return installed

    def _call_pacman_install(self, context: TransactionContext, to_install: List[str], overwrite_files: bool, status_handler: Optional[object] = None) -> Tuple[bool, str]:
        return context.handler.handle_simple(pacman.install_as_process(pkgpaths=to_install,
                                                                       root_password=context.root_password,
                                                                       file=context.has_install_files(),
                                                                       pkgdir=context.project_dir,
                                                                       overwrite_conflicting_files=overwrite_files,
                                                                       as_deps=context.dependency),
                                             output_handler=status_handler.handle if status_handler else None)

    def _handle_install_call(self, context: TransactionContext, to_install: List[str], status_handler) -> bool:
        installed, output = self._call_pacman_install(context=context, to_install=to_install,
                                                      overwrite_files=False, status_handler=status_handler)

        if not installed and 'conflicting files' in output:
            if not context.handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(),
                                                                body=self.i18n['arch.install.error.conflicting_files'].format(bold(context.name)) + ':',
                                                                deny_label=self.i18n['arch.install.error.conflicting_files.proceed'],
                                                                confirmation_label=self.i18n['arch.install.error.conflicting_files.stop'],
                                                                components=self._map_conflicting_file(output)):
                installed, output = self._call_pacman_install(context=context, to_install=to_install,
                                                              overwrite_files=True, status_handler=status_handler)

        return installed

    def _update_progress(self, context: TransactionContext, val: int):
        if context.change_progress:
            context.watcher.change_progress(val)

    def _import_pgp_keys(self, pkgname: str, root_password: Optional[str], handler: ProcessHandler):
        srcinfo = self.aur_client.get_src_info(pkgname)

        if srcinfo.get('validpgpkeys'):
            handler.watcher.print(self.i18n['arch.aur.install.verifying_pgp'])
            keys_to_download = [key for key in srcinfo['validpgpkeys'] if not pacman.verify_pgp_key(key)]

            if keys_to_download:
                keys_str = ''.join(
                    ['<br/><span style="font-weight:bold">  - {}</span>'.format(k) for k in keys_to_download])
                msg_body = '{}:<br/>{}<br/><br/>{}'.format(self.i18n['arch.aur.install.pgp.body'].format(bold(pkgname)),
                                                           keys_str, self.i18n['ask.continue'])

                if handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.pgp.title'], body=msg_body):
                    for key in keys_to_download:
                        handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.substatus'].format(bold(key)))
                        if not handler.handle(pacman.receive_key(key, root_password)):
                            handler.watcher.show_message(title=self.i18n['error'],
                                                         body=self.i18n['arch.aur.install.pgp.receive_fail'].format(
                                                             bold(key)),
                                                         type_=MessageType.ERROR)
                            return False

                        if not handler.handle(pacman.sign_key(key, root_password)):
                            handler.watcher.show_message(title=self.i18n['error'],
                                                         body=self.i18n['arch.aur.install.pgp.sign_fail'].format(
                                                             bold(key)),
                                                         type_=MessageType.ERROR)
                            return False

                        handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.success'])
                else:
                    handler.watcher.print(self.i18n['action.cancelled'])
                    return False

    def _install_from_aur(self, context: TransactionContext) -> bool:
        if not context.dependency and not self.add_package_builder_user(context.handler):
            return False

        self._optimize_makepkg(context.config, context.watcher)

        context.build_dir = f'{get_build_dir(context.config, self.pkgbuilder_user)}/build_{int(time.time())}'

        try:
            if not os.path.exists(context.build_dir):
                build_dir, build_dir_error = sshell.mkdir(dir_path=context.build_dir, custom_user=self.pkgbuilder_user)
                self._update_progress(context, 10)

                if not build_dir:
                    context.watcher.print(build_dir_error)
                else:
                    base_name = context.get_base_name()
                    context.watcher.change_substatus(self.i18n['arch.clone'].format(bold(base_name)))
                    clone_dir = f'{context.build_dir}/{base_name}'
                    cloned = context.handler.handle_simple(git.clone(url=URL_GIT.format(base_name), target_dir=clone_dir,
                                                                     depth=1, custom_user=self.pkgbuilder_user))

                    if cloned:
                        self._update_progress(context, 40)
                        context.project_dir = clone_dir
                        return self._build(context)
        finally:
            if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']:
                context.handler.handle(SystemProcess(new_subprocess(['rm', '-rf', context.build_dir])))

        return False

    def _sync_databases(self, arch_config: dict, aur_supported: bool, root_password: Optional[str], handler: ProcessHandler, change_substatus: bool = True):
        if bool(arch_config['sync_databases']) and database.should_sync(arch_config, aur_supported, handler, self.logger):
            if change_substatus:
                handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus'])

            synced, output = handler.handle_simple(pacman.sync_databases(root_password=root_password, force=True))
            if synced:
                database.register_sync(self.logger)
            else:
                self.logger.warning("It was not possible to synchronized the package databases")
                handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus.error'])

    def _optimize_makepkg(self, arch_config: dict, watcher: Optional[ProcessWatcher]):
        if arch_config['optimize'] and not os.path.exists(CUSTOM_MAKEPKG_FILE):
            watcher.change_substatus(self.i18n['arch.makepkg.optimizing'])
            ArchCompilationOptimizer(i18n=self.i18n, logger=self.context.logger, taskman=TaskManager()).optimize()

    def install(self, pkg: ArchPackage, root_password: Optional[str], disk_loader: Optional[DiskCacheLoader], watcher: ProcessWatcher, context: TransactionContext = None) -> TransactionResult:
        if not self.check_action_allowed(pkg, watcher):
            return TransactionResult.fail()

        self.aur_client.clean_caches()

        handler = ProcessHandler(watcher) if not context else context.handler

        if self._is_database_locked(handler, root_password):
            return TransactionResult(success=False, installed=[], removed=[])

        if context:
            install_context = context
        else:
            install_context = TransactionContext.gen_context_from(pkg=pkg, handler=handler, arch_config=self.configman.get_config(),
                                                                  root_password=root_password)
            install_context.skip_opt_deps = False
            install_context.disk_loader = disk_loader
            install_context.update_aur_index = pkg.repository == 'aur'

        self._sync_databases(arch_config=install_context.config, aur_supported=install_context.aur_supported,
                             root_password=root_password, handler=handler)

        if pkg.repository == 'aur':
            pkg_installed = self._install_from_aur(install_context)
        else:
            pkg_installed = self._install_from_repository(install_context)

        if pkg_installed:
            pkg.name = install_context.name  # changes the package name in case the PKGBUILD was edited

            if os.path.exists(pkg.get_disk_data_path()):
                with open(pkg.get_disk_data_path()) as f:
                    data = f.read()
                    if data:
                        data = json.loads(data)
                        pkg.fill_cached_data(data)

            if install_context.new_pkg and install_context.config['edit_aur_pkgbuild'] is not False and pkg.repository == 'aur':
                if install_context.pkgbuild_edited:
                    pkg.pkgbuild_editable = self._add_as_editable_pkgbuild(pkg.name)
                else:
                    pkg.pkgbuild_editable = not self._remove_from_editable_pkgbuilds(pkg.name)

        installed = []

        if pkg_installed and disk_loader and install_context.installed:
            installed.append(pkg)

            installed_to_load = []

            if len(install_context.installed) > 1:
                installed_to_load.extend({i for i in install_context.installed if i != pkg.name})

            if installed_to_load:
                installed_loaded = self.read_installed(disk_loader=disk_loader,
                                                       names=installed_to_load,
                                                       internet_available=True).installed

                if installed_loaded:
                    installed.extend(installed_loaded)

                    if len(installed_loaded) + 1 != len(install_context.installed):
                        missing = ','.join({p for p in installed_loaded if p.name not in install_context.installed})
                        self.logger.warning("Could not load all installed packages. Missing: {}".format(missing))

        removed = [*install_context.removed.values()] if install_context.removed else []
        return TransactionResult(success=pkg_installed, installed=installed, removed=removed)

    def _install_from_repository(self, context: TransactionContext) -> bool:
        if not self._handle_missing_deps(context):
            return False

        res = self._install(context)

        if res and not context.skip_opt_deps:
            self._update_progress(context, 100)
            return self._install_optdeps(context)

        return res

    def is_enabled(self) -> bool:
        return self.enabled

    def set_enabled(self, enabled: bool):
        self.enabled = enabled

    def can_work(self) -> Tuple[bool, Optional[str]]:
        if not self.arch_distro:
            return False, self.i18n['arch.can_work.not_arch_distro']

        if not pacman.is_available():
            return False, self.i18n['missing_dep'].format(dep=bold('pacman'))

        return True, None

    def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool):
        pass

    def requires_root(self, action: SoftwareAction, pkg: ArchPackage) -> bool:
        if action == SoftwareAction.PREPARE:
            arch_config = self.configman.get_config()
            aur_supported = (pkg and pkg.repository == 'aur') or aur.is_supported(arch_config)

            if RefreshMirrors.should_synchronize(arch_config, aur_supported, self.logger):
                return True

            return SyncDatabases.should_sync(mirrors_refreshed=False, arch_config=arch_config,
                                             aur_supported=aur_supported, logger=self.logger)

        return action != SoftwareAction.SEARCH

    def _start_category_task(self, taskman: TaskManager, create_config: CreateConfigFile, downloader: CategoriesDownloader):
        taskman.update_progress('arch_aur_cats', 0, self.i18n['task.waiting_task'].format(bold(create_config.task_name)))
        create_config.join()
        arch_config = create_config.config

        downloader.expiration = arch_config['categories_exp'] if isinstance(arch_config['categories_exp'], int) else None
        taskman.update_progress('arch_aur_cats', 50, None)

    def _finish_category_task(self, taskman: TaskManager):
        taskman.update_progress('arch_aur_cats', 100, None)
        taskman.finish_task('arch_aur_cats')

    def prepare(self, task_manager: TaskManager, root_password: Optional[str], internet_available: bool):
        create_config = CreateConfigFile(taskman=task_manager, configman=self.configman, i18n=self.i18n,
                                         task_icon_path=get_icon_path(), logger=self.logger)
        create_config.start()

        if internet_available:
            self.index_aur = AURIndexUpdater(context=self.context, taskman=task_manager, create_config=create_config)  # must always execute to properly determine the installed packages (even that AUR is disabled)
            self.index_aur.start()

            if not self.suggestions_downloader.is_custom_local_file_mapped():
                self.suggestions_downloader.create_config = create_config
                self.suggestions_downloader.register_task(task_manager)
                self.suggestions_downloader.start()

            refresh_mirrors = RefreshMirrors(taskman=task_manager, i18n=self.i18n, root_password=root_password,
                                             logger=self.logger, create_config=create_config)
            refresh_mirrors.start()

            SyncDatabases(taskman=task_manager, root_password=root_password, i18n=self.i18n,
                          logger=self.logger, refresh_mirrors=refresh_mirrors, create_config=create_config).start()

        ArchCompilationOptimizer(i18n=self.i18n, logger=self.context.logger,
                                 taskman=task_manager, create_config=create_config).start()

        self.disk_cache_updater = ArchDiskCacheUpdater(taskman=task_manager,
                                                       i18n=self.i18n,
                                                       logger=self.context.logger,
                                                       controller=self,
                                                       internet_available=internet_available,
                                                       aur_indexer=self.index_aur,
                                                       create_config=create_config)
        self.disk_cache_updater.start()

        task_manager.register_task('arch_aur_cats', self.i18n['task.download_categories'], get_icon_path())
        cat_download = CategoriesDownloader(id_='Arch', http_client=self.context.http_client,
                                            logger=self.context.logger,
                                            manager=self, url_categories_file=URL_CATEGORIES_FILE,
                                            categories_path=CATEGORIES_FILE_PATH,
                                            internet_connection=internet_available,
                                            internet_checker=self.context.internet_checker,
                                            after=lambda: self._finish_category_task(task_manager))
        cat_download.before = lambda: self._start_category_task(taskman=task_manager, create_config=create_config,
                                                                downloader=cat_download)
        cat_download.start()

    def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
        installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed

        aur_type, repo_type = self.i18n['gem.arch.type.aur.label'], self.i18n['gem.arch.type.arch_repo.label']

        return [PackageUpdate(p.name, p.latest_version, aur_type if p.repository == 'aur' else repo_type, p.name) for p in installed if p.update and not p.is_update_ignored()]

    def list_warnings(self, internet_available: bool) -> Optional[List[str]]:
        if not git.is_installed():
            return [self.i18n['arch.warning.aur_missing_dep'].format(bold('git'))]

    def is_default_enabled(self) -> bool:
        return True

    def launch(self, pkg: ArchPackage):
        if pkg.command:
            final_cmd = pkg.command.replace('%U', '')
            subprocess.Popen(final_cmd, shell=True)

    def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool,
                           max_width: Optional[int] = None, capitalize_label: bool = True,
                           label_params: Optional[list] = None, tooltip_params: Optional[list] = None) \
            -> SingleSelectComponent:

        opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True),
                InputOption(label=self.i18n['no'].capitalize(), value=False)]

        lb = self.i18n[label_key]

        if label_params:
            lb = lb.format(*label_params)

        tip = self.i18n[tooltip_key]

        if tooltip_params:
            tip = tip.format(*tooltip_params)

        return SingleSelectComponent(label=lb,
                                     options=opts,
                                     default_option=[o for o in opts if o.value == value][0],
                                     max_per_line=len(opts),
                                     type_=SelectViewType.RADIO,
                                     tooltip=tip,
                                     max_width=max_width,
                                     id_=id_,
                                     capitalize_label=capitalize_label)

    def _get_general_settings(self, arch_config: dict) -> SettingsView:
        db_sync_start = self._gen_bool_selector(id_='sync_dbs_start',
                                                label_key='arch.config.sync_dbs',
                                                tooltip_key='arch.config.sync_dbs_start.tip',
                                                value=bool(arch_config['sync_databases_startup']))

        db_sync_start.label += f" ({self.i18n['initialization'].capitalize()})"

        fields = [
            self._gen_bool_selector(id_='repos',
                                    label_key='arch.config.repos',
                                    tooltip_key='arch.config.repos.tip',
                                    value=bool(arch_config['repositories'])),
            self._gen_bool_selector(id_='autoprovs',
                                    label_key='arch.config.automatch_providers',
                                    tooltip_key='arch.config.automatch_providers.tip',
                                    value=bool(arch_config['automatch_providers'])),
            self._gen_bool_selector(id_='prefer_repo_provider',
                                    label_key='arch.config.prefer_repository_provider',
                                    tooltip_key='arch.config.prefer_repository_provider.tip',
                                    value=bool(arch_config['prefer_repository_provider']),
                                    tooltip_params=['AUR']),
            self._gen_bool_selector(id_='check_dependency_breakage',
                                    label_key='arch.config.check_dependency_breakage',
                                    tooltip_key='arch.config.check_dependency_breakage.tip',
                                    value=bool(arch_config['check_dependency_breakage'])),
            self._gen_bool_selector(id_='mthread_download',
                                    label_key='arch.config.pacman_mthread_download',
                                    tooltip_key='arch.config.pacman_mthread_download.tip',
                                    value=arch_config['repositories_mthread_download'],
                                    capitalize_label=True),
            self._gen_bool_selector(id_='sync_dbs',
                                    label_key='arch.config.sync_dbs',
                                    tooltip_key='arch.config.sync_dbs.tip',
                                    value=bool(arch_config['sync_databases'])),
            db_sync_start,
            self._gen_bool_selector(id_='clean_cached',
                                    label_key='arch.config.clean_cache',
                                    tooltip_key='arch.config.clean_cache.tip',
                                    value=bool(arch_config['clean_cached'])),
            self._gen_bool_selector(id_='suggest_unneeded_uninstall',
                                    label_key='arch.config.suggest_unneeded_uninstall',
                                    tooltip_params=['"{}"'.format(self.i18n['arch.config.suggest_optdep_uninstall'])],
                                    tooltip_key='arch.config.suggest_unneeded_uninstall.tip',
                                    value=bool(arch_config['suggest_unneeded_uninstall'])),
            self._gen_bool_selector(id_='suggest_optdep_uninstall',
                                    label_key='arch.config.suggest_optdep_uninstall',
                                    tooltip_key='arch.config.suggest_optdep_uninstall.tip',
                                    value=bool(arch_config['suggest_optdep_uninstall'])),
            self._gen_bool_selector(id_='suggest_optdep_select',
                                    label_key='arch.config.suggest_optdep_select',
                                    tooltip_key='arch.config.suggest_optdep_select.tip',
                                    value=bool(arch_config['suggest_optdep_select'])),
            self._gen_bool_selector(id_='ref_mirs',
                                    label_key='arch.config.refresh_mirrors',
                                    tooltip_key='arch.config.refresh_mirrors.tip',
                                    value=bool(arch_config['refresh_mirrors_startup'])),
            TextInputComponent(id_='mirrors_sort_limit',
                               label=self.i18n['arch.config.mirrors_sort_limit'],
                               tooltip=self.i18n['arch.config.mirrors_sort_limit.tip'],
                               only_int=True,
                               value=arch_config['mirrors_sort_limit'] if isinstance(arch_config['mirrors_sort_limit'], int) else ''),
            TextInputComponent(id_='arch_cats_exp',
                               label=self.i18n['arch.config.categories_exp'],
                               tooltip=self.i18n['arch.config.categories_exp.tip'],
                               only_int=True,
                               capitalize_label=False,
                               value=arch_config['categories_exp'] if isinstance(arch_config['categories_exp'], int) else ''),
            TextInputComponent(id_='arch_sugs_exp',
                               label=self.i18n['arch.config.suggestions_exp'],
                               tooltip=self.i18n['arch.config.suggestions_exp.tip'],
                               only_int=True,
                               capitalize_label=False,
                               value=arch_config['suggestions_exp'] if isinstance(arch_config['suggestions_exp'], int) else '')
        ]

        return SettingsView(self, PanelComponent([FormComponent(fields, spaces=False)], id_="repo"), icon_path=get_repo_icon_path())

    def _get_aur_settings(self, arch_config: dict) -> SettingsView:
        fields = [
            self._gen_bool_selector(id_='aur',
                                    label_key='arch.config.aur',
                                    tooltip_key='arch.config.aur.tip',
                                    value=arch_config['aur'],
                                    capitalize_label=False),
            self._gen_bool_selector(id_='opts',
                                    label_key='arch.config.optimize',
                                    tooltip_key='arch.config.optimize.tip',
                                    value=bool(arch_config['optimize']),
                                    capitalize_label=False),
            self._gen_bool_selector(id_='rebuild_detector',
                                    label_key='arch.config.aur_rebuild_detector',
                                    tooltip_key='arch.config.aur_rebuild_detector.tip',
                                    value=bool(arch_config['aur_rebuild_detector']),
                                    tooltip_params=["'rebuild-detector'"],
                                    capitalize_label=False),
            self._gen_bool_selector(id_='rebuild_detector_no_bin',
                                    label_key='arch.config.aur_rebuild_detector_no_bin',
                                    label_params=['rebuild-detector'],
                                    tooltip_key='arch.config.aur_rebuild_detector_no_bin.tip',
                                    tooltip_params=['rebuild-detector',
                                                    self.i18n['arch.config.aur_rebuild_detector'].format('')],
                                    value=bool(arch_config['aur_rebuild_detector_no_bin']),
                                    capitalize_label=False),
            new_select(id_='aur_build_only_chosen',
                       label=self.i18n['arch.config.aur_build_only_chosen'],
                       tip=self.i18n['arch.config.aur_build_only_chosen.tip'],
                       opts=[(self.i18n['yes'].capitalize(), True, None),
                             (self.i18n['no'].capitalize(), False, None),
                             (self.i18n['ask'].capitalize(), None, None),
                             ],
                       value=arch_config['aur_build_only_chosen'],
                       type_=SelectViewType.RADIO,
                       capitalize_label=False),
            new_select(label=self.i18n['arch.config.edit_aur_pkgbuild'],
                       tip=self.i18n['arch.config.edit_aur_pkgbuild.tip'],
                       id_='edit_aur_pkgbuild',
                       opts=[(self.i18n['yes'].capitalize(), True, None),
                             (self.i18n['no'].capitalize(), False, None),
                             (self.i18n['ask'].capitalize(), None, None),
                             ],
                       value=arch_config['edit_aur_pkgbuild'],
                       type_=SelectViewType.RADIO,
                       capitalize_label=False),
            self._gen_bool_selector(id_='aur_remove_build_dir',
                                    label_key='arch.config.aur_remove_build_dir',
                                    tooltip_key='arch.config.aur_remove_build_dir.tip',
                                    value=bool(arch_config['aur_remove_build_dir']),
                                    capitalize_label=False),
            FileChooserComponent(id_='aur_build_dir',
                                 label=self.i18n['arch.config.aur_build_dir'],
                                 tooltip=self.i18n['arch.config.aur_build_dir.tip'].format(
                                     get_build_dir(arch_config, self.pkgbuilder_user)),
                                 file_path=arch_config['aur_build_dir'],
                                 capitalize_label=False,
                                 directory=True),
            TextInputComponent(id_='aur_idx_exp',
                               label=self.i18n['arch.config.aur_idx_exp'],
                               tooltip=self.i18n['arch.config.aur_idx_exp.tip'],
                               only_int=True,
                               capitalize_label=False,
                               value=arch_config['aur_idx_exp'] if isinstance(arch_config['aur_idx_exp'], int) else '')
        ]

        return SettingsView(self, PanelComponent([FormComponent(fields, spaces=False)], id_='aur'),
                            label='AUR',
                            icon_path=get_icon_path())

    def get_settings(self) -> Optional[Generator[SettingsView, None, None]]:
        arch_config = self.configman.get_config()
        yield self._get_general_settings(arch_config)
        yield self._get_aur_settings(arch_config)

    @staticmethod
    def fill_general_settings(arch_config: dict, form: FormComponent):
        arch_config['repositories'] = form.get_component('repos', SingleSelectComponent).get_selected()
        arch_config['sync_databases'] = form.get_component('sync_dbs', SingleSelectComponent).get_selected()
        arch_config['clean_cached'] = form.get_component('clean_cached', SingleSelectComponent).get_selected()
        arch_config['refresh_mirrors_startup'] = form.get_component('ref_mirs', SingleSelectComponent).get_selected()
        arch_config['mirrors_sort_limit'] = form.get_component('mirrors_sort_limit', TextInputComponent).get_int_value()
        arch_config['automatch_providers'] = form.get_component('autoprovs', SingleSelectComponent).get_selected()
        
        sync_dbs_startup = form.get_component('sync_dbs_start', SingleSelectComponent).get_selected()
        arch_config['sync_databases_startup'] = sync_dbs_startup

        mthread_download = form.get_component('mthread_download', SingleSelectComponent).get_selected()
        arch_config['repositories_mthread_download'] = mthread_download

        prefer_repo_provider = form.get_component('prefer_repo_provider', SingleSelectComponent).get_selected()
        arch_config['prefer_repository_provider'] = prefer_repo_provider

        check_dep_break = form.get_component('check_dependency_breakage', SingleSelectComponent).get_selected()
        arch_config['check_dependency_breakage'] = check_dep_break

        sug_opt_dep_uni = form.get_component('suggest_optdep_uninstall', SingleSelectComponent).get_selected()
        arch_config['suggest_optdep_uninstall'] = sug_opt_dep_uni

        sug_unneeded_uni = form.get_component('suggest_unneeded_uninstall', SingleSelectComponent).get_selected()
        arch_config['suggest_unneeded_uninstall'] = sug_unneeded_uni
        
        sug_optdep_download = form.get_component('suggest_optdep_select', SingleSelectComponent).get_selected()
        arch_config['suggest_optdep_select'] = sug_optdep_download

        arch_config['categories_exp'] = form.get_component('arch_cats_exp', TextInputComponent).get_int_value()
        arch_config['suggestions_exp'] = form.get_component('arch_sugs_exp', TextInputComponent).get_int_value()

    def _fill_aur_settings(self, arch_config: dict, form: FormComponent):
        arch_config['optimize'] = form.get_component('opts', SingleSelectComponent).get_selected()

        rebuild_detect = form.get_component('rebuild_detector', SingleSelectComponent).get_selected()
        arch_config['aur_rebuild_detector'] = rebuild_detect

        rebuild_no_bin = form.get_component('rebuild_detector_no_bin', SingleSelectComponent).get_selected()
        arch_config['aur_rebuild_detector_no_bin'] = rebuild_no_bin

        arch_config['edit_aur_pkgbuild'] = form.get_component('edit_aur_pkgbuild', SingleSelectComponent).get_selected()

        remove_build_dir = form.get_component('aur_remove_build_dir', SingleSelectComponent).get_selected()
        arch_config['aur_remove_build_dir'] = remove_build_dir

        build_chosen = form.get_component('aur_build_only_chosen', SingleSelectComponent).get_selected()
        arch_config['aur_build_only_chosen'] = build_chosen

        arch_config['aur_build_dir'] = form.get_component('aur_build_dir', FileChooserComponent).file_path
        arch_config['aur_idx_exp'] = form.get_component('aur_idx_exp', TextInputComponent).get_int_value()

        if not arch_config['aur_build_dir']:
            arch_config['aur_build_dir'] = None

        aur_enabled_select = form.get_component('aur', SingleSelectComponent)
        arch_config['aur'] = aur_enabled_select.get_selected()

        if aur_enabled_select.changed() and arch_config['aur']:
            self.index_aur = AURIndexUpdater(context=self.context, taskman=TaskManager(), arch_config=arch_config)
            self.index_aur.start()

    def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]:
        arch_config = self.configman.get_config()
        form = component.get_component_by_idx(0, FormComponent)

        if component.id == 'repo':
            self.fill_general_settings(arch_config, form)
        elif component.id == 'aur':
            self._fill_aur_settings(arch_config, form)

        try:
            self.configman.save_config(arch_config)
            return True, None
        except Exception:
            return False, [traceback.format_exc()]

    def get_upgrade_requirements(self, pkgs: List[ArchPackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements:
        self.aur_client.clean_caches()
        arch_config = self.configman.get_config()

        aur_supported = aur.is_supported(arch_config)

        self._sync_databases(arch_config=arch_config, aur_supported=aur_supported,
                             root_password=root_password, handler=ProcessHandler(watcher), change_substatus=False)

        summarizer = UpdatesSummarizer(aur_client=self.aur_client,
                                       aur_supported=aur_supported,
                                       i18n=self.i18n,
                                       logger=self.logger,
                                       deps_analyser=self.deps_analyser,
                                       watcher=watcher)
        try:
            return summarizer.summarize(pkgs, root_password, arch_config)
        except PackageNotFoundException:
            pass  # when nothing is returned, the upgrade is called off by the UI

    def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
        if self._custom_actions is None:
            self._custom_actions = {
                'sys_up': CustomSoftwareAction(i18n_label_key='arch.custom_action.upgrade_system',
                                               i18n_status_key='arch.custom_action.upgrade_system.status',
                                               i18n_description_key='arch.custom_action.upgrade_system.desc',
                                               manager_method='upgrade_system',
                                               icon_path=get_icon_path(),
                                               requires_root=True,
                                               backup=True,
                                               manager=self),
                'ref_dbs': CustomSoftwareAction(i18n_label_key='arch.custom_action.refresh_dbs',
                                                i18n_status_key='arch.sync_databases.substatus',
                                                i18n_description_key='arch.custom_action.refresh_dbs.desc',
                                                manager_method='sync_databases',
                                                icon_path=get_icon_path(),
                                                requires_root=True,
                                                manager=self),
                'ref_mirrors': CustomSoftwareAction(i18n_label_key='arch.custom_action.refresh_mirrors',
                                                    i18n_status_key='arch.task.mirrors',
                                                    i18n_description_key='arch.custom_action.refresh_mirrors.desc',
                                                    manager_method='refresh_mirrors',
                                                    icon_path=get_icon_path(),
                                                    requires_root=True,
                                                    manager=self,
                                                    requires_confirmation=False),
                'clean_cache': CustomSoftwareAction(i18n_label_key='arch.custom_action.clean_cache',
                                                    i18n_status_key='arch.custom_action.clean_cache.status',
                                                    i18n_description_key='arch.custom_action.clean_cache.desc',
                                                    manager_method='clean_cache',
                                                    icon_path=get_icon_path(),
                                                    requires_root=True,
                                                    refresh=False,
                                                    manager=self,
                                                    requires_confirmation=False)
            }

        arch_config = self.configman.get_config()

        if pacman.is_mirrors_available():
            yield self._custom_actions['ref_mirrors']

        yield self._custom_actions['ref_dbs']
        yield self._custom_actions['clean_cache']

        if bool(arch_config['repositories']):
            yield self._custom_actions['sys_up']

    def fill_sizes(self, pkgs: List[ArchPackage]):
        installed, new, all_names, installed_names = [], [], [], []

        for p in pkgs:
            if p.repository != 'aur':
                all_names.append(p.name)
                if p.installed:
                    installed.append(p)
                    installed_names.append(p.name)
                else:
                    new.append(p)

        new_sizes = pacman.map_update_sizes(all_names)

        if new_sizes:
            if new:
                for p in new:
                    p.size = new_sizes.get(p.name)

            if installed:
                installed_sizes = pacman.get_installed_size(installed_names)

                for p in installed:
                    p.size = installed_sizes.get(p.name)
                    new_size = new_sizes.get(p.name)

                    if p.size is None:
                        p.size = new_size
                    elif new_size is not None:
                        p.size = new_size - p.size

    def upgrade_system(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        # repo_map = pacman.map_repositories()
        net_available = self.context.internet_checker.is_available()
        installed = self.read_installed(limit=-1, only_apps=False, pkg_types=None, internet_available=net_available, disk_loader=None).installed

        if not installed:
            watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'],
                                 body=self.i18n['arch.custom_action.upgrade_system.no_updates'],
                                 type_=MessageType.INFO)
            return False

        to_update = [p for p in installed if p.repository != 'aur' and p.update]

        if not to_update:
            watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'],
                                 body=self.i18n['arch.custom_action.upgrade_system.no_updates'],
                                 type_=MessageType.INFO)
            return False

        if not self._confirm_upgrade_risks(watcher=watcher, internet_available=net_available):
            watcher.print('Operation cancelled')
            return False

        # icon_path = get_repo_icon_path()

        # pkg_opts, size = [], 0

        # self.fill_sizes(to_update)
        #
        # for pkg in to_update:
        #     lb = '{} ( {} > {} ) - {}: {}'.format(pkg.name,
        #                                           pkg.version,
        #                                           pkg.latest_version,
        #                                           self.i18n['size'].capitalize(),
        #                                           '?' if pkg.size is None else get_human_size_str(pkg.size))
        #     pkg_opts.append(InputOption(label=lb,
        #                                 value=pkg.name,
        #                                 read_only=True,
        #                                 icon_path=icon_path))
        #
        #     if pkg.size is not None:
        #         size += pkg.size
        #
        # pkg_opts.sort(key=lambda o: o.label)

        # select = MultipleSelectComponent(label='',
        #                                  options=pkg_opts,
        #                                  default_options=set(pkg_opts))

        # if watcher.request_confirmation(title=self.i18n['arch.custom_action.upgrade_system'],
        #                                 body="{}. {}: {}".format(self.i18n['arch.custom_action.upgrade_system.pkgs'],
        #                                                          self.i18n['size'].capitalize(),
        #                                                          get_human_size_str(size)),
        #                                 confirmation_label=self.i18n['proceed'].capitalize(),
        #                                 deny_label=self.i18n['cancel'].capitalize(),
        #                                 components=[select]):

            # watcher.change_substatus(self.i18n['arch.custom_action.upgrade_system.substatus'])
        handler = ProcessHandler(watcher)

        if self._is_database_locked(handler, root_password):
            return False

        success, output = handler.handle_simple(pacman.upgrade_system(root_password))

        if not success or 'error:' in output:
            watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'],
                                 body="An error occurred during the upgrade process. Check out the {}".format(
                                     bold('Details')),
                                 type_=MessageType.ERROR)
            return False
        else:
            database.register_sync(self.logger)
            msg = '<p>{}</p><br/>{}</p><p>{}</p>'.format(self.i18n['action.update.success.reboot.line1'],
                                                         self.i18n['action.update.success.reboot.line2'],
                                                         self.i18n['action.update.success.reboot.line3'])
            watcher.request_reboot(msg)
            return True

    def _read_pending_pacnew_files(self, limit: int = 30) -> List[str]:
        output = run_cmd("find /etc -type f -name '*.pacnew' 2>/dev/null", print_error=False)

        if not output:
            return []

        files = sorted((line.strip() for line in output.splitlines() if line and line.strip()))
        return files[:limit]

    def _fetch_arch_news(self, limit: int = 5) -> List[Tuple[str, str]]:
        if not self.context.http_client:
            return []

        try:
            res = self.context.http_client.get(URL_ARCH_NEWS_FEED, session=True, single_call=True)
        except Exception:
            return []

        if not res or res.status_code != 200 or not res.text:
            return []

        news = []
        for item in RE_ARCH_NEWS_ITEM.findall(res.text):
            title_match = RE_ARCH_NEWS_TITLE.search(item)
            link_match = RE_ARCH_NEWS_LINK.search(item)

            if not title_match or not link_match:
                continue

            title = (title_match.group(1) or title_match.group(2) or '').strip()
            link_ = link_match.group(1).strip()

            if title and link_:
                news.append((title, link_))

            if len(news) >= limit:
                break

        return news

    def _confirm_upgrade_risks(self, watcher: ProcessWatcher, internet_available: bool) -> bool:
        pacnew_files = self._read_pending_pacnew_files()
        arch_news = self._fetch_arch_news() if internet_available else []

        if not pacnew_files and not arch_news:
            return True

        body_parts = [
            '<p><b>Review Arch system notices before continuing.</b></p>'
        ]

        if pacnew_files:
            max_items = 10
            listed = ''.join(f'<li><code>{p}</code></li>' for p in pacnew_files[:max_items])
            extra = len(pacnew_files) - max_items
            extra_msg = f'<p>... and {extra} more.</p>' if extra > 0 else ''
            body_parts.append(
                f"<p>Detected <b>{len(pacnew_files)}</b> pending <code>.pacnew</code> file(s). "
                f"These usually require manual merge/review:</p><ul>{listed}</ul>{extra_msg}"
            )

        if arch_news:
            listed = ''.join(f"<li><a href='{link_}'>{title}</a></li>" for title, link_ in arch_news)
            body_parts.append(
                f"<p>Recent Arch Linux news (check for manual intervention notices):</p><ul>{listed}</ul>"
            )

        body_parts.append('<p>Proceed with the system upgrade anyway?</p>')

        return watcher.request_confirmation(
            title=self.i18n['arch.custom_action.upgrade_system'],
            body=''.join(body_parts),
            confirmation_label=self.i18n['proceed'].capitalize(),
            deny_label=self.i18n['cancel'].capitalize()
        )

    def clean_cache(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:

        cache_dir = pacman.get_cache_dir()

        if not cache_dir or not os.path.isdir(cache_dir):
            watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(),
                                 body=self.i18n['arch.custom_action.clean_cache.no_dir'].format(bold(cache_dir)).capitalize(),
                                 type_=MessageType.WARNING)
            return True

        text = '<p>{}.</p><p>{}.</p><p>{}.</p>'.format(self.i18n['arch.custom_action.clean_cache.msg1'],
                                                       self.i18n['arch.custom_action.clean_cache.msg2'],
                                                       self.i18n['arch.custom_action.clean_cache.msg3'])

        if watcher.request_confirmation(title=self.i18n['arch.custom_action.clean_cache'].capitalize(),
                                        body=text,
                                        confirmation_label=self.i18n['clean'].capitalize(),
                                        deny_label=self.i18n['cancel'].capitalize()):

            handler = ProcessHandler(watcher)
            rm = SimpleProcess(cmd=['rm', '-rf', cache_dir], root_password=root_password)
            success, _ = handler.handle_simple(rm)

            if success:
                watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(),
                                     body=self.i18n['arch.custom_action.clean_cache.success'],
                                     type_=MessageType.INFO)

                mkcache = SimpleProcess(cmd=['mkdir', '-p', cache_dir], root_password=root_password)
                handler.handle_simple(mkcache)
                return True
            else:
                watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(),
                                     body=self.i18n['arch.custom_action.clean_cache.fail'],
                                     type_=MessageType.ERROR)
                return False

        return True

    def _fill_ignored_updates(self, output: Set[str]) -> Set[str]:
        if os.path.exists(UPDATES_IGNORED_FILE):
            with open(UPDATES_IGNORED_FILE) as f:
                ignored_lines = f.readlines()

            for line in ignored_lines:
                if line:
                    line_clean = line.strip()

                    if line_clean:
                        output.add(line_clean)

        return output

    def _write_ignored(self, names: Set[str]):
        Path(ARCH_CONFIG_DIR).mkdir(parents=True, exist_ok=True)
        ignored_list = [*names]
        ignored_list.sort()

        with open(UPDATES_IGNORED_FILE, 'w+') as f:
            if ignored_list:
                for pkg in ignored_list:
                    f.write('{}\n'.format(pkg))
            else:
                f.write('')

    def ignore_update(self, pkg: ArchPackage):
        ignored = self._fill_ignored_updates(set())

        if pkg.name not in ignored:
            ignored.add(pkg.name)
            self._write_ignored(ignored)

        pkg.update_ignored = True

    def _revert_ignored_updates(self, pkgs: Iterable[str]):
        ignored = self._fill_ignored_updates(set())

        for p in pkgs:
            if p in ignored:
                ignored.remove(p)

        self._write_ignored(ignored)

    def revert_ignored_update(self, pkg: ArchPackage):
        self._revert_ignored_updates({pkg.name})
        pkg.update_ignored = False

    def _add_as_editable_pkgbuild(self, pkgname: str):
        try:
            Path('/'.join(EDITABLE_PKGBUILDS_FILE.split('/')[0:-1])).mkdir(parents=True, exist_ok=True)

            editable = self._read_editable_pkgbuilds()

            if pkgname not in editable:
                editable.add(pkgname)

            self._write_editable_pkgbuilds(editable)
            return True
        except Exception:
            traceback.print_exc()
            return False

    def _write_editable_pkgbuilds(self, editable: Set[str]):
        if editable:
            with open(EDITABLE_PKGBUILDS_FILE, 'w+') as f:
                for name in sorted([*editable]):
                    f.write('{}\n'.format(name))
        else:
            os.remove(EDITABLE_PKGBUILDS_FILE)

    def _remove_from_editable_pkgbuilds(self, pkgname: str):
        if os.path.exists(EDITABLE_PKGBUILDS_FILE):
            try:
                editable = self._read_editable_pkgbuilds()

                if pkgname in editable:
                    editable.remove(pkgname)

                self._write_editable_pkgbuilds(editable)
            except Exception:
                traceback.print_exc()
                return False

        return True

    def _read_editable_pkgbuilds(self) -> Set[str]:
        if os.path.exists(EDITABLE_PKGBUILDS_FILE):
            with open(EDITABLE_PKGBUILDS_FILE) as f:
                return {l.strip() for l in f.readlines() if l and l.strip()}

        return set()

    def enable_pkgbuild_edition(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher):
        if self._add_as_editable_pkgbuild(pkg.name):
            pkg.pkgbuild_editable = True

    def disable_pkgbuild_edition(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher):
        if self._remove_from_editable_pkgbuilds(pkg.name):
            pkg.pkgbuild_editable = False

    def _gen_custom_pkgbuild_if_required(self, context: TransactionContext) -> Optional[str]:
        build_only_chosen = context.config.get('aur_build_only_chosen')

        pkgs_to_build = aur.map_srcinfo(string=self._read_srcinfo(context), pkgname=None, fields={'pkgname'}).get('pkgname')

        if isinstance(pkgs_to_build, str):
            pkgs_to_build = {pkgs_to_build}
        else:
            pkgs_to_build = {*pkgs_to_build}

        if build_only_chosen is False:
            context.pkgs_to_build = pkgs_to_build
            return

        # checking if more than one package is mapped for this pkgbuild

        if not pkgs_to_build or not isinstance(pkgs_to_build, set) or len(pkgs_to_build) == 1 or context.name not in pkgs_to_build:
            context.pkgs_to_build = pkgs_to_build
            return

        if build_only_chosen is None:
            if not context.dependency:
                pkgnames = [InputOption(label=n, value=n, read_only=False) for n in pkgs_to_build if n != context.name]
                select = MultipleSelectComponent(label='',
                                                 options=pkgnames,
                                                 default_options={*pkgnames},
                                                 max_per_line=1)

                if not context.watcher.request_confirmation(title=self.i18n['warning'].capitalize(),
                                                            body=self.i18n['arch.aur.sync.several_names.popup.body'].format(bold(context.name)) + ':',
                                                            components=[select],
                                                            confirmation_label=self.i18n['arch.aur.sync.several_names.popup.bt_only_chosen'].format(context.name),
                                                            deny_label=self.i18n['arch.aur.sync.several_names.popup.bt_selected']):
                    context.pkgs_to_build = {context.name, *select.get_selected_values()}

        pkgbuild_path = f'{context.project_dir}/PKGBUILD'

        with open(pkgbuild_path) as f:
            current_pkgbuild = f.read()

        if context.pkgs_to_build:
            names = '({})'.format(' '.join(("'{}'".format(p) for p in context.pkgs_to_build)))
        else:
            names = context.name
            context.pkgs_to_build = {context.name}

        new_pkgbuild = RE_PKGBUILD_PKGNAME.sub(f"pkgname={names}", current_pkgbuild)
        custom_pkgbuild_path = f'{pkgbuild_path}_CUSTOM'

        if not write_as_user(content=new_pkgbuild,
                             file_path=custom_pkgbuild_path,
                             user=self.pkgbuilder_user):
            self.logger.error(f"Could not write edited PKGBUILD to '{custom_pkgbuild_path}'")
            return

        new_srcinfo = makepkg.gen_srcinfo(build_dir=context.project_dir,
                                          custom_pkgbuild_path=custom_pkgbuild_path,
                                          custom_user=self.pkgbuilder_user)

        srcinfo_path = f'{context.project_dir}/.SRCINFO'
        if not write_as_user(content=new_srcinfo, file_path=srcinfo_path, user=self.pkgbuilder_user):
            self.logger.warning(f"Could not write the updated .SRCINFO content to '{srcinfo_path}'")

        return custom_pkgbuild_path

    def _list_opt_deps_with_no_hard_requirements(self, source_pkgs: Set[str], installed_provided: Optional[Dict[str, Set[str]]] = None) -> Set[str]:
        optdeps = set()

        for deps in pacman.map_optional_deps(names=source_pkgs, remote=False).values():
            optdeps.update(deps.keys())

        res = set()
        if optdeps:
            all_provided = pacman.map_provided() if not installed_provided else installed_provided

            real_optdeps = set()
            for o in optdeps:
                dep_providers = all_provided.get(o)

                if dep_providers:
                    for p in dep_providers:
                        if p not in source_pkgs:
                            real_optdeps.add(p)

            if real_optdeps:
                for p in real_optdeps:
                    try:
                        reqs = pacman.list_hard_requirements(p, self.logger)

                        if reqs is not None and (not reqs or reqs.issubset(source_pkgs)):
                            res.add(p)
                    except PackageInHoldException:
                        self.logger.warning("There is a requirement in hold for opt dep '{}'".format(p))
                        continue
                    except PackageNotFoundException:
                        self.logger.warning(f"No hard requirements found for optional {p}. Reason: package not found")

        return res

    def reinstall(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher) -> bool:  # only available for AUR packages
        if not self.context.internet_checker.is_available():
            raise NoInternetException()

        self.aur_client.clean_caches()

        apidatas = self.aur_client.get_info((pkg.name,))

        if not apidatas:
            watcher.show_message(title=self.i18n['error'],
                                 body=self.i18n['arch.action.reinstall.error.no_apidata'],
                                 type_=MessageType.ERROR)
            return False

        self.aur_mapper.fill_last_modified(pkg, apidatas[0])
        context = TransactionContext.gen_context_from(pkg=pkg,
                                                      arch_config=self.configman.get_config(),
                                                      root_password=root_password,
                                                      handler=ProcessHandler(watcher),
                                                      aur_supported=True)
        context.skip_opt_deps = False
        context.update_aur_index = True

        return self.install(pkg=pkg,
                            root_password=root_password,
                            watcher=watcher,
                            context=context,
                            disk_loader=self.context.disk_loader_factory.new()).success

    def set_rebuild_check(self, pkg: ArchPackage, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
        if pkg.repository != 'aur':
            return False

        try:
            if pkg.allow_rebuild:
                rebuild_detector.add_as_ignored(pkg.name)
                pkg.allow_rebuild = False
            else:
                rebuild_detector.remove_from_ignored(pkg.name)
                pkg.allow_rebuild = True
        except Exception:
            self.logger.error("An unexpected exception happened")
            traceback.print_exc()
            return False

        pkg.update_state()
        return True

    def check_action_allowed(self, pkg: ArchPackage, watcher: Optional[ProcessWatcher]) -> bool:
        if self.context.root_user and pkg.repository == 'aur':
            if not shutil.which('useradd'):
                if watcher:
                    watcher.show_message(title=self.i18n['error'].capitalize(),
                                         type_=MessageType.ERROR,
                                         body=self.i18n['arch.aur.error.missing_root_dep'].format(dep=bold('useradd'),
                                                                                                  aur=bold('AUR'),
                                                                                                  root=bold('root')))
                return False

            if not shutil.which('runuser'):
                if watcher:
                    watcher.show_message(title=self.i18n['error'].capitalize(),
                                         type_=MessageType.ERROR,
                                         body=self.i18n['arch.aur.error.missing_root_dep'].format(dep=bold('runuser'),
                                                                                                  aur=bold('AUR'),
                                                                                                  root=bold('root')))
                return False

        return True

    def add_package_builder_user(self, handler: ProcessHandler) -> bool:
        if self.context.root_user and self.pkgbuilder_user:
            try:
                getpwnam(self.pkgbuilder_user)
                return True
            except KeyError:
                self.logger.warning(f"Package builder user '{self.pkgbuilder_user}' does not exist")
                self.logger.info(f"Adding the package builder user '{self.pkgbuilder_user}'")
                added, output = handler.handle_simple(SimpleProcess(cmd=['useradd', self.pkgbuilder_user], shell=True))

                if not added:
                    output_log = "Command output: {}".format(output.replace('\n', ' ') if output else '(no output)')
                    self.logger.error(f"Could not add the package builder user '{self.pkgbuilder_user}'. {output_log}")
                    handler.watcher.show_message(title=self.i18n['error'].capitalize(),
                                                 type_=MessageType.ERROR,
                                                 body=self.i18n['arch.aur.error.add_builder_user'].format(user=bold(self.pkgbuilder_user),
                                                                                                          aur=bold('AUR')))

                return added

        return True

    def _fill_available_packages(self, output: Dict[str, Set[str]]):
        output.update(pacman.map_available_packages())

    def _fill_suggestions(self, output: Dict[str, int]):
        self.suggestions_downloader.register_task(None)
        suggestions = self.suggestions_downloader.read(self.configman.read_config())

        if suggestions:
            output.update(suggestions)

    def _fill_cached_if_unset(self, pkg: ArchPackage, loader: DiskCacheLoader):
        data = loader.read(pkg)

        if data:
            for attr, cached_val in data.items():
                if cached_val:
                    current_val = getattr(pkg, attr)

                    if current_val is None:
                        setattr(pkg, attr, cached_val)

    def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[PackageSuggestion]]:
        if limit == 0:
            return

        arch_config = self.configman.get_config()

        if not arch_config['repositories']:
            return

        name_priority = dict()

        fill_suggestions = Thread(target=self._fill_suggestions, args=(name_priority,), daemon=True)
        fill_suggestions.start()

        available_packages = dict()
        fill_available = Thread(target=self._fill_available_packages, args=(available_packages,), daemon=True)
        fill_available.start()

        ignored_pkgs = set()
        fill_ignored = Thread(target=pacman.fill_ignored_packages, args=(ignored_pkgs,), daemon=True)
        fill_ignored.start()

        fill_suggestions.join()

        if not name_priority:
            self.logger.info("No Arch package suggestions found")
            return

        self.logger.info(f"Found {len(name_priority)} named Arch package suggestions")

        if fill_available:
            fill_available.join()

        if not available_packages:
            self.logger.error("No available Arch package found. It will not be possible to return suggestions")
            return

        fill_ignored.join()

        available_suggestions = dict()

        for n in name_priority:
            if n not in ignored_pkgs:
                data = available_packages.get(n)

                if data and (not filter_installed or not data['i']):
                    available_suggestions[n] = data

        if not available_suggestions:
            self.logger.info("No Arch package suggestion to return")
            return

        if filter_installed:
            ignored_updates = set()
            thread_fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,), daemon=True)
            thread_fill_ignored_updates.start()
        else:
            ignored_updates, thread_fill_ignored_updates = None, None

        suggestion_by_priority = sort_by_priority({n: name_priority[n] for n in available_suggestions})

        if available_suggestions and 0 < limit < len(available_suggestions):
            suggestion_by_priority = suggestion_by_priority[0:limit]

        self.logger.info(f'Available Arch package suggestions: {len(suggestion_by_priority)}')

        if thread_fill_ignored_updates:
            thread_fill_ignored_updates.join()

        full_data = pacman.map_packages(names=suggestion_by_priority, remote=filter_installed, not_signed=False,
                                        skip_ignored=True)

        if full_data and full_data.get('signed'):
            full_data = full_data['signed']

        disk_loader, caching_threads = None, None
        if not filter_installed:
            disk_loader = self.context.disk_loader_factory.new()
            caching_threads = list()

        suggestions = []
        for name in suggestion_by_priority:
            pkg_data = available_suggestions[name]
            pkg_full_data = full_data.get(name)
            description = None

            if pkg_full_data:
                description = pkg_full_data.get('description')

            pkg_updates_ignored = pkg_data['i'] and ignored_updates and name in ignored_updates
            pkg = ArchPackage(name=name,
                              version=pkg_data['v'],
                              latest_version=pkg_data['v'],
                              repository=pkg_data['r'],
                              installed=pkg_data['i'],
                              description=description,
                              categories=self.categories.get(name),
                              i18n=self.i18n,
                              maintainer=pkg_data['r'],
                              update_ignored=pkg_updates_ignored)

            if disk_loader:
                t = Thread(target=self._fill_cached_if_unset, args=(pkg, disk_loader), daemon=True)
                t.start()
                caching_threads.append(t)

            suggestions.append(PackageSuggestion(package=pkg, priority=name_priority[name]))

        if caching_threads:
            for t in caching_threads:
                t.join()

        return suggestions

    @property
    def suggestions_downloader(self) -> RepositorySuggestionsDownloader:
        if not self._suggestions_downloader:
            file_url = self.context.get_suggestion_url(self.__module__)

            self._suggestions_downloader = RepositorySuggestionsDownloader(logger=self.logger,
                                                                           http_client=self.http_client,
                                                                           i18n=self.i18n,
                                                                           file_url=file_url)

            if self._suggestions_downloader.is_custom_local_file_mapped():
                self.logger.info(f"Local Arch suggestions file mapped: {file_url}")

        return self._suggestions_downloader
