#!/usr/bin/python3 -u
# -*- coding: utf-8 -*-
#
##########################################################################
# Maj-Auto - Manage automatique update of EOLE server
# Copyright © 2015 Pôle de compétences EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
##########################################################################

#add this in post-upgrade
#systemctl mask netplan-wait-online.service networkd-dispatcher.service netplan-apply.service

import sys
import warnings
import atexit
from argparse import ArgumentParser
import locale
import time
import re
from os import system, unlink
from os.path import join, isfile, isdir, basename
from shutil import copytree, copy, rmtree
from glob import glob
import apt
from pathlib import Path

from pyeole.i18n import i18n
from pyeole import lock
from pyeole.process import system_code
from pyeole.ansiprint import print_title, print_red, print_green
from pyeole.ihm import question_ouinon
from pyeole.pkg import EolePkg, _configure_sources_mirror
from pyeole import pkg

from creole.fonctionseole import controle_kernel, zephir
from creole.config import eoledir, templatedir, configeoldir
from creole.template import CreoleTemplateEngine
from creole.client import CreoleClient
from creole.eoleversion import UBUNTU_VERSION

warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning)
_ = i18n('update-manager')

if isfile(join(configeoldir, '.release_available')):
    print("")
    print_red(_('No newer major version available'))
    print("")
    print(_('Please, use Maj-Release script first to upgrade the minor version'))
    print("")
    sys.exit(1)

if not isfile(join(configeoldir, '.upgrade_available')):
    print("")
    print_red(_('No newer major version available'))
    print("")
    sys.exit(1)

try:
    sys.path.append('/usr/lib/python3/dist-packages')
    from UpdateManager.Core.MetaRelease import MetaReleaseCore
    from DistUpgrade import DistUpgradeFetcherCore
    from DistUpgrade.utils import init_proxy
    sys.path.pop()
except:
    print("")
    print_red(_('No newer major version available'))
    print("")
    sys.exit(1)

quit_re = re.compile(r'q|(quit)|(exit)|(abort)', re.I)
tmp_dir = '/tmp/Upgrade-Auto'

MATRIX = {}
MATRIX['2.10.0'] = ['amon',
                    'amonecole',
                    'eolebase',
                    'scribe',
                    'seshat',
                    'seth',
                    'sphynx',
                    'thot',
                    'zephir',
                    #'hapy',
                    ]
UBUNTU_TARGET = 'noble'
ENVOLE_TARGET = '9'
CLIENT = CreoleClient()

def verify_module_not_allowed(release):
    module = CLIENT.get_creole('eole_module')
    if module not in MATRIX[release]:
        confirmation_msg = _("The upgrade of this module is not officially supported, do you to want to continue?")
        if question_ouinon(confirmation_msg) != 'oui':
            sys.exit(1)

def release_lock():
    if lock.is_locked('upgrade-auto', level='system'):
        lock.release('upgrade-auto', level='system')


# do not confirme the upgrade
def fake_readline():
    return DistUpgradeFetcherCore._("y")
DistUpgradeFetcherCore.readline = fake_readline


class EOLEDistUpgradeFetcherCore(DistUpgradeFetcherCore.DistUpgradeFetcherCore):
#    def __init__(self, **kwargs):
#        self.DEBUG = True
#        super().__init__(**kwargs)

    def verifyDistUprader(self, *args, **kwargs):
        copy(join(tmp_dir, 'DistUpgradeViewEOLE.py'), self.tmpdir)
        copy(join(self.tmpdir, 'utils.py'), join(self.tmpdir, 'old_utils.py'))
        copy(join(tmp_dir, 'utils.py'), self.tmpdir)
        return super(EOLEDistUpgradeFetcherCore, self).verifyDistUprader(*args, **kwargs)


