#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (C) 2014-2016 Canonical Ltd.
# Author: Christopher Townsend <christopher.townsend@canonical.com>

# 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; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import libertine.utils
import getpass
import os
import sys
import re

from apt.debfile import DebPackage
from libertine import LibertineContainer
from libertine.ContainersConfig import ContainersConfig
from libertine.HostInfo import HostInfo


class LibertineContainerManager(object):

    def __init__(self):
        self.containers_config = ContainersConfig()
        self.host_info = HostInfo()

    def create(self, args):
        password = None

        if args.distro and not self.host_info.is_distro_valid(args.distro, args.force):
            print("Invalid distro %s" % args.distro, file=sys.stderr)
            sys.exit(1)

        if self.containers_config.container_exists(args.id):
            print("Container id '%s' is already used." % args.id, file=sys.stderr)
            sys.exit(1)
        elif re.match("^[a-z0-9][a-z0-9+.-]+$", args.id) is None:
            print("Container id '%s' invalid. ID must be of form ([a-z0-9][a-z0-9+.-]+)." % args.id, file=sys.stderr)
            sys.exit(1)

        if not args.type:
            container_type = self.host_info.select_container_type_by_kernel()
        else:
            if args.type == 'lxc' and not self.host_info.has_lxc_support():
                print("System kernel does not support lxc type containers. "
                      "Please either use chroot or omit the -t option.")
                sys.exit(1)
            container_type = args.type

        if not args.distro:
            args.distro = self.host_info.get_host_distro_release()
        elif container_type == "chroot":
            host_distro = self.host_info.get_host_distro_release()

            if args.distro != host_distro:
                print("The container distribution needs to match the host ditribution for chroot"
                      " based containers. Please either use \'%s\' or omit the -d/--distro option."
                      % host_distro)
                sys.exit(1)

        if not args.name:
            args.name = "Ubuntu \'" + (self.host_info.get_distro_codename(args.distro) or args.distro) + "\'"

        if container_type == "lxc":
            if args.password:
                password = args.password
            elif sys.stdin.isatty():
                print("Enter password for your user in the Libertine container or leave blank for no password:")
                password = getpass.getpass()
            else:
                password = sys.stdin.readline().rstrip()

        self.containers_config.add_new_container(args.id, args.name, container_type, args.distro)

        multiarch = 'disabled'
        if args.multiarch == 'enable':
            multiarch = 'enabled'
        self.containers_config.update_container_multiarch_support(args.id, multiarch)

        container = LibertineContainer(args.id)
        self.containers_config.update_container_install_status(args.id, "installing")
        if not container.create_libertine_container(password, args.multiarch, args.verbosity):
            self.containers_config.delete_container(args.id)
            sys.exit(1)
        self.containers_config.update_container_install_status(args.id, "ready")

        libertine.utils.refresh_libertine_scope()

    def destroy_container_by_id(self, id):
        container = LibertineContainer(id)

        self.containers_config.update_container_install_status(id, "removing")
        container.destroy_libertine_container()
        self.containers_config.update_container_install_status(id, "removed")
        self.containers_config.delete_container(id)

    def destroy(self, args):
        args.id = self.containers_config.check_container_id(args.id)

        self.destroy_container_by_id(args.id)

        libertine.utils.refresh_libertine_scope()

    def install_package(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        is_debian_package = args.package.endswith('.deb')

        if is_debian_package:
            if os.path.exists(args.package):
                package = DebPackage(args.package).pkgname
            else:
                print("%s does not exist." % args.package)
                sys.exit(1)
        else:
            package = args.package

        if self.containers_config.package_exists(container_id, package):
            if not is_debian_package:
                print("Package \'%s\' is already installed." % package)
                sys.exit(1)
        else:
            self.containers_config.add_new_package(container_id, package)

        container = LibertineContainer(container_id)

        self.containers_config.update_package_install_status(container_id, package, "installing")
        if not container.install_package(args.package, args.verbosity, args.no_dialog):
            self.containers_config.delete_package(container_id, package)
            sys.exit(1)

        self.containers_config.update_package_install_status(container_id, package, "installed")

        libertine.utils.refresh_libertine_scope()

    def remove_package_by_name(self, container_id, package_name, verbosity=1, no_dialog=False):
        fallback_status = self.containers_config.get_package_install_status(container_id, package_name)
        self.containers_config.update_package_install_status(container_id, package_name, "removing")

        container = LibertineContainer(container_id)
        if not container.remove_package(package_name, verbosity, no_dialog) and fallback_status == 'installed':
            self.containers_config.update_package_install_status(container_id, package_name, fallback_status)
            return False

        self.containers_config.update_package_install_status(container_id, package_name, "removed")
        self.containers_config.delete_package(container_id, package_name)

        return True

    def remove_package(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        if self.containers_config.get_package_install_status(container_id, args.package) != 'installed':
            print("Package \'%s\' is not installed." % args.package)
            sys.exit(1)

        if not self.remove_package_by_name(container_id, args.package, args.verbosity, args.no_dialog):
            sys.exit(1)

        libertine.utils.refresh_libertine_scope()

    def search_cache(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        container = LibertineContainer(container_id)
        if container.search_package_cache(args.search_string) is not 0:
            sys.exit(1)

    def update(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        container = LibertineContainer(container_id)

        self.containers_config.update_container_install_status(container_id, "updating")
        if not container.update_libertine_container(args.verbosity):
            self.containers_config.update_container_install_status(container_id, "ready")
            sys.exit(1)

        self.containers_config.update_container_install_status(container_id, "ready")

    def list(self, args):
        containers = libertine.utils.Libertine.list_containers()
        for container in containers:
            print("%s" % container)

    def list_apps(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        container = LibertineContainer(container_id)
        print(container.list_app_launchers(use_json=args.json))

    def exec(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        container = LibertineContainer(container_id)

        if not container.exec_command(args.command):
            sys.exit(1)

    def delete_archive_by_name(self, container_id, archive_name, verbosity=1):
        if self.containers_config.get_archive_install_status(container_id, archive_name) == 'installed':
            self.containers_config.update_archive_install_status(container_id, archive_name, 'removing')
            if LibertineContainer(container_id).configure_remove_archive("\"" + archive_name + "\"", verbosity) is not 0:
                self.containers_config.update_archive_install_status(container_id, archive_name, 'installed')
                return False

        self.containers_config.delete_container_archive(container_id, archive_name)
        return True

    def configure(self, args):
        container_id = self.containers_config.check_container_id(args.id)

        container = LibertineContainer(container_id)

        if args.multiarch and self.host_info.get_host_architecture() == 'amd64':
            multiarch = 'disabled'
            if args.multiarch == 'enable':
                multiarch = 'enabled'

            current_multiarch = self.containers_config.get_container_multiarch_support(container_id)
            if current_multiarch == multiarch:
                print("i386 multiarch support is already %s" % multiarch)
                sys.exit(1)

            if container.configure_multiarch(args.multiarch, args.verbosity) is not 0:
                sys.exit(1)

            self.containers_config.update_container_multiarch_support(container_id, multiarch)

        elif args.archive is not None:
            if args.archive_name is None:
                print("Configure archive called with no archive name. See configure --help for usage.")
                sys.exit(1)

            archive_name = args.archive_name.strip("\'\"")
            archive_name_esc = "\"" + archive_name + "\""

            if args.archive == 'add':
                if self.containers_config.archive_exists(container_id, archive_name):
                    print("%s already added in container." % archive_name)
                    sys.exit(1)

                self.containers_config.add_container_archive(container_id, archive_name)
                self.containers_config.update_archive_install_status(container_id, archive_name, 'installing')
                if container.configure_add_archive(archive_name_esc, args.public_key_file, args.verbosity) is not 0:
                    self.containers_config.delete_container_archive(container_id, archive_name)
                    sys.exit(1)

                self.containers_config.update_archive_install_status(container_id, archive_name, 'installed')

            elif args.archive == 'remove':
                if not self.containers_config.archive_exists(container_id, archive_name):
                    print("%s is not added in container." % archive_name)
                    sys.exit(1)

                if not self.delete_archive_by_name(container_id, archive_name):
                    print("%s was not properly deleted." % archive_name)
                    sys.exit(1)

        elif args.bind_mount is not None:
            if args.mount_path is None:
                print("Configure bind-mounts called without mount path. See configure --help for usage")
                sys.exit(1)

            mount_path = args.mount_path.rstrip('/')

            # validate bind-mount
            if not mount_path.startswith(os.environ['HOME']) and not mount_path.startswith('/media/%s' % os.environ['USER']):
                print("Cannot mount '%s', mount path must be in $HOME or /media/$USER." % mount_path)
                sys.exit(1)
            if mount_path.startswith('/media/%s' % os.environ['USER']) and \
                   self.containers_config.get_container_type(container_id) == 'lxc':
                print("/media mounts not currently supported in lxc.")
                sys.exit(1)
            if not os.path.isdir(mount_path):
                print("Cannot mount '%s', mount path must be an existing directory." % mount_path)
                sys.exit(1)

            # update database with new bind-mount
            container_bind_mounts = self.containers_config.get_container_bind_mounts(container_id)
            if args.bind_mount == 'add':
                if mount_path in container_bind_mounts:
                    print("Cannot add mount '%s', bind-mount already exists." % mount_path)
                    sys.exit(1)
                self.containers_config.add_new_bind_mount(container_id, mount_path)
            elif args.bind_mount == 'remove':
                if mount_path not in container_bind_mounts:
                    print("Cannot remove mount '%s', bind-mount does not exist." % mount_path)
                    sys.exit(1)
                self.containers_config.delete_bind_mount(container_id, mount_path)


        else:
            print("Configure called with no subcommand. See configure --help for usage.")
            sys.exit(1)


    def merge(self, args):
        self.containers_config.merge_container_config_files(args.file)

    def fix_integrity(self, args):
        if 'containerList' in self.containers_config.container_list:
            for container in self.containers_config.container_list['containerList']:
                if 'installStatus' not in container or container['installStatus'] != 'ready':
                    self.destroy_container_by_id(container['id'])
                    continue
                LibertineContainer(container['id']).exec_command('dpkg --configure -a')

                for package in container['installedApps']:
                    if package['appStatus'] != 'installed':
                        self.remove_package_by_name(container['id'], package['packageName'])

                if 'extraArchives' in container:
                    for archive in container['extraArchives']:
                        if archive['archiveStatus'] != 'installed':
                            self.delete_archive_by_name(container['id'], archive['archiveName'])

    def set_default(self, args):
        if args.clear:
            self.containers_config.clear_default_container_id(True)
            sys.exit(0)

        container_id = self.containers_config.check_container_id(args.id)

        self.containers_config.set_default_container_id(container_id, True)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Legacy X application support for Unity 8")

    if not os.geteuid():
        print("Please do not run %s using sudo" % parser.prog)
        sys.exit(1)

    container_manager = LibertineContainerManager()

    parser.add_argument('-q', '--quiet',
                        action='store_const', dest='verbosity', const=0,
                        help=('do not print status updates on stdout'))
    parser.add_argument('-v', '--verbose',
                        action='store_const', dest='verbosity', const=2,
                        help=('extra verbose output'))
    subparsers = parser.add_subparsers(dest="subparser_name",
                                       title="subcommands",
                                       metavar='create, destroy, install-package, remove-package, search-cache, update, list, list-apps, configure')

    # Handle the create command and its options
    parser_create = subparsers.add_parser(
        'create',
        help=("Create a new Libertine container."))
    parser_create.add_argument(
        '-i', '--id',
        required=True,
        help=("Container identifier of form ([a-z0-9][a-z0-9+.-]+). Required."))
    parser_create.add_argument(
        '-t', '--type',
        help=("Type of Libertine container to create. Either 'lxc' or 'chroot'."))
    parser_create.add_argument(
        '-d', '--distro',
        help=("Ubuntu distro series to create."))
    parser_create.add_argument(
        '-n', '--name',
        help=("User friendly container name."))
    parser_create.add_argument(
        '--force', action='store_true',
        help=("Force the installation of the given valid Ubuntu distro even if "
              "it is no longer supported."))
    parser_create.add_argument(
        '-m', '--multiarch', action='store_true',
        help=("Add i386 support to amd64 Libertine containers.  This option has "
              "no effect when the Libertine container is i386."))
    parser_create.add_argument(
        '--password',
        help=("Pass in the user's password when creating an LXC container.  This "
              "is intended for testing only and is very insecure."))
    parser_create.set_defaults(func=container_manager.create)

    # Handle the destroy command and its options
    parser_destroy = subparsers.add_parser(
        'destroy',
        help=("Destroy any existing environment entirely."))
    parser_destroy.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_destroy.set_defaults(func=container_manager.destroy)

    # Handle the install-package command and its options
    parser_install = subparsers.add_parser(
        'install-package',
        help=("Install a package in the specified Libertine container."))
    parser_install.add_argument(
        '-p', '--package',
        required=True,
        help=("Name of package to install or full path to a Debian package. Required."))
    parser_install.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_install.add_argument(
        '-n', '--no-dialog', action='store_true',
        help=("No dialog mode. Use text-based frontend during debconf interactions."))
    parser_install.set_defaults(func=container_manager.install_package)

    # Handle the remove-package command and its options
    parser_remove = subparsers.add_parser(
        'remove-package',
        help=("Remove a package in the specified Libertine container."))
    parser_remove.add_argument(
        '-p', '--package',
        required=True,
        help=("Name of package to remove. Required."))
    parser_remove.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_remove.add_argument(
        '-n', '--no-dialog', action='store_true',
        help=("No dialog mode. Use text-based frontend during debconf interactions."))
    parser_remove.set_defaults(func=container_manager.remove_package)

    # Handle the search-cache command and its options
    parser_search = subparsers.add_parser(
        'search-cache',
        help=("Search for packages based on the search string in the specified Libertine container."))
    parser_search.add_argument(
        '-s', '--search-string',
        required=True,
        help=("String to search for in the package cache. Required."))
    parser_search.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_search.set_defaults(func=container_manager.search_cache)

    # Handle the update command and its options
    parser_update = subparsers.add_parser(
        'update',
        help=("Update the packages in the Libertine container."))
    parser_update.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_update.set_defaults(func=container_manager.update)

    # Handle the list command
    parser_list = subparsers.add_parser(
        "list",
        help=("List all Libertine containers."))
    parser_list.set_defaults(func=container_manager.list)

    # Handle the list-apps command and its options
    parser_list_apps = subparsers.add_parser(
        'list-apps',
        help=("List available app launchers in a container."))
    parser_list_apps.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_list_apps.add_argument(
        '-j', '--json',
        action='store_true',
        help=("use JSON output format."))
    parser_list_apps.set_defaults(func=container_manager.list_apps)

    # Handle the execute command and it's options
    parser_exec = subparsers.add_parser(
        'exec',
        add_help=False)
        #help=("Run an arbitrary command in the specified Libertine container."))
    parser_exec.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_exec.add_argument(
        '-c', '--command',
        help=("The command to run in the specified container."))
    parser_exec.set_defaults(func=container_manager.exec)

    # Handle the configure command and it's options
    parser_configure = subparsers.add_parser(
        'configure',
        help=("Configure various options in the specified Libertine container."))
    parser_configure.add_argument(
        '-i', '--id',
        help=("Container identifier.  Default container is used if omitted."))
    multiarch_group = parser_configure.add_argument_group("Multiarch support",
                      "Enable or disable multiarch support for a container.")
    multiarch_group.add_argument(
         '-m', '--multiarch',
         choices=['enable', 'disable'],
         help=("Enables or disables i386 multiarch support for amd64 Libertine "
               "containers. This option has no effect when the Libertine "
               "container is i386."))

    archive_group = parser_configure.add_argument_group("Additional archive support",
                    "Add or delete an additional archive (PPA).")
    archive_group.add_argument(
        '-a', '--archive',
        choices=['add', 'remove'],
        help=("Adds or removes an archive (PPA) in the specified Libertine container."))
    archive_group.add_argument(
      '-n', '--archive-name',
      metavar='Archive name',
      help=("Archive name to be added or removed."))
    archive_group.add_argument(
        '-k', '--public-key-file',
        metavar='Public key file',
        help=("File containing the key used to sign the given archive. "
              "Useful for third-party or private archives."))

    mount_group = parser_configure.add_argument_group("Additional bind-mounts",
                    "Add or delete an additional bind-mount.")
    mount_group.add_argument(
        '-b', '--bind-mount',
        choices=['add', 'remove'],
        help="Adds or removes a bind-mount in the specified Libertine container.")
    mount_group.add_argument(
      '-p', '--mount-path',
      metavar='Mount path',
      help=("The absolute host path to bind-mount."))

    parser_configure.set_defaults(func=container_manager.configure)

    # Handle merging another ContainersConfig.json file into the main ContainersConfig.json file
    parser_merge = subparsers.add_parser(
        'merge-configs',
        add_help=False)
    parser_merge.add_argument(
        '-f', '--file',
        required=True)
    parser_merge.set_defaults(func=container_manager.merge)

    # Indiscriminately destroy containers, packages, and archives which are not fully installed
    parser_integrity = subparsers.add_parser(
        'fix-integrity',
        add_help=False)
    parser_integrity.set_defaults(func=container_manager.fix_integrity)

    # Set the default container in ContainersConfig
    parser_default = subparsers.add_parser(
        'set-default',
        help=("Set the default container."))
    parser_default.add_argument(
        '-i', '--id',
        metavar='Container id',
        help=("Container identifier.  Default container is used if omitted."))
    parser_default.add_argument(
        '-c', '--clear', action='store_true',
        help=("Clear the default container."))
    parser_default.set_defaults(func=container_manager.set_default)

    # Actually parse the args
    args = parser.parse_args()
    if args.verbosity is None:
        args.verbosity = 1

    if args.subparser_name == None:
        parser.print_help()
    else:
        args.func(args)
