# Copyright 2005 Duke University
# Copyright (C) 2012-2016 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
Entrance point for the yum command line interface.
"""

from __future__ import print_function
from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.conf import Conf
from dnf.cli.cli import Cli
from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import ucd
from dnf.cli.utils import show_lock_owner
from dnf.i18n import _

import dnf.cli
import dnf.cli.cli
import dnf.cli.option_parser
import dnf.exceptions
import dnf.lock
import dnf.i18n
import dnf.logging
import dnf.util
import errno
import hawkey
import libdnf.error
import logging
import os
import os.path
import sys
from contextlib import nullcontext

logger = logging.getLogger("dnf")


def ex_IOError(e):
    logger.log(dnf.logging.SUBDEBUG, '', exc_info=True)
    logger.critical(ucd(e))
    return 1


def ex_Error(e):
    logger.log(dnf.logging.SUBDEBUG, '', exc_info=True)
    if e.value is not None:
        logger.critical(_('Error: %s'), ucd(e))
    return 1


def main(args, conf_class=Conf, cli_class=Cli, option_parser_class=OptionParser):
    try:
        dnf.i18n.setup_stdout()
        with dnf.cli.cli.BaseCli(conf_class()) as base:
            return _main(base, args, cli_class, option_parser_class)
    except dnf.exceptions.ProcessLockError as e:
        logger.critical(e.value)
        show_lock_owner(e.pid)
        return 200
    except dnf.exceptions.LockError as e:
        logger.critical(e.value)
        return 200
    except dnf.exceptions.DepsolveError as e:
        return 1
    except dnf.exceptions.Error as e:
        return ex_Error(e)
    except hawkey.Exception as e:
        logger.critical(_('Error: %s'), ucd(e))
        return 1
    except libdnf.error.Error as e:
        logger.critical(_('Error: %s'), ucd(e))
        return 1
    except IOError as e:
        return ex_IOError(e)
    except KeyboardInterrupt as e:
        logger.critical('{}: {}'.format(type(e).__name__, _("Terminated.")))
        return 1


def _main(base, args, cli_class, option_parser):
    """Run the dnf program from a command line interface."""

    # our core object for the cli
    base._logging._presetup()
    cli = cli_class(base)

    # do our cli parsing and config file setup
    # also sanity check the things being passed on the cli
    try:
        cli.configure(list(map(ucd, args)), option_parser())
    except (IOError, OSError) as e:
        return ex_IOError(e)

    return cli_run(cli, base)


def cli_run(cli, base):
    # Try to open the current directory to see if we have
    # read and execute access. If not, chdir to /
    try:
        f = open(".")
    except IOError as e:
        if e.errno == errno.EACCES:
            logger.critical(_('No read/execute access in current directory, moving to /'))
            os.chdir("/")
    else:
        f.close()

    lock = None
    if cli.demands.clean_command_lock == CleanCommandLock.NONE:
        lock = nullcontext()
    elif cli.demands.clean_command_lock == CleanCommandLock.READ:
        lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, True)
    elif cli.demands.clean_command_lock == CleanCommandLock.WRITE:
        lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, False)
    else:
        raise RuntimeError('Invalid demands.clean_command_lock: %s' % cli.demands.clean_command_lock)

    with lock:
        try:
            cli.run()
        except dnf.exceptions.LockError:
            raise
        except (IOError, OSError) as e:
            return ex_IOError(e)

        if cli.demands.resolving:
            try:
                ret = resolving(cli, base)
            except dnf.exceptions.DepsolveError as e:
                ex_Error(e)
                msg = ""
                if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True):
                    msg += _("try to add '{}' to command line to replace conflicting "
                             "packages").format("--allowerasing")
                if cli.base.conf.strict:
                    if not msg:
                        msg += _("try to add '{}' to skip uninstallable packages").format(
                            "--skip-broken")
                    else:
                        msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken")
                if cli.base.conf.best:
                    prio = cli.base.conf._get_priority("best")
                    if prio <= dnf.conf.PRIO_MAINCONFIG:
                        if not msg:
                            msg += _("try to add '{}' to use not only best candidate packages").format(
                                "--nobest")
                        else:
                            msg += _(" or '{}' to use not only best candidate packages").format(
                                "--nobest")
                if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types:
                    if not msg:
                        msg += _("try to add '{}' to load additional filelists metadata").format(
                            "--setopt=optional_metadata_types=filelists")
                    else:
                        msg += _(" or '{}' to load additional filelists metadata").format(
                            "--setopt=optional_metadata_types=filelists")
                if msg:
                    logger.info("({})".format(msg))
                raise
            if ret:
                return ret

        cli.command.run_transaction()
        return cli.demands.success_exit_status


def resolving(cli, base):
    """Perform the depsolve, download and RPM transaction stage."""

    if base.transaction is None:
        base.resolve(cli.demands.allow_erasing)
        logger.info(_('Dependencies resolved.'))

    cli.command.run_resolved()

    # Run the transaction
    displays = []
    if cli.demands.transaction_display is not None:
        displays.append(cli.demands.transaction_display)
    try:
        base.do_transaction(display=displays)
    except dnf.cli.CliError as exc:
        logger.error(ucd(exc))
        return 1
    except dnf.exceptions.TransactionCheckError as err:
        for msg in cli.command.get_error_output(err):
            logger.critical(msg)
        return 1
    except IOError as e:
        return ex_IOError(e)
    else:
        logger.info(_('Complete!'))
    return 0


def user_main(args, exit_code=False):
    """Call one of the multiple main() functions based on environment variables.

    :param args: command line arguments passed into yum
    :param exit_code: if *exit_code* is True, this function will exit
       python with its exit code when it has finished executing.
       Otherwise, it will return its exit code.
    :return: the exit code from dnf.yum execution
    """

    errcode = main(args)
    if exit_code:
        sys.exit(errcode)
    return errcode


if __name__ == "__main__":
    user_main(sys.argv[1:], exit_code=True)
