#! /usr/bin/env python2
# -*- coding: utf-8 -*-

# This script is part of the desktop management solution opsi
# (open pc server integration) http://www.opsi.org

# Copyright (C) 2018 uib GmbH

# http://www.uib.de/

# All rights reserved.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# as published by the Free Software Foundation.

# 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
# Affero 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/>.
"""
Migrating opsi-product-updater.conf to the recommended 4.1 formats.

@copyright:	uib GmbH <info@uib.de>
@author: Niko Wenselowski <n.wenselowski@uib.de>
@license: GNU Affero GPL version 3
"""

from __future__ import print_function

import argparse
import codecs
import glob
import os

from ConfigParser import SafeConfigParser

__version__ = '4.1.1.17'


TRANSFORMATION = {
    'opsi4.0/products/localboot': ['opsi4.1/stable/packages/windows/localboot'],
    'opsi4.0/products/netboot': ['opsi4.1/stable/packages/windows/netboot'],
    'opsi4.0/products/opsi-linux': [
        'opsi4.1/stable/packages/linux/localboot',
        'opsi4.1/stable/packages/linux/netboot'],
    'opsi4.0/products/opsi-local-image': [
        'opsi4.1/stable/packages/opsi-local-image/localboot',
        'opsi4.1/stable/packages/opsi-local-image/netboot'],
    'opsi4.0/testing/opsi-v407/products/localboot': ['opsi4.1/testing/packages/windows/localboot'],
    'opsi4.0/testing/opsi-v407/products/netboot': ['opsi4.1/testing/packages/windows/netboot'],
    'opsi4.0/testing/opsi-v407/products/opsi-linux': [
        'opsi4.1/testing/packages/linux/localboot',
        'opsi4.1/testing/packages/linux/netboot'],
    'opsi4.0/testing/opsi-v407/products/opsi-local-image': [
        'opsi4.1/testing/packages/opsi-local-image/localboot',
        'opsi4.1/testing/packages/opsi-local-image/netboot'],
    'opsi4.0/experimental/opsi-v407/products/localboot': ['opsi4.1/experimental/packages/windows/localboot'],
    'opsi4.0/experimental/opsi-v407/products/netboot': ['opsi4.1/experimental/packages/windows/netboot'],
    'opsi4.0/experimental/opsi-v407/products/opsi-linux': [
        'opsi4.1/experimental/packages/linux/localboot',
        'opsi4.1/experimental/packages/linux/netboot'],
    'opsi4.0/experimental/opsi-v407/products/opsi-local-image': [
        'opsi4.1/experimental/packages/opsi-local-image/localboot',
        'opsi4.1/experimental/packages/opsi-local-image/netboot']
}

# Self check
for key, values in TRANSFORMATION.items():
    if key.startswith('/') or key.endswith('/'):
        raise ValueError("Self check failed: invalid key {!r}".format(key))

    for value in values:
        if value.startswith('/') or value.endswith('/'):
            raise ValueError("Self check failed: invalid replacement {!r}".format(value))


def main():
    options = parse_cli()

    if not os.path.exists(options.configfile):
        raise OSError("No file {!r} found!".format(options.configfile))

    if not os.path.exists(options.destination):
        print("Creating missing directory {!r}".format(options.destination))
        os.makedirs(options.destination)

    if options.disable_destination:
        print("Disabling repos at {!r}".format(options.destination))
        disable_repos_in_directory(options.destination)
    elif options.clean_destination:
        print("Removing all .repo files from {!r}".format(options.destination))
        for filename in glob.glob(os.path.join(options.destination, '*.repo')):
            print("Removing {!r}".format(filename))
            os.unlink(filename)

    migrate(options.configfile, options.destination, options.new_config_file)