def cli_choice(alternatives, prompt, title=None, guess=False):
    """
    Display choices in terminal and return chosen one
    :param alternatives: choices proposed to user
    :type alternatives: list
    :param guess: wether to guess choice
    :type guess: boolean
    """
    def default_input(alt_mapping, prompt, title=None, guess=False):
        choices = "\n".join(["[{0}] {1}".format(alt[0], alt[1])
                             for alt in alt_mapping.items()])
        choices = _("Available choices:\n{}\n").format(choices)
        if title is not None:
            print_green(title)
        print(choices)
        if guess is True and len(alt_mapping) < 2:
            print_green(_("Automatically selected first choice: {}\n").format(alt_mapping[1]))
            choice = "1"
        else:
            try:
                prompt = prompt + _("\n[1]: ")
                choice = input(prompt)
            except(KeyboardInterrupt, EOFError):
                print(_("\nUpgrade aborted by user"))
                sys.exit(0)
            if choice == '':
                choice = "1"
        return choice

    alt_mapping = {num + 1: choice for num, choice in enumerate(alternatives)}
    choice = default_input(alt_mapping, prompt, title=title, guess=guess)
    if choice not in alt_mapping.values():
        try:
            choice = alt_mapping[int(choice)]
        except KeyError:
            print(_("Choice {} not available\n").format(choice))
            choice = cli_choice(alternatives, prompt, guess=guess)
        except ValueError:
            if quit_re.match(choice):
                print(_("Upgrade cancelled by user"))
                sys.exit(0)
            else:
                print(_("Invalid input: {}\n").format(choice))
                choice = cli_choice(alternatives, prompt, guess=guess)
    return choice


def upgrade_container_source(container, *, scribe=False):
    """Edit source list in container
    :param container: container name
    :type container: str
    :param scribe: the module is not in container mode but has a container
    :type scribe: bool
    """
    print('changement des sources.list pour le conteneur {}'.format(container))
    if scribe:
        source_list = '/var/lib/lxc/{}/rootfs/etc/apt/sources.list'.format(container)
    else:
        source_list = '/opt/lxc/{}/rootfs/etc/apt/sources.list'.format(container)
    with open(source_list) as old_source:
        sources = old_source.read()
    with open(source_list, 'w') as new_source:
        new_source.write(sources.replace(UBUNTU_VERSION, UBUNTU_TARGET))
    if not scribe:
        cmd = ['apt-get', 'update']
        code = system_code(cmd, container=container)
        return code


# set the devel/rc version here, otherwise ''
#RC_VERSION = 'rc1'
#RC_VERSION = 'devel'
RC_VERSION = ''
ALTERNATIVES = tuple(MATRIX.keys())
RUNPARTS_CMD = '/bin/run-parts --exit-on-error -v {directory}'
PRE_SOURCE = 'pre_source'
PRE_UPGRADE = 'pre_upgrade'
UPGRADE_CONTAINER = 'upgrade_{0}'
UPGRADE_DIR = join(eoledir, 'upgrade')
z_proc = "UPGRADE"


def main():
    args_parser = ArgumentParser(description=_("EOLE distribution upgrade tool."))

    args_parser.add_argument('--release',
                             help=_("Target release number"))

#    args_parser.add_argument('--download', action='store_true',
#                             help=_("Only download the ISO image"))

    args_parser.add_argument('-f', '--force', action='store_true',
                             help=_("Do not ask confirmation"))

    args = args_parser.parse_args()


    try:
        locale.setlocale(locale.LC_ALL, "")
    except:
        pass

    print_red(_("This script will upgrade this server to a new release"))
    print_red(_("Modifications will be irreversible."))
    init_proxy()
    zephir("INIT", _("Starting Upgrade-Auto ({})").format(" ".join(sys.argv[1:])), z_proc)

    if args.release is not None:
        if args.release not in ALTERNATIVES:
            msg = _("Invalid release {version} use: {values}")
            err_msg = msg.format(version=args.release,
                                 values=', '.join(ALTERNATIVES))
            zephir("ERR", err_msg, z_proc)
            print_red(err_msg)
            sys.exit(1)
    else:
        title = _("Choose which version you want to upgrade to\n")
        prompt = _("Which version do you want to upgrade to (or 'q' to quit)?")
        args.release = cli_choice(ALTERNATIVES, prompt, title=title, guess=False)

    if not args.force:
        confirmation_msg = _("Do you really want to upgrade to version {}?")
        if question_ouinon(confirmation_msg.format(args.release)) != 'oui':
            end_msg = _('Upgrade cancelled by user')
            zephir("FIN", end_msg, z_proc)
            print_red(end_msg)
            sys.exit(0)

    verify_module_not_allowed(args.release)

    lock.acquire('upgrade-auto', valid=False, level='system')
    atexit.register(release_lock)

#    if not args.download:
    print_title(_("Check update status"))
    PKGMGR = EolePkg('apt', ignore=False)
    PKGMGR.set_option('APT::Get::Simulate', 'true')
    if RC_VERSION == 'devel':
        eole_level = 'unstable'
        envole_level = 'unstable'
    elif RC_VERSION:
        eole_level = 'proposed'
        envole_level = 'proposed'
    else:
        eole_level = 'stable'
        envole_level = 'stable'
    _configure_sources_mirror(PKGMGR.pkgmgr,
                              eole_level=eole_level,
                              envole_level=envole_level)
    PKGMGR.update(silent=True)
    upgrades = PKGMGR.get_upgradable_list()

    for container, packages in upgrades.items():
        if packages:
            err_msg = _("Some packages are not up-to-date!")
            zephir("ERR", err_msg, z_proc)
            print_red(err_msg)
            print_red(_("Update this server (Maj-Auto) before another attempt to upgrade"))
            sys.exit(1)

    print_green(_('Server is up-to-date'))

    if controle_kernel():
        err_msg = _("In order to upgrade, most recent kernel endorsed for this release must be used")
        zephir("ERR", err_msg, z_proc)
        print_red(err_msg)
        sys.exit(1)

    print_green(_('This server uses most recent kernel'))

#    if args.download:
#        end_msg = _("Download only detected, stop")
#        print_green(end_msg)
#        zephir("FIN", end_msg, z_proc)
#        sys.exit(0)

    print_title(_("Copying upgrade scripts"))
    if isdir(tmp_dir):
        err_msg = _("Directory {0} already exists").format(tmp_dir)
        zephir("ERR", err_msg, z_proc)
        print_red(err_msg)
        if args.force:
            sys.exit(1)
        confirmation_msg = _("Do you want to delete the directory and continue?")
        if question_ouinon(confirmation_msg) != 'oui':
            sys.exit(1)
        rmtree(tmp_dir)

    copytree(UPGRADE_DIR, tmp_dir)

    print_title(_("Module specific commands"))
    pre_source = join(UPGRADE_DIR, PRE_SOURCE)
    code = system(RUNPARTS_CMD.format(directory=pre_source))
    if code != 0:
        err_msg = _('Error {0}').format(pre_source)
        print_red(err_msg)
        zephir("ERR", err_msg, z_proc)
        sys.exit(1)

    print_title(_("Configuring upgrade"))
    engine = CreoleTemplateEngine()
    rootctx = {'name': 'root', 'path': ''}

    # order is arbitrary and eole.cfg must be the last loaded
    # so remove extra .cfg
    for filename in glob('/etc/update-manager/release-upgrades.d/*.cfg'):
        unlink(filename)
    for filename in ['/etc/update-manager/release-upgrades.d/eole.cfg', '/etc/update-manager/mirrors_eole.cfg', '/etc/update-manager/meta-release']:
        file_info = {'name': filename,
                     'source': join(templatedir, basename(filename)),
                     'full_name': filename,
                     'activate' : True,
                     'del_comment': '',
                     'mkdir' : False,
                     'rm' : False}
        engine.prepare_template(file_info)
        engine.process(file_info, rootctx)

    release = args.release
    #overwrite envole_version
    envole_release = ENVOLE_TARGET
    level = 'stable'
    if RC_VERSION:
        if RC_VERSION == 'devel':
            level = 'unstable'
        else:
            level = 'proposed'
    version = release.rsplit('.', 1)[0]
    _configure_sources_mirror(PKGMGR.pkgmgr, version=version, release=release, envole_release=envole_release, eole_level=level)

    print_title(_("Module specific commands"))
    pre_upgrade = join(tmp_dir, PRE_UPGRADE)
    code = system(RUNPARTS_CMD.format(directory=pre_upgrade))
    if code != 0:
        err_msg = _('Error {0}').format(pre_upgrade)
        print_red(err_msg)
        zephir("ERR", err_msg, z_proc)
        sys.exit(1)

    for container in (cont for cont in upgrades if cont != 'root'):
        upgrade_container_source(container)
        for cont in CLIENT.get_group_infos(container)['containers']:
            upgrade_container = join(UPGRADE_DIR, UPGRADE_CONTAINER.format(cont))
            if Path(upgrade_container).is_dir():
                code = system(RUNPARTS_CMD.format(directory=upgrade_container))
                if code != 0:
                    err_msg = _('Error {0}').format(upgrade_container)
                    print_red(err_msg)
                    zephir("ERR", err_msg, z_proc)
                    sys.exit(1)
        code = system_code(['chroot', f'/opt/lxc/{container}/rootfs/', 'apt', 'install', 'systemd', 'openssh-server', 'eole-common-pkg', '-y'])
        if code != 0:
            err_msg = _(f'Error in container {container}')
            print_red(err_msg)
            zephir("ERR", err_msg, z_proc)
            sys.exit(1)
        PKGMGR.dist_upgrade(container=container, silent=False)
    if Path('/var/lib/lxc/addc/rootfs').is_dir() and Path('/usr/share/eole/majauto/eolead').is_file():
        pkg._SOURCES_LISTS_FILE = '/var/lib/lxc/addc/rootfs/etc/apt/sources.list'
        pkg._configure_sources_mirror(PKGMGR.pkgmgr, version=version, release=release, eole_level=level, force_no_envole=True)
        upgrade_container_source("addc", scribe=True)
        prefix_cmd = ['chroot', '/var/lib/lxc/addc/rootfs/']
        for cmd in (['cp', '/etc/apt/trusted.gpg.d/eole-archive-keyring.gpg', '/var/lib/lxc/addc/rootfs/etc/apt/trusted.gpg.d/eole-archive-keyring.gpg'],
                    ['cp', '/etc/apt/apt.conf.d/02eoleproxy', '/var/lib/lxc/addc/rootfs/etc/apt/apt.conf.d/02eoleproxy'],
                    ["lxc-stop", "-n", "addc"],
                    prefix_cmd + ['apt', 'update'],
                    prefix_cmd + ['apt-get', 'dist-upgrade', '-y'],
                    prefix_cmd + ['apt', 'install', 'eole-ad-pkg', '-y'],
                    ):
            code = system_code(cmd)
            if code != 0:
                err_msg = _('Error in ADDC container')
                print_red(err_msg)
                zephir("ERR", err_msg, z_proc)
                sys.exit(1)
    meta = MetaReleaseCore(useDevelopmentRelease=False,
                           useProposed=False)
    # this will timeout eventually
    while meta.downloading:
        time.sleep(0.5)
    progress = apt.progress.text.AcquireProgress()
    fetcher = EOLEDistUpgradeFetcherCore(new_dist=meta.new_dist,
                                         progress=progress)
    fetcher.run_options += ["--mode=server",
                            "--frontend=DistUpgradeViewEOLE",
                            ]
    print_title(_("Upgrading server"))
    fetcher.run()
    # all lines below will not be executed


if __name__ == "__main__":
    try:
        main()
    except (KeyboardInterrupt, EOFError):
        print_red(_("\nUpgrade aborted by user"))
    except SystemError as err:
        print_red(str(err))
        sys.exit(1)
