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

import copy
import random
import string
import sys
import ctypes
import time
import json
import os
import multiprocessing
import yaml
import hashlib
import re
import math
from nettacker.core.load_modules import load_all_languages
from nettacker.core.time import now
from nettacker.core.color import color


def process_conditions(
        event,
        module_name,
        target,
        scan_unique_id,
        options,
        response,
        process_number,
        module_thread_number,
        total_module_thread_number,
        request_number_counter,
        total_number_of_requests
):
    from nettacker.core.alert import (success_event_info,
                            verbose_info,
                            messages)

    if 'save_to_temp_events_only' in event.get('response', ''):
        from nettacker.database.db import submit_temp_logs_to_db
        submit_temp_logs_to_db(
            {
                "date": now(model=None),
                "target": target,
                "module_name": module_name,
                "scan_unique_id": scan_unique_id,
                "event_name": event['response']['save_to_temp_events_only'],
                "port": event.get('ports', ''),
                "event": event,
                "data": response
            }
        )
    if event['response']['conditions_results'] and 'save_to_temp_events_only' not in event.get('response', ''):
        from nettacker.database.db import submit_logs_to_db

        # remove sensitive information before submitting to db
        from nettacker.config import nettacker_api_config
        options = copy.deepcopy(options)
        for key in nettacker_api_config():
            try:
                del options[key]
            except Exception:
                continue
        del event['response']['conditions']
        del event['response']['condition_type']
        if 'log' in event['response']:
            del event['response']['log']
        event_request_keys = copy.deepcopy(event)
        del event_request_keys['response']
        submit_logs_to_db(
            {
                "date": now(model=None),
                "target": target,
                "module_name": module_name,
                "scan_unique_id": scan_unique_id,
                "port": event.get('ports') or event.get('port') or (
                    event.get('url').split(':')[2].split('/')[0] if
                    type(event.get('url')) == str and len(event.get('url').split(':')) >= 3 and
                    event.get('url').split(':')[2].split('/')[0].isdigit() else ""
                ),
                "event": " ".join(
                    yaml.dump(event_request_keys).split()
                ) + " conditions: " + " ".join(
                    yaml.dump(event['response']['conditions_results']).split()
                ),
                "json_event": event
            }
        )
        log_list = merge_logs_to_list(event['response']['conditions_results'])
        if log_list:
            success_event_info(
                messages("send_success_event_from_module").format(
                    process_number,
                    module_name,
                    target,
                    module_thread_number,
                    total_module_thread_number,
                    request_number_counter,
                    total_number_of_requests,
                    " ",
                    filter_large_content(
                        "\n".join(
                            [
                                color('purple') + key + color('reset')
                                for key in log_list
                            ]
                        ),
                        filter_rate=100000
                    )
                )
            )
        else:
            success_event_info(
                messages("send_success_event_from_module").format(
                    process_number,
                    module_name,
                    target,
                    module_thread_number,
                    total_module_thread_number,
                    request_number_counter,
                    total_number_of_requests,
                    " ".join(
                        [
                            color('yellow') + key + color('reset') if ':' in key
                            else color('green') + key + color('reset')
                            for key in yaml.dump(event_request_keys).split()
                        ]
                    ),
                    filter_large_content(
                        "conditions: " + " ".join(
                            [
                                color('purple') + key + color('reset') if ':' in key
                                else color('green') + key + color('reset')
                                for key in yaml.dump(event['response']['conditions_results']).split()
                            ]
                        ),
                        filter_rate=150
                    )
                )
            )
        verbose_info(
            json.dumps(event)
        )
        return True
    else:
        del event['response']['conditions']
        verbose_info(
            messages("send_unsuccess_event_from_module").format(
                process_number,
                module_name,
                target,
                module_thread_number,
                total_module_thread_number,
                request_number_counter,
                total_number_of_requests
            )
        )
        verbose_info(
            json.dumps(event)
        )
        return 'save_to_temp_events_only' in event['response']


def filter_large_content(content, filter_rate=150):
    from nettacker.core.alert import messages
    if len(content) <= filter_rate:
        return content
    else:
        filter_rate -= 1
        filter_index = filter_rate
        for char in content[filter_rate:]:
            if char == ' ':
                return content[0:filter_index] + messages('filtered_content')
            else:
                filter_index += 1
        return content


def get_dependent_results_from_database(target, module_name, scan_unique_id, event_names):
    from nettacker.database.db import find_temp_events
    events = []
    for event_name in event_names.split(','):
        while True:
            event = find_temp_events(target, module_name, scan_unique_id, event_name)
            if event:
                events.append(json.loads(event.event)['response']['conditions_results'])
                break
            time.sleep(0.1)
    return events


def find_and_replace_dependent_values(sub_step, dependent_on_temp_event):
    if type(sub_step) == dict:
        for key in copy.deepcopy(sub_step):
            if type(sub_step[key]) not in [str, float, int, bytes]:
                sub_step[key] = find_and_replace_dependent_values(
                    copy.deepcopy(sub_step[key]), dependent_on_temp_event
                )
            else:
                if type(sub_step[key]) == str:
                    if 'dependent_on_temp_event' in sub_step[key]:
                        globals().update(locals())
                        generate_new_step = copy.deepcopy(sub_step[key])
                        key_name = re.findall(
                            re.compile("dependent_on_temp_event\\[\\S+\\]\\['\\S+\\]\\[\\S+\\]"),
                            generate_new_step
                        )[0]
                        try:
                            key_value = eval(key_name)
                        except Exception:
                            key_value = "error"
                        sub_step[key] = sub_step[key].replace(key_name, key_value)
    if type(sub_step) == list:
        value_index = 0
        for value in copy.deepcopy(sub_step):
            if type(sub_step[value_index]) not in [str, float, int, bytes]:
                sub_step[key] = find_and_replace_dependent_values(
                    copy.deepcopy(sub_step[value_index]), dependent_on_temp_event
                )
            else:
                if type(sub_step[value_index]) == str:
                    if 'dependent_on_temp_event' in sub_step[value_index]:
                        globals().update(locals())
                        generate_new_step = copy.deepcopy(sub_step[key])
                        key_name = re.findall(
                            re.compile("dependent_on_temp_event\\['\\S+\\]\\[\\S+\\]"),
                            generate_new_step
                        )[0]
                        try:
                            key_value = eval(key_name)
                        except Exception:
                            key_value = "error"
                        sub_step[value_index] = sub_step[value_index].replace(key_name, key_value)
            value_index += 1
    return sub_step


def replace_dependent_values(sub_step, dependent_on_temp_event):
    return find_and_replace_dependent_values(sub_step, dependent_on_temp_event)


def replace_dependent_response(log, result):
    response_dependent = result
    if str(log):
        key_name = re.findall(
            re.compile("response_dependent\\['\\S+\\]"),
            log
        )
        for i in key_name:
            try:
                key_value = eval(i)
            except Exception:
                key_value = "response dependent error"
            log = log.replace(i, " ".join(key_value))
        return log


def merge_logs_to_list(result, log_list=[]):
    if type(result) == dict:
        for i in result:
            if 'log' == i:
                log_list.append(result['log'])
            else:
                merge_logs_to_list(result[i], log_list)
    return list(set(log_list))


def reverse_and_regex_condition(regex, reverse):
    if regex:
        if reverse:
            return []
        return list(set(regex))
    else:
        if reverse:
            return True
        return []


def select_maximum_cpu_core(mode):
    cpu_count = multiprocessing.cpu_count()

    if cpu_count - 1 == 0:
        return 1

    mode_core_mapping = {
        "maximum": cpu_count - 1,
        "high": cpu_count / 2,
        "normal": cpu_count / 4,
        "low": cpu_count / 8,
    }
    rounded = math.ceil if mode == "high" else math.floor

    return int(max((rounded(mode_core_mapping.get(mode, 1)), 1)))

def wait_for_threads_to_finish(threads, maximum=None, terminable=False, sub_process=False):
    while threads:
        try:
            dead_threads = []
            for thread in threads:
                if not thread.is_alive():
                    dead_threads.append(thread)
            if dead_threads:
                for thread in dead_threads:
                    threads.remove(thread)
                dead_threads = []
            if maximum and len(threads) < maximum:
                break
            time.sleep(0.01)
        except KeyboardInterrupt:
            if terminable:
                for thread in threads:
                    terminate_thread(thread)
            if sub_process:
                for thread in threads:
                    thread.kill()
            return False
    return True


def terminate_thread(thread, verbose=True):
    """
    kill a thread https://stackoverflow.com/a/15274929
    Args:
        thread: an alive thread
        verbose: verbose mode/boolean
    Returns:
        True/None
    """
    from nettacker.core.alert import info
    if verbose:
        info("killing {0}".format(thread.name))
    if not thread.is_alive():
        return

    exc = ctypes.py_object(SystemExit)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
        ctypes.c_long(thread.ident),
        exc
    )
    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")
    return True


def find_args_value(args_name):
    try:
        return sys.argv[sys.argv.index(args_name) + 1]
    except Exception:
        return None


def application_language():
    from nettacker.config import nettacker_global_config
    nettacker_global_configuration = nettacker_global_config()
    if "-L" in sys.argv:
        language = find_args_value('-L') or 'en'
    elif "--language" in sys.argv:
        language = find_args_value('--language') or 'en'
    else:
        language = nettacker_global_configuration['nettacker_user_application_config']['language']
    if language not in load_all_languages():
        language = 'en'
    return language


def generate_random_token(length=10):
    return "".join(
        random.choice(string.ascii_lowercase) for _ in range(length)
    )


def re_address_repeaters_key_name(key_name):
    return "".join(['[\'' + _key + '\']' for _key in key_name.split('/')[:-1]])


def generate_new_sub_steps(sub_steps, data_matrix, arrays):
    original_sub_steps = copy.deepcopy(sub_steps)
    steps_array = []
    for array in data_matrix:
        array_name_position = 0
        for array_name in arrays:
            for sub_step in sub_steps:
                exec(
                    "original_sub_steps{key_name} = {matrix_value}".format(
                        key_name=re_address_repeaters_key_name(array_name),
                        matrix_value='"' + str(array[array_name_position]) + '"' if type(
                            array[array_name_position]) == int or type(array[array_name_position]) == str else array[
                            array_name_position]
                    )
                )
            array_name_position += 1
        steps_array.append(copy.deepcopy(original_sub_steps))
    return steps_array


def find_repeaters(sub_content, root, arrays):
    if type(sub_content) == dict and 'nettacker_fuzzer' not in sub_content:
        temprory_content = copy.deepcopy(sub_content)
        original_root = root
        for key in sub_content:
            root = original_root
            root += key + '/'
            temprory_content[key], _root, arrays = find_repeaters(sub_content[key], root, arrays)
        sub_content = copy.deepcopy(temprory_content)
        root = original_root
    if (type(sub_content) not in [bool, int, float]) and (
            type(sub_content) == list or 'nettacker_fuzzer' in sub_content):
        arrays[root] = sub_content
    return (sub_content, root, arrays) if root != '' else arrays


def find_and_replace_configuration_keys(module_content, module_inputs):
    if type(module_content) == dict:
        for key in copy.deepcopy(module_content):
            if key in module_inputs:
                if module_inputs[key]:
                    module_content[key] = module_inputs[key]
            elif type(module_content[key]) in [dict, list]:
                module_content[key] = find_and_replace_configuration_keys(module_content[key], module_inputs)
    elif type(module_content) == list:
        array_index = 0
        for key in copy.deepcopy(module_content):
            module_content[array_index] = find_and_replace_configuration_keys(key, module_inputs)
            array_index += 1
    else:
        return module_content
    return module_content


class value_to_class:
    def __init__(self, value):
        self.value = value


def class_to_value(arrays):
    original_arrays = copy.deepcopy(arrays)
    array_index = 0
    for array in arrays:
        value_index = 0
        for value in array:
            if type(value) == value_to_class:
                original_arrays[array_index][value_index] = value.value
            value_index += 1
        array_index += 1
    return original_arrays


def generate_and_replace_md5(content):
    # todo: make it betetr and document it
    md5_content = content.split('NETTACKER_MD5_GENERATOR_START')[1].split('NETTACKER_MD5_GENERATOR_STOP')[0]
    md5_content_backup = md5_content
    if type(md5_content) == str:
        md5_content = md5_content.encode()
    md5_hash = hashlib.md5(md5_content).hexdigest()
    return content.replace(
        'NETTACKER_MD5_GENERATOR_START' + md5_content_backup + 'NETTACKER_MD5_GENERATOR_STOP',
        md5_hash
    )


def arrays_to_matrix(arrays):
    import numpy
    return numpy.array(
        numpy.meshgrid(*[
            arrays[array_name] for array_name in arrays
        ])
    ).T.reshape(
        -1,
        len(arrays.keys())
    ).tolist()


def string_to_bytes(string):
    return string.encode()


def fuzzer_function_read_file_as_array(filename):
    from nettacker.config import nettacker_paths
    return open(
        os.path.join(
            nettacker_paths()['payloads_path'],
            filename
        )
    ).read().split('\n')


def apply_data_functions(data):
    original_data = copy.deepcopy(data)
    function_results = {}
    globals().update(locals())
    for data_name in data:
        if type(data[data_name]) == str and data[data_name].startswith('fuzzer_function'):
            exec(
                "fuzzer_function = {fuzzer_function}".format(
                    fuzzer_function=data[data_name]
                ),
                globals(),
                function_results
            )
            original_data[data_name] = function_results['fuzzer_function']
    return original_data


def nettacker_fuzzer_repeater_perform(arrays):
    original_arrays = copy.deepcopy(arrays)
    for array_name in arrays:
        if 'nettacker_fuzzer' in arrays[array_name]:
            data = arrays[array_name]['nettacker_fuzzer']['data']
            data_matrix = arrays_to_matrix(apply_data_functions(data))
            prefix = arrays[array_name]['nettacker_fuzzer']['prefix']
            input_format = arrays[array_name]['nettacker_fuzzer']['input_format']
            interceptors = copy.deepcopy(arrays[array_name]['nettacker_fuzzer']['interceptors'])
            if interceptors:
                interceptors = interceptors.split(',')
            suffix = arrays[array_name]['nettacker_fuzzer']['suffix']
            processed_array = []
            for sub_data in data_matrix:
                formatted_data = {}
                index_input = 0
                for value in sub_data:
                    formatted_data[list(data.keys())[index_input]] = value
                    index_input += 1
                interceptors_function = ''
                interceptors_function_processed = ''
                if interceptors:
                    interceptors_function += 'interceptors_function_processed = '
                    for interceptor in interceptors[::-1]:
                        interceptors_function += '{interceptor}('.format(interceptor=interceptor)
                    interceptors_function += 'input_format.format(**formatted_data)' + str(
                        ')' * interceptors_function.count('('))
                    expected_variables = {}
                    globals().update(locals())
                    exec(interceptors_function, globals(), expected_variables)
                    interceptors_function_processed = expected_variables['interceptors_function_processed']
                else:
                    interceptors_function_processed = input_format.format(**formatted_data)
                processed_sub_data = interceptors_function_processed
                if prefix:
                    processed_sub_data = prefix + processed_sub_data
                if suffix:
                    processed_sub_data = processed_sub_data + suffix
                processed_array.append(copy.deepcopy(processed_sub_data))
            original_arrays[array_name] = processed_array
    return original_arrays


def expand_module_steps(content):
    return [expand_protocol(x) for x in copy.deepcopy(content)]


def expand_protocol(protocol):
    protocol['steps'] = [expand_step(x) for x in protocol['steps']]
    return protocol


def expand_step(step):
    arrays = nettacker_fuzzer_repeater_perform(find_repeaters(step, '', {}))
    if arrays:
        return generate_new_sub_steps(step, class_to_value(arrays_to_matrix(arrays)), arrays)
    else:
        # Minimum 1 step in array
        return [step]


def sort_dictionary(dictionary):
    etc_flag = '...' in dictionary
    if etc_flag:
        del dictionary['...']
    sorted_dictionary = {}
    for key in sorted(dictionary):
        sorted_dictionary[key] = dictionary[key]
    if etc_flag:
        sorted_dictionary['...'] = {}
    return sorted_dictionary
