#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (C) 2013:
#    Gabes Jean, j.gabes@shinken-solutions.com
#
# This file is part of Shinken Enterprise, all rights reserved.

import errno
import optparse
import os
import re
import shutil
import subprocess
import sys
from os import fdopen
from tempfile import mkstemp

from pymongo.connection import Connection

from shinkensolutions.localinstall import protect_stdout, unprotect_stdout, OK_COLOR, WARNING_COLOR, ERROR_COLOR, RESET_SHELL_TAG

try:
    from shinken.synchronizer.dao.crypto import CRYPTO_NOT_TO_LOOK
    from shinken.synchronizer.synchronizerconfig import SynchronizerConfig
except ImportError:
    from synchronizer.dao.crypto import CRYPTO_NOT_TO_LOOK
    from synchronizer.synchronizerconfig import SynchronizerConfig

SYNCHRONIZER_OVERLOAD_FILE = u'/etc/shinken-user/configuration/daemons/synchronizers/synchronizer_cfg_overload.cfg'
DEFAULT_UMASK = 0022
# Set umask to avoid problems when creating files
os.umask(DEFAULT_UMASK)

YELLOW_COLOR = 33


def get_colored_text(text, color):
    # type: (unicode, int) -> unicode
    return u'%(color_tag)s%(text)s%(reset_tag)s' % {
        u'color_tag': u'\033[%dm' % color,
        u'text'     : text,
        u'reset_tag': RESET_SHELL_TAG
    }


def print_error(error_msg):
    print get_colored_text(u'\n%s: ERROR: %s\n' % (os.path.basename(sys.argv[0]), error_msg), ERROR_COLOR)


def get_conf(sync_config_files):
    # avoid printing the config files in stdout
    protect_stdout()
    conf = SynchronizerConfig()
    buf = conf.read_config(sync_config_files)
    raw_objects = conf.read_config_buf(buf)
    conf.create_objects_for_type(raw_objects, u'synchronizer')
    conf.early_synchronizer_linking()
    conf.fill_default()
    unprotect_stdout()
    return conf


def get_mongo_db(conf):
    try:
        mongodb_uri = conf.mongodb_uri
        mongodb_database = conf.mongodb_database
        mongodb_con = Connection(mongodb_uri, fsync=True)  # force a fsync connection, so all access are forced even if it's slower on writes
        mongodb_db = getattr(mongodb_con, mongodb_database)
        return mongodb_db
    except:
        print get_colored_text(u'ERROR: Unable to connect to the mongo database to retrieve data impacted by the fields protection', ERROR_COLOR)
        sys.exit(1)


def show_data_to_proceed(conf, src_fields, additional_substrings=None, removal_substrings=None, quiet=False):
    if removal_substrings is None:
        removal_substrings = []
    if additional_substrings is None:
        additional_substrings = []
    dst_fields = src_fields.union(set(additional_substrings)) - set(removal_substrings)
    
    data_to_unprotect = set()
    data_to_protect = set()
    
    mongodb_db = get_mongo_db(conf)
    
    all_collection_names = mongodb_db.collection_names(include_system_collections=False)
    
    for collection_name in all_collection_names:
        if not collection_name.startswith(u'configuration'):
            continue
        collection = getattr(mongodb_db, collection_name)
        items = collection.find()
        for item in items:
            data_keys = [k.upper() for k in item.iterkeys() if k.startswith(u'_') and k not in CRYPTO_NOT_TO_LOOK]
            for data_name in data_keys:
                for to_unprotect in src_fields:
                    if to_unprotect in data_name:
                        data_to_unprotect.add(data_name)
                for to_protect in dst_fields:
                    if to_protect in data_name:
                        data_to_protect.add(data_name)
    
    additional_data_name = data_to_protect - data_to_unprotect
    removal_data_name = data_to_unprotect - data_to_protect
    data_name_already_protected = data_to_unprotect - removal_data_name - additional_data_name
    
    if not quiet:
        print u'The following listing shows :'
        print u' - Data which contains the substrings is already defined in the current configuration'
        print get_colored_text(u' - Data which contains the substrings you are adding\t => will be protected when the Synchronizer is restarted', OK_COLOR)
        print get_colored_text(u' - Data which contains the substrings you are removing\t => will be in cleartext when the Synchronizer is restarted', ERROR_COLOR)
        print
    
    string_currently_matching = u' Currently matching :'
    max_len = len(string_currently_matching)
    string_added_substrings = {}
    string_removed_substrings = {}
    for substring in additional_substrings:
        string_added_substrings[substring] = u' Added substrings   〖 %s 〗 matches :' % substring
        max_len = max(max_len, len(string_added_substrings[substring]) + 2)
    
    for substring in removal_substrings:
        string_removed_substrings[substring] = u' Removed substrings 〖 %s 〗 data name that doesn\'t match anymore :' % substring
        max_len = max(max_len, len(string_removed_substrings[substring]) + 2)
    
    for data_name in sorted(data_name_already_protected):
        print (u'%-' + unicode(max_len) + u's %s') % (string_currently_matching, data_name)
    
    for data_name in sorted(additional_data_name):
        _substring_matched = next((_substring for _substring in string_added_substrings.iterkeys() if _substring in data_name), None)
        print get_colored_text((u'%-' + unicode(max_len - 2) + u's %s') % (string_added_substrings.get(_substring_matched, u'substring not found'), data_name), OK_COLOR)
    
    for data_name in sorted(removal_data_name):
        _substring_matched = next((_substring for _substring in string_removed_substrings.iterkeys() if _substring in data_name), None)
        print get_colored_text((u'%-' + unicode(max_len - 2) + u's %s') % (string_removed_substrings.get(_substring_matched, u'substring not found'), data_name), ERROR_COLOR)
    
    if not any(additional_data_name.union(removal_data_name)):
        if additional_substrings or removal_substrings:
            print get_colored_text(u'\nThere is no field containing any of the substrings you are adding or removing.\n', YELLOW_COLOR)


def yn_choice(message, default=u'y'):
    choices = u'Y/n' if default.lower() in (u'y', u'yes') else u'y/N'
    choice = raw_input(u'%s (%s) ' % (message, choices))
    values = (u'y', u'yes', u'') if choices == u'Y/n' else (u'y', u'yes')
    return choice.strip().lower() in values


def update_field_in_file(field, new_value, file_path):
    line_to_write = u'%s=%s\n' % (field, new_value)
    try:
        f = open(file_path, u'r')
        lines = f.read().splitlines()
        f.close()
    except IOError as exp:
        msg_error = u'Cannot change %s=%s into the file %s: %s' % (field, new_value, file_path, exp)
        if (exp.errno == errno.EPERM) or (exp.errno == errno.EACCES):
            msg_error = u'Cannot change %s:%s into the file %s : Permission denied' % (field, new_value, file_path)
        if exp.errno == errno.ENOENT:
            msg_error = u'Cannot change %s:%s into the file %s : File not found' % (field, new_value, file_path)
        return msg_error
    
    # Create temp file
    temp_file_descriptor, temp_file_name = mkstemp()
    with fdopen(temp_file_descriptor, u'w') as new_file:
        found = False
        for line in lines:
            line = line.decode(u'utf-8', u'ignore')
            if line.startswith(u'#'):  # comments directly go to new_file
                new_file.write((line + u'\n').encode(u'utf-8', u'ignore'))
                continue
            
            elts = line.strip().split(u'=')
            if elts[0] != field:  # Do not erase other fields
                new_file.write((line + u'\n').encode(u'utf-8', u'ignore'))
                continue
            
            found = True
            # we have our field here, replace the line now
            new_file.write(line_to_write.encode(u'utf-8', u'ignore'))
        if not found:
            # the field was not found, we must add the line
            new_file.write(line_to_write.encode(u'utf-8', u'ignore'))
    # Move new file
    shutil.move(temp_file_name, file_path)
    return None


def get_options_parser():
    parser = optparse.OptionParser()
    config_opts = optparse.OptionGroup(parser, u'General options')
    config_opts.add_option(u'-c', u'--config', dest=u'synchronizer_config', action=u'store', type=u'string', default=u'/etc/shinken/synchronizer.cfg', help=u'The synchronizer cfg file from which the configuration is read')
    config_opts.add_option(u'-q', u'--quiet', dest=u'quiet', action=u'store_true', default=False, help=u'Only display meaningful information without all the detailed explanations', metavar=u'fields')
    
    action_opts = optparse.OptionGroup(parser, title=u'Available actions.', description=u'Substrings can only contain these characters : "A" to "Z", "0" to "9", "_", "-".\nLower case characters will be capitalized')
    action_opts.add_option(u'-a', u'--add', dest=u'additional_substrings', action=u'append', type=u'string', default=[], help=u'Substrings you want to add in the configuration', metavar=u'fields')
    action_opts.add_option(u'-r', u'--remove', dest=u'removal_substrings', action=u'append', type=u'string', default=[], help=u'Substrings you want to remove from the configuration', metavar=u'fields')
    
    parser.add_option_group(config_opts)
    parser.add_option_group(action_opts)
    
    return parser


def main():
    try:
        subprocess.check_call([u'shinken-daemons-has', u'synchronizer'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError:
        print get_colored_text(u'\n\nThe Synchronizer is not installed on this server ; this tool is not relevant.\n\n', ERROR_COLOR)
        sys.exit(1)
    
    parser = get_options_parser()
    opts, args = parser.parse_args()
    
    has_errors = False
    if len(args) > 0:
        print_error(u'Arguments %s are not allowed' % args)
        has_errors = True
    
    for field in opts.additional_substrings + opts.removal_substrings:
        field = field.decode(u'utf-8')
        if re.search(r'[^0-9a-zA-Z_-]', field):
            print_error(u'The substring [%s] contains forbidden characters' % field)
            has_errors = True
    
    if has_errors:
        parser.print_help()
        sys.exit(1)
    
    if not os.path.exists(opts.synchronizer_config):
        print u'The synchronizer config file "%s" cannot be found' % opts.synchronizer_config
        sys.exit(2)
    
    synchronizer_config = get_conf([opts.synchronizer_config, SYNCHRONIZER_OVERLOAD_FILE])
    substrings_matching_fields_in_file = getattr(synchronizer_config, u'protect_fields__substrings_matching_fields')
    if isinstance(substrings_matching_fields_in_file, basestring):
        substrings_matching_fields_in_file = set([s.strip().upper() for s in substrings_matching_fields_in_file.split(u',') if s.strip()])
    
    asked_to_add = set([f.upper() for f in opts.additional_substrings])
    asked_to_remove = set([f.upper() for f in opts.removal_substrings])
    new_substring_not_in_file = asked_to_add - set(substrings_matching_fields_in_file) - asked_to_remove
    
    bogus_fields = asked_to_add.intersection(asked_to_remove)
    if bogus_fields != set():
        print_error(u'You asked to add and to remove same fields : %s' % u','.join([i for i in bogus_fields]))
        sys.exit(1)
    
    to_remove_in_file = asked_to_remove.intersection(substrings_matching_fields_in_file)
    not_remove_fields = asked_to_remove.difference(to_remove_in_file)
    if not opts.quiet:
        print u'\nObjects data containing one of those substrings will be protected in Synchronizer interface. Enabling encryption will encrypt these fields'
        if substrings_matching_fields_in_file:
            print u'\nSubstrings in current configuration file :'
            print u'%s\n' % u' '.join(sorted(substrings_matching_fields_in_file))
        else:
            print u'\nThere is no substrings in current configuration file.'
        
        if new_substring_not_in_file:
            print u'Substrings will be %s to the configuration : ' % get_colored_text(u'added', OK_COLOR)
            print get_colored_text(u' '.join(sorted(new_substring_not_in_file)), OK_COLOR)
        if to_remove_in_file:
            print u'Substrings will be %s from the configuration : ' % get_colored_text(u'removed', ERROR_COLOR)
            print get_colored_text(u' '.join(sorted(to_remove_in_file)), ERROR_COLOR)
        if not_remove_fields:
            print u'Substrings will not be %s because they are not in the current configuration :' % get_colored_text(u'removed', WARNING_COLOR)
            print get_colored_text(u' '.join(sorted(not_remove_fields)), WARNING_COLOR)
        
        print
    
    new_substring_list = set(substrings_matching_fields_in_file.union(asked_to_add) - to_remove_in_file)
    new_substrings_to_write = u','.join(sorted(new_substring_list))
    
    if substrings_matching_fields_in_file != new_substring_list:
        print u'Substrings which will be written in the new configuration : '
        
        print get_colored_text(u'%s\n' % (u' '.join(sorted(new_substring_list))), YELLOW_COLOR)
    elif asked_to_remove or asked_to_add:
        print u'You are not adding or removing any substring to the configuration.\n'
    
    # will show all the data impacted
    show_data_to_proceed(synchronizer_config, substrings_matching_fields_in_file, sorted(new_substring_not_in_file), sorted(to_remove_in_file), opts.quiet)
    if substrings_matching_fields_in_file != new_substring_list:
        accept_write = yn_choice(u'\nDo you want to save your new configuration in the file "%s"' % SYNCHRONIZER_OVERLOAD_FILE)
        if accept_write:
            msg_error = update_field_in_file(u'protect_fields__substrings_matching_fields', new_substrings_to_write, SYNCHRONIZER_OVERLOAD_FILE)
            if msg_error:
                print u'\n"%s" save failed, %s' % (SYNCHRONIZER_OVERLOAD_FILE, msg_error)
                sys.exit(1)
            elif not opts.quiet:
                print u'\n"%s" save successful' % SYNCHRONIZER_OVERLOAD_FILE
                print get_colored_text(u'\nYou need to restart the synchronizer for the new configuration to take effect.\n\n', WARNING_COLOR)
        else:
            print u'\n Nothing was done.'
    
    sys.exit(0)


if __name__ == u'__main__':
    try:
        main()
    except KeyboardInterrupt:
        print get_colored_text(u'\n\nAborted by user (CTRL-C)', WARNING_COLOR)
