#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" TcEx Framework Profile Generation Module """
import argparse
import colorama as c
import json
import os
import sys
import traceback
from uuid import uuid4
from collections import OrderedDict
from random import randint

import redis

# Python 2 unicode
if sys.version_info[0] == 2:
    reload(sys)
    sys.setdefaultencoding('utf-8')

parser = argparse.ArgumentParser()
parser.add_argument(
    '--action', choices=['create', 'delete', 'replace_validation', 'update'], default='create')
parser.add_argument(
    '--ij', default='install.json', help='The install.json file name (default: install.json).')
parser.add_argument(
    '--data_file', action='store_true', help='Attempt to build data file templates for the profile.')
parser.add_argument(
    '--outdir', default='tcex.d', help='The *base* output directory containing the data/profiles folder.')
parser.add_argument(
    '--outfile', help='The name of the file to write the profile.')
parser.add_argument(
    '--profile_name', help='The profile name to create, delete, or update.', required=True)
parser.add_argument('--redis_hash', help='The redis hash.')
parser.add_argument('--redis_host', default='localhost', help='The redis host.')
parser.add_argument('--redis_port', default='6379', help='The redis port.')
parser.add_argument('--replace_validation', action='store_true', help='Update existing profiles.')

parser.add_argument('--update_only', action='store_true', help='Update existing profiles.')
args, extra_args = parser.parse_known_args()

c.init(autoreset=True, strip=False)