def parse_cli():
    parser = argparse.ArgumentParser(
        description=('Migrating opsi-product-updater.conf for '
                     'opsi-package-updater in opsi 4.1.'),
        epilog=('In the default configuration this will override an '
                'existing /etc/opsi/opsi-package-updater.conf.'),
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("--version", "-V", action='version', version=__version__)
    parser.add_argument('--configfile',
                        default='/etc/opsi/opsi-product-updater.conf',
                        help='The configuration file to migrate.',)
    parser.add_argument('--repo-destination', dest='destination',
                        default='/etc/opsi/package-updater.repos.d',
                        help="The folder where the .repo files will be created.")
    parser.add_argument('--new-config-file',
                        default='/etc/opsi/opsi-package-updater.conf',
                        help='Where will the new configuration file will be written.')
    dest_handling = parser.add_mutually_exclusive_group()
    dest_handling.add_argument('--clean-destination', dest='clean_destination',
                               action='store_true',
                               help="Remove all .repo files from the destination.")
    dest_handling.add_argument('--disable-repos', dest='disable_destination',
                               action='store_true',
                               help="Disable all repos in .repo files at the destination.")

    return parser.parse_args()


def disable_repos_in_directory(directory):
    for filename in glob.glob(os.path.join(directory, '*.repo')):
        is_active = False
        content = []
        with codecs.open(filename, 'r', 'utf-8') as f:
            for line in f:
                if line.startswith('active'):
                    try:
                        key, value = line.split('=', 1)

                        if value.lower().strip() in ('true', 'on'):
                            is_active = True
                            content.append('active = false\n')
                            continue
                    except IndexError:
                        pass

                content.append(line)

        if is_active:
            with codecs.open(filename, 'w', 'utf-8') as f:
                f.writelines(content)

            print("Disabled repos in {!r}".format(filename))


def migrate(configfile, destination, new_configfile):
    config = SafeConfigParser()
    config.read(configfile)

    globalConfig = {}
    for section in config.sections():
        settings = {}
        for key, value in config.items(section):
            settings[key] = value

        if section.startswith('repository_'):
            if 'active' not in settings:
                print("Unable to determine repository status of {!r} - skipping".format(section))
                continue

            if settings['active'].lower() not in ('true', 'on'):
                print("Skipping inactive repository section {!r}".format(section))
                continue

            print("Processing section {!r}".format(section))
            try:
                if 'download.uib.de' in settings['baseurl']:
                    paths = [path.strip() for path in settings['dirs'].split(',') if path.strip()]
                    settings['dirs'] = ', '.join(transform_paths(paths))
            except KeyError:
                pass

            new_config = SafeConfigParser()
            new_config.add_section(section)
            for key, value in settings.items():
                new_config.set(section, key, value)

            filename = section[len('repository_'):]
            filename += '.repo'
            filename = os.path.join(destination, filename)

            print("Migrating section {!r} to file {!r}".format(section, filename))
            with codecs.open(filename, 'w', 'utf-8') as repoFile:
                new_config.write(repoFile)
        else:
            globalConfig[section] = settings

    # Adding path to .repo files
    globalConfig['general']['repositoryConfigDir'] = os.path.abspath(destination)

    # Changing path of logfile if this is the old default
    if globalConfig['general']['logfile'].strip() == '/var/log/opsi/opsi-product-updater.log':
        globalConfig['general']['logfile'] = '/var/log/opsi/opsi-package-updater.log'

    new_global_config = SafeConfigParser()
    for section in sorted(globalConfig):
        new_global_config.add_section(section)
        for key, value in globalConfig[section].items():
            new_global_config.set(section, key, value)

    print("Writing new configuration to {!r}".format(new_configfile))
    with codecs.open(new_configfile, 'w', 'utf-8') as repoFile:
        new_global_config.write(repoFile)

    print("Configuration file migrated.")
    print("Please check for duplicated repository configurations.")


def transform_paths(old_paths):
    paths = []
    for path in old_paths:
        try:
            norm_path = normalize_path(path)
            for new_path in TRANSFORMATION[norm_path]:
                print("Replacing directory {!r} by {!r}".format(path, new_path))
                paths.append(new_path)
        except KeyError:
            paths.append(path)

    return paths


def normalize_path(path):
    new_path = path.strip()

    if new_path.startswith('/'):
        new_path = new_path[1:]

    if new_path.endswith('/'):
        new_path = new_path[:-1]

    return new_path

if __name__ == '__main__':
    main()