class TcProfile(object):
    """Create profiles for App"""

    def __init__(self, _args):
        """ """
        self._ij = None
        self._redis = None
        self.args = _args
        self.app_path = os.getcwd()
        # self.config = None
        self.data_dir = os.path.join(self.args.outdir, 'data')
        self.profile_dir = os.path.join(self.args.outdir, 'profiles')
        self.profiles = {}

    @staticmethod
    def _handle_error(err, halt=True):
        """Print errors message and optionally exit."""
        print('{}{}{}.'.format(c.Style.BRIGHT, c.Fore.RED, err))
        if halt:
            sys.exit(1)

    @property
    def ij(self):
        """Return the install.json file."""
        if self._ij is None:
            if not os.path.isfile(self.args.ij):
                print('{}{}Configuration file ({}) could not be found.'.format(
                    c.Style.BRIGHT, c.Fore.CYAN, self.args.ij))

            with open(self.args.ij) as fh:
                self._ij = json.load(fh)
        return self._ij

    def load_profiles(self):
        """Return configuration data.

        Load on first access, otherwise return existing data.

        self.profiles = {
            <profile name>: {
                'data': {},
                'ij_filename': <filename>,
                'fqfn': 'tcex.json'
            }
        """
        if not os.path.isfile('tcex.json'):
            msg = 'The tcex.json config file is required.'
            sys.exit(msg)

        # open tcex.json configuration file
        with open('tcex.json', 'r+') as fh:
            data = json.load(fh)
            if data.get('profiles') is not None:
                # no longer supporting profiles in tcex.json
                print('{}{}Migrating profiles from tcex.json to individual files.'.format(
                    c.Style.BRIGHT, c.Fore.YELLOW))

                for profile in data.get('profiles', []):
                    outfile = '{}.json'.format(profile.get('profile_name').replace(' ', '_').lower())
                    self.profile_write(profile, outfile)

                # remove legacy profile key
                del data['profiles']
                data.setdefault('profile_include_dirs', [])
                if self.profile_dir not in data.get('profile_include_dirs'):
                    data['profile_include_dirs'].append(self.profile_dir)
            fh.seek(0)
            fh.write(json.dumps(data, indent=2, sort_keys=True))
            fh.truncate()

        # load includes
        for directory in data.get('profile_include_dirs', []):
            self.load_profile_include(directory)

    def load_profile_include(self, include_directory):
        """Load included configuration files."""
        include_directory = os.path.join(self.app_path, include_directory)
        if not os.path.isdir(include_directory):
            msg = 'Provided include directory does not exist ({}).'.format(include_directory)
            sys.exit(msg)

        for filename in sorted(os.listdir(include_directory)):
            if filename.endswith('.json'):
                fqfn = os.path.join(include_directory, filename)
                self.load_profiles_from_file(fqfn)

    def load_profiles_from_file(self, fqfn):
        """Load profiles from file.

        Args:
            fqfn (str): Fully qualified file name.
        """
        print('Loading profiles from File: {}{}{}'.format(c.Style.BRIGHT, c.Fore.MAGENTA, fqfn))
        with open(fqfn, 'r+') as fh:
            data = json.load(fh)
            for profile in data:
                # force update old profiles
                self.profile_upgrade(profile)
            fh.seek(0)
            fh.write(json.dumps(data, indent=2, sort_keys=True))
            fh.truncate()

        for d in data:
            if d.get('profile_name') in self.profiles:
                print('{}{}Found a duplicate profile name ({}).'.format(
                    c.Style.BRIGHT, c.Fore.RED, d.get('profile_name')))
                sys.exit()
            self.profiles.setdefault(d.get('profile_name'), {
                'data': d,
                'ij_filename': d.get('install_json'),
                'fqfn': fqfn
            })

    def load_install_json(self, filename):
        """Return install.json data.

        Load on first access, otherwise return existing data.
        """
        install_json = None
        load_output = 'Load install.json: {}{}{}{}'.format(
            c.Style.BRIGHT, c.Fore.CYAN, filename, c.Style.RESET_ALL)
        if filename is not None and os.path.isfile(filename):
            with open(filename) as config_data:
                install_json = json.load(config_data)
            load_output += ' {}{}(Loaded){}'.format(
                c.Style.BRIGHT, c.Fore.GREEN, c.Style.RESET_ALL)
        else:
            load_output += ' {}{}(Not Found){}'.format(
                c.Style.BRIGHT, c.Fore.YELLOW, c.Style.RESET_ALL)
            sys.exit(1)
        # display load status
        print(load_output)
        return install_json

    @property
    def profile_args_defaults(self):
        """A Default profile."""
        profile_args = OrderedDict()
        profile_args['api_default_org'] = '$env.API_DEFAULT_ORG'
        profile_args['api_access_id'] = '$env.API_ACCESS_ID'
        profile_args['api_secret_key'] = '$envs.API_SECRET_KEY'
        profile_args['tc_api_path'] = '$env.TC_API_PATH'
        profile_args['tc_log_level'] = 'debug'
        profile_args['tc_log_path'] = 'log'
        profile_args['tc_log_to_api'] = False
        profile_args['tc_out_path'] = 'log'
        profile_args['tc_proxy_external'] = False
        profile_args['tc_proxy_host'] = '$env.TC_PROXY_HOST'
        profile_args['tc_proxy_port'] = '$env.TC_PROXY_PORT'
        profile_args['tc_proxy_password'] = '$envs.TC_PROXY_PASSWORD'
        profile_args['tc_proxy_tc'] = False
        profile_args['tc_proxy_username'] = '$env.TC_PROXY_USERNAME'
        profile_args['tc_temp_path'] = 'log'
        if self.ij.get('runtimeLevel') == 'Playbook':
            profile_args['tc_playbook_db_type'] = 'Redis'
            profile_args['tc_playbook_db_context'] = str(uuid4())
            profile_args['tc_playbook_db_path'] = '$env.DB_PATH'
            profile_args['tc_playbook_db_port'] = '$env.DB_PORT'
            profile_args['tc_playbook_out_variables'] = ''
        return profile_args

    @property
    def profile_args(self):
        """App specific inputs as App args."""
        profile_args = self.profile_args_defaults

        # add App specific args
        for p in self.ij.get('params', []):
            if p.get('type') == 'Boolean':
                profile_args[p.get('name')] = p.get('default', False)
            elif p.get('name') in ['api_access_id', 'api_secret_key']:
                # leave these parameters set to the value defined above
                pass
            else:
                profile_args[p.get('name')] = p.get('default', '')
        return profile_args

    def profile_delete(self):
        """Delete a profile."""
        self.validate_profile_exists()

        profile_data = self.profiles.get(self.args.profile_name)
        fqfn = profile_data.get('fqfn')
        with open(fqfn, 'r+') as fh:
            data = json.load(fh)
            for profile in data:
                if profile.get('profile_name') == self.args.profile_name:
                    data.remove(profile)
            fh.seek(0)
            fh.write(json.dumps(data, indent=2, sort_keys=True))
            fh.truncate()

        if not data:
            # remove empty file
            os.remove(fqfn)

    def profile_create(self):
        """Create a profile."""
        if self.args.profile_name in self.profiles:
            print('{}{}Profile "{}" already exists.'.format(
                c.Style.BRIGHT, c.Fore.RED, self.args.profile_name))
            sys.exit(1)

        print('Building Profile: {}{}{}'.format(c.Style.BRIGHT, c.Fore.CYAN, self.args.profile_name))
        profile = OrderedDict()
        profile['args'] = self.profile_args
        profile['clear'] = []
        profile['description'] = ''
        profile['data_files'] = []
        profile['exit_codes'] = [0]
        profile['groups'] = [os.environ.get('TCEX_GROUP', 'qa-build')]
        profile['install_json'] = self.args.ij
        profile['profile_name'] = self.args.profile_name
        profile['quiet'] = True

        if self.ij.get('runtimeLevel') == 'Playbook':
            validations = self.profile_validations
            profile['validations'] = validations.get('rules')
            profile['args']['tc_playbook_out_variables'] = '{}'.format(
                ','.join(validations.get('outputs')))
        return profile

    @staticmethod
    def profile_upgrade(profile):
        """Update an existing profile with new parameters or remove deprecated parameters."""
        # warn about missing install_json parameter
        if profile.get('install_json') is None:
            print('{}{}Missing install_json parameter for profile {}.'.format(
                c.Style.BRIGHT, c.Fore.YELLOW, profile.get('profile_name')))

        # cleanup
        if (profile.get('install_json') is not None and profile.get('script') is not None):
            print('{}{}Removing deprecated "script" parameter.'.format(
                c.Style.BRIGHT, c.Fore.YELLOW))
            profile.pop('script')

        # update old profiles with new "data_type" field.
        for validation in profile.get('validations', []):
            if validation.get('data_type') is None:
                print('{}{}Adding new "data_type" parameter.'.format(c.Style.BRIGHT, c.Fore.YELLOW))
                validation['data_type'] = 'redis'

    @property
    def profile_validations(self):
        """Profile validation rules."""
        validations = {
            'rules': [],
            'outputs': []
        }

        job_id = randint(1000, 9999)
        for o in self.ij.get('playbook', {}).get('outputVariables', []):
            variable = '#App:{}:{}!{}'.format(job_id, o.get('name'), o.get('type'))
            validations['outputs'].append(variable)

            # null check
            od = OrderedDict()
            if o.get('type').endswith('Array'):
                od['data'] = [None, []]
                od['data_type'] = 'redis'
                od['operator'] = 'ni'
            else:
                od['data'] = None
                od['data_type'] = 'redis'
                od['operator'] = 'ne'
            od['variable'] = variable
            validations['rules'].append(od)

            # type check
            od = OrderedDict()
            if o.get('type').endswith('Array'):
                od['data'] = 'array'
                od['data_type'] = 'redis'
                od['operator'] = 'it'
            elif o.get('type').endswith('Binary'):
                od['data'] = 'binary'
                od['data_type'] = 'redis'
                od['operator'] = 'it'
            elif o.get('type').endswith('Entity') or o.get('type') == 'KeyValue':
                od['data'] = 'entity'
                od['data_type'] = 'redis'
                od['operator'] = 'it'
            else:
                od['data'] = 'string'
                od['data_type'] = 'redis'
                od['operator'] = 'it'
            od['variable'] = variable
            validations['rules'].append(od)
        return validations

    def profile_write(self, profile, outfile=None):
        """Write the profile to the output directory."""
        if not os.path.isdir(self.profile_dir):
            os.makedirs(self.profile_dir)
        if not os.path.isdir(self.data_dir):
            os.makedirs(self.data_dir)
        # fully qualified output file
        if outfile is None:
            outfile = '{}.json'.format(profile.get('profile_name').replace(' ', '_').lower())
        fqpn = os.path.join(self.profile_dir, outfile)

        if os.path.isfile(fqpn):
            # append
            print('Append to File: {}{}{}'.format(c.Style.BRIGHT, c.Fore.CYAN, fqpn))
            with open(fqpn, 'r+') as fh:
                try:
                    data = json.load(fh, object_pairs_hook=OrderedDict)
                except ValueError as e:
                    print('{}{}Can not parse JSON data ({}).'.format(c.Style.BRIGHT, c.Fore.RED, e))
                    sys.exit(1)

                data.append(profile)
                fh.seek(0)
                fh.write(json.dumps(data, indent=2, sort_keys=True))
                fh.truncate()
        else:
            # create
            print('Create File: {}{}{}'.format(c.Style.BRIGHT, c.Fore.CYAN, fqpn))
            with open(fqpn, 'w') as fh:
                data = [profile]
                fh.write(json.dumps(data, indent=2, sort_keys=True))

    @property
    def redis(self):
        if self._redis is None:
            self._redis = redis.StrictRedis(host=self.args.redis_host, port=self.args.redis_port)
        return self._redis

    def replace_validation(self):
        """ """
        self.validate_profile_exists()
        profile_data = self.profiles.get(self.args.profile_name)

        # check redis
        if redis is None:
            self._handle_error('Could not get connection to Redis')

        # load hash
        redis_hash = profile_data.get('data', {}).get('args', {}).get('tc_playbook_db_context')
        if redis_hash is None:
            self._handle_error('Could not find redis hash (db context).')

        # load data
        data = self.redis.hgetall(redis_hash)
        if data is None:
            self._handle_error('Could not load data for hash {}.'.format(redis_hash))
        validations = {
            'rules': [],
            'outputs': []
        }
        for v, d in data.items():
            variable = v.decode('utf-8')
            # data = d.decode('utf-8')
            data = json.loads(d.decode('utf-8'))
            # if data == 'null':
            if data is None:
                continue
            validations['outputs'].append(variable)

            # null check
            od = OrderedDict()
            od['data'] = data
            od['data_type'] = 'redis'
            od['operator'] = 'eq'
            od['variable'] = variable
            # if variable.endswith('Array'):
            #     od['data'] = json.loads(data)
            #     od['data_type'] = 'redis'
            #     od['operator'] = 'eq'
            #     od['variable'] = variable
            # elif variable.endswith('Binary'):
            #     od['data'] = json.loads(data)
            #     od['data_type'] = 'redis'
            #     od['operator'] = 'eq'
            #     od['variable'] = variable
            # elif variable.endswith('String'):
            #     od['data'] = json.loads(data)
            #     od['data_type'] = 'redis'
            #     od['operator'] = 'eq'
            #     od['variable'] = variable
            validations['rules'].append(od)

        fqfn = profile_data.get('fqfn')
        with open(fqfn, 'r+') as fh:
            data = json.load(fh)
            for profile in data:
                if profile.get('profile_name') == self.args.profile_name:
                    profile['validations'] = validations.get('rules')
                    profile['args']['tc_playbook_out_variables'] = ','.join(validations.get('outputs'))
            fh.seek(0)
            fh.write(json.dumps(data, indent=2, sort_keys=True))
            fh.truncate()

    def validate_profile_exists(self):
        """Validate the provided profiles name exists."""
        if not self.args.profile_name in self.profiles:
            self._handle_error('Could not find profile "{}"'.format(self.args.profile_name))




if __name__ == '__main__':
    try:
        tcp = TcProfile(args)

        # load all profiles
        tcp.load_profiles()

        if args.action == 'create':
            profile = tcp.profile_create()
            tcp.profile_write(profile, args.outfile)
        elif args.action == 'delete':
            tcp.profile_delete()
        elif args.action == 'replace_validation':
            tcp.replace_validation()
        # elif args.action == 'update':
        #     tcp.profile_update()


        # exit
        sys.exit()
    except Exception as e:
        print('{}{}{}'.format(c.Style.BRIGHT, c.Fore.RED, traceback.format_exc()))
        sys.exit(1)