import dateparser
import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *  # noqa
from typing import Callable, Dict, List, Any, Union, Tuple

# Disable insecure warnings
requests.packages.urllib3.disable_warnings()  # pylint: disable=no-member

''' CONSTANTS '''
INTEGRATION_NAME = 'Carbon Black EDR'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'

''' PARSING PROCESS EVENT COMPLEX FIELDS CLASS'''


class ProcessEventDetail:
    """
    This class representing the Process Event Details as found here:
    https://developer.carbonblack.com/reference/enterprise-response/6.3/rest-api/#process-event-details
    Each sub-class representing a different piped-versioned field, and support the format method.
    """

    def __init__(self, piped_version: Union[str, list], fields):
        self.fields = []
        if not isinstance(piped_version, list):
            piped_version = [piped_version]
        for entry in piped_version:
            data = entry.split('|')

            # zip works when number of values is not equal, which can result in incorrect data.
            if len(data) != len(fields):
                demisto.debug(f'{INTEGRATION_NAME} - Missing details. Ignoring entry: {entry}.')

            self.fields.append(dict(zip(fields, data)))

    def format(self):
        return self.fields


class filemod_complete(ProcessEventDetail):
    FIELDS = ['operation_type', 'event_time', 'file_path', 'md5_after_last_write',
              'file_type', 'flagged_as_potential_tamper_attempt']
    OPERATION_TYPE = {'1': 'Created the file',
                      '2': 'First wrote to the file',
                      '4': 'Deleted the file',
                      '8': 'Last wrote to the file'}
    FILE_TYPE = {'1': 'PE',
                 '2': 'Elf',
                 '3': 'UniversalBin',
                 '8': 'EICAR',
                 '16': 'OfficeLegacy',
                 '17': 'OfficeOpenXml',
                 '48': 'Pdf',
                 '64': 'ArchivePkzip',
                 '65': 'ArchiveLzh',
                 '66': 'ArchiveLzw',
                 '67': 'ArchiveRar',
                 '68': 'ArchiveTar',
                 '69': 'Archive7zip'}

    def __init__(self, piped_version):
        super().__init__(piped_version, self.FIELDS)

    def format(self):
        for entry in self.fields:
            entry['operation_type'] = self.OPERATION_TYPE.get(entry.get('operation_type', ''), '')
            entry['file_type'] = self.FILE_TYPE.get(entry.get('file_type', ''), '')
        return self.fields


class modload_complete(ProcessEventDetail):
    FIELDS = ['event_time', 'loaded_module_md5', 'loaded_module_full_path']

    def __init__(self, piped_version):
        super().__init__(piped_version, self.FIELDS)

    def format(self):
        return self.fields


class regmod_complete(ProcessEventDetail):
    FIELDS = ['operation_type', 'event_time', 'registry_key_path']
    OPERATION_TYPE = {'1': 'Created the file',
                      '2': 'First wrote to the file',
                      '4': 'Deleted the file',
                      '8': 'Last wrote to the file'}

    def __init__(self, piped_version):
        super().__init__(piped_version, self.FIELDS)

    def format(self):
        for entry in self.fields:
            entry['operation_type'] = self.OPERATION_TYPE.get(entry.get('operation_type', ''), '')
        return self.fields


class crossproc_complete(ProcessEventDetail):
    FIELDS = ['cross-process_access_type', 'event_time', 'targeted_process_unique_id',
              'targeted_process_md5', 'targeted_process_path', 'ProcessOpen_sub-type',
              'requested_access_priviledges', 'flagged_as_potential_tamper_attempt']
    SUB_TYPES = {'1': 'handle open to process', '2': 'handle open to thread in process'}

    def __init__(self, piped_version):
        super().__init__(piped_version, self.FIELDS)

    def format(self):
        for entry in self.fields:
            entry['ProcessOpen_sub-type'] = self.SUB_TYPES.get(entry.get('ProcessOpen_sub-type', ''), '')
        return self.fields


''' CLIENT CLASS '''


class Client(BaseClient):
    def __init__(self, base_url: str, apitoken: str, use_ssl: bool, use_proxy: bool):
        headers = {'X-Auth-Token': apitoken, 'Accept': 'application/json', 'Content-Type': 'application/json'}
        super().__init__(base_url, headers=headers, verify=use_ssl, proxy=use_proxy)

    def http_request(self, url: str, method: str, params: dict = None, json_data: dict = None,
                     ok_codes: tuple = (200, 204), resp_type: str = 'json') -> dict:
        """
        initiates a http request to openphish
        """
        data = self._http_request(
            method=method,
            ok_codes=ok_codes,
            url_suffix=url,
            params=params,
            resp_type=resp_type,
            return_empty_response=True,
            json_data=json_data,
            timeout=30,
        )
        return data

    def get_sensors(self, id: str = None, hostname: str = None, ipaddr: str = None,  # noqa: F841
                    groupid: str = None, inactive_filter_days: str = None,  # noqa: F841
                    limit: Union[int, str] = None) -> Tuple[int, List[dict]]:
        url = f'/v1/sensor/{id}' if id else '/v1/sensor'
        query_params = assign_params(
            ip=ipaddr,
            hostname=hostname,
            groupid=groupid,
            inactive_filter_days=inactive_filter_days
        )
        res = self.http_request(url=url, method='GET', params=query_params, ok_codes=(200, 204))

        # When querying specific sensor without filters, the api returns dictionary instead of list.
        return len(res), res[:arg_to_number(limit, 'limit')] if isinstance(res, list) else [res]

    def get_alerts(self, status: str = None, username: str = None, feedname: str = None,
                   hostname: str = None, report: str = None, sort: str = None, query: str = None,
                   facet: str = None, limit: Union[str, int] = None, start: str = None,
                   allow_empty_params: bool = False) -> dict:

        query_params = assign_params(
            status=status,
            username=username,
            feedname=feedname,
            hostname=hostname,
            report=report,
            query=query
        )
        query_string = _create_query_string(query_params, allow_empty_params=allow_empty_params)
        params = assign_params(q=query_string,
                               rows=arg_to_number(limit, 'limit'),
                               start=start,
                               sort=sort,
                               facet=facet,
                               )

        return self.http_request(url='/v2/alert', method='GET', params=params)

    def get_binaries(self, md5: str = None, product_name: str = None, signed: str = None,  # noqa: F841
                     group: str = None, hostname: str = None, digsig_publisher: str = None,  # noqa: F841
                     company_name: str = None, sort: str = None,
                     observed_filename: str = None, query: str = None, facet: str = None,
                     limit: str = None, start: str = None) -> dict:

        query_params = assign_params(
            md5=md5,
            product_name=product_name,
            signed=signed,
            group=group,
            hostname=hostname,
            digsig_publisher=digsig_publisher,
            company_name=company_name,
            observed_filename=observed_filename,
            query=query
        )
        query_string = _create_query_string(query_params)
        params = assign_params(q=query_string,
                               rows=arg_to_number(limit, 'limit'),
                               start=start,
                               sort=sort,
                               facet=facet,
                               )
        return self.http_request(url='/v1/binary', method='GET', params=params)

    def get_processes(self, process_name: str = None, group: str = None, hostname: str = None,
                      parent_name: str = None, process_path: str = None, md5: str = None,
                      query: str = None, group_by: str = None, sort: str = None, facet: str = None,
                      facet_field: str = None, limit: str = None, start: str = None, allow_empty: bool = False):
        query_fields = ['process_name', 'group', 'hostname', 'parent_name', 'process_path', 'md5', 'query']
        local_params = locals()
        query_params = assign_params(
            process_name=process_name,
            parent_name=parent_name,
            process_path=process_path,
            group=group,
            hostname=hostname,
            md5=md5,
            query=query
        )
        query_string = _create_query_string(query_params, allow_empty)
        params = assign_params(q=query_string,
                               rows=arg_to_number(limit, 'limit'),
                               start=start,
                               sort=sort,
                               facet=facet,
                               )
        if facet_field:
            params['facet.field'] = facet_field
        if group_by:
            params['cb.group'] = group_by
        return self.http_request(url='/v1/process', method='GET', params=params)

    def get_formatted_ProcessEventDetail(self, process_json: dict):
        complex_fields = {'filemod_complete': filemod_complete, 'modload_complete': modload_complete,
                          'regmod_complete': regmod_complete, 'crossproc_complete': crossproc_complete}
        formatted_json = {}
        for field in process_json:
            if field in complex_fields:
                # creating the relevant field object and formatting it.
                field_object: ProcessEventDetail = complex_fields[field](process_json.get(field))
                formatted_json[field] = field_object.format()
            else:
                formatted_json[field] = process_json.get(field)

        return formatted_json


''' HELPER FUNCTIONS '''


def _create_query_string(params: dict, allow_empty_params: bool = False) -> str:
    """
    Creating a cb query from params according to https://developer.carbonblack.com/resources/query_overview.pdf.
    if 'query' in params, it overrides the other params.
    allow_empty_params is used for testing and not production as it would overload the context.
    """
    # If user provided both params and query, it means he is not experienced, and might expect different results,
    # therefore we decided to prohibit the use of both in search commands.
    if 'query' in params and len(params) > 1:
        raise DemistoException(f'{INTEGRATION_NAME} - Searching with both query and other filters is not allowed. '
                               f'Please provide either a search query or one of the possible filters.')
    elif 'query' in params:
        return params['query']

    current_query = [f"{query_field}:{params[query_field]}" for query_field in params]
    current_query = ' AND '.join(current_query)

    if not current_query and not allow_empty_params:
        raise DemistoException(f'{INTEGRATION_NAME} - Search without any filter is not permitted.')

    return current_query


def _add_to_current_query(current_query: str = '', params: dict = None) -> str:
    new_query = ''
    if not params:
        return current_query
    if current_query:
        new_query += f'({current_query}) AND '
    current_query_params = [f"{query_field}:{params[query_field]}" for query_field in params]
    new_query += ' AND '.join(current_query_params)
    return new_query


def _get_sensor_isolation_change_body(client: Client, sensor_id: str, new_isolation: bool) -> dict:
    sensor_data = client.get_sensors(sensor_id)[1][0]  # returns (length, [sensor_data])
    new_sensor_data = {
        'network_isolation_enabled': new_isolation,
        'group_id': sensor_data.get('group_id')
    }

    return new_sensor_data


def _parse_field(raw_field: str, sep: str = ',', index_after_split: int = 0, chars_to_remove: str = '') -> str:
    '''
    This function allows getting a specific complex sub-string. "example,example2|" -> 'example2'
    '''
    if not raw_field:
        demisto.debug(f'{INTEGRATION_NAME} - Got empty raw field to parse.')
        return ''
    try:
        new_field = raw_field.split(sep)[index_after_split]
    except IndexError:
        demisto.error(f'{INTEGRATION_NAME} - raw: {raw_field}, split by {sep} has no index {index_after_split}')
        return ''
    chars_to_remove = set(chars_to_remove)
    for char in chars_to_remove:
        new_field = new_field.replace(char, '')
    return new_field


def _get_isolation_status_field(isolation_activated: bool, is_isolated: bool) -> str:
    # Logic for isolation can be found in:
    # https://developer.carbonblack.com/reference/enterprise-response/6.3/rest-api/#sensorsendpoints
    if isolation_activated:
        sensor_isolation_status = 'Yes' if is_isolated else 'Pending isolation'
    else:
        sensor_isolation_status = 'Pending unisolation' if is_isolated else 'No'

    return sensor_isolation_status


''' COMMAND FUNCTIONS '''


def unquarantine_device_command(client: Client, sensor_id: str) -> CommandResults:
    url = f'/v1/sensor/{sensor_id}'
    res = client.http_request(url=url, method='PUT',
                              json_data=_get_sensor_isolation_change_body(client, sensor_id, False))
    if not res:
        raise Exception(f'{INTEGRATION_NAME} - could not un-isolate sensor {sensor_id}')
    return CommandResults(readable_output='Sensor was un-isolated successfully.')


def quarantine_device_command(client: Client, sensor_id: str) -> CommandResults:
    url = f'/v1/sensor/{sensor_id}'
    res = client.http_request(url=url, method='PUT',
                              json_data=_get_sensor_isolation_change_body(client, sensor_id, True))
    if not res:
        raise Exception(f'{INTEGRATION_NAME} - could not isolate sensor {sensor_id}')
    return CommandResults(readable_output='Sensor was isolated successfully.')


def sensors_list_command(client: Client, id: str = None, hostname: str = None, ip: str = None,
                         group_id: str = None, inactive_filter_days: str = None, limit: int = None) -> CommandResults:
    try:
        total_num_of_sensors, res = client.get_sensors(id, hostname, ip, group_id, inactive_filter_days, limit)

        human_readable_data = []
        for sensor_data in res:
            human_readable_data.append({
                'Computer Name': sensor_data.get('computer_name'),
                'Status': sensor_data.get('status'),
                'OS Version': sensor_data.get('os_type'),
                'Node Id': sensor_data.get('node_id'),
                'Sensor Version': sensor_data.get('build_version_string'),
                'Sensor Id': sensor_data.get('id'),
                'IP Address/MAC Info': _parse_field(sensor_data.get('network_adapters', ''), index_after_split=1,
                                                    chars_to_remove='|'),
                'Group ID': sensor_data.get('group_id'),
                'Power State': sensor_data.get('power_state'),
                'Health Score': sensor_data.get('sensor_health_status'),
                'Is Isolating': sensor_data.get('is_isolating')

            })

        md = tableToMarkdown(f'{INTEGRATION_NAME} - Sensors', human_readable_data, removeNull=True, headers=[
            'Sensor Id', 'Computer Name', 'Status', 'Power State', 'Group ID', 'OS Version', 'Health Score',
            'Is Isolating', 'Node Id', 'Sensor Version', 'IP Address/MAC Info'])
        md += f"\nShowing {len(res)} out of {total_num_of_sensors} results."
        return CommandResults(outputs=res, outputs_prefix='CarbonBlackEDR.Sensor', outputs_key_field='id',
                              readable_output=md, raw_response=res)
    except DemistoException as e:
        if '404' in e.message:
            raise Exception(f'{INTEGRATION_NAME} - The sensor {id} could not be found. '
                            f'Please try using a different sensor.')
        else:
            raise Exception(f'{INTEGRATION_NAME} - Error connecting to API. Error: {e.message}')


def watchlist_delete_command(client: Client, id: str) -> CommandResults:
    res = client.http_request(url=f'/v1/watchlist/{id}', method='DELETE')
    # res contains whether the task successful.
    return CommandResults(readable_output=res.get('result'))


def watchlist_update_command(client: Client, id: str, search_query: str, description: str,
                             enabled: bool = None) -> CommandResults:
    params = assign_params(enabled=enabled, search_query=search_query, description=description)
    res = client.http_request(url=f'/v1/watchlist/{id}', method='PUT', json_data=params)

    # res contains whether the task successful.
    return CommandResults(readable_output=res.get('result'))


def watchlist_create_command(client: Client, name: str, search_query: str, index_type: str = 'events',
                             description: str = '') -> CommandResults:
    params = assign_params(name=name, search_query=search_query, description=description, index_type=index_type)
    res = client.http_request(url='/v1/watchlist', method='POST', json_data=params)
    watchlist_id = res.get('id')
    if id:
        output = {'id': watchlist_id}
        return CommandResults(outputs=output, outputs_prefix='CarbonBlackEDR.Watchlist', outputs_key_field='id',
                              readable_output=f"Successfully created new watchlist with id {watchlist_id}")
    return CommandResults(readable_output="Could not create new watchlist.")


def get_watchlist_list_command(client: Client, id: str = None, limit: str = None) -> CommandResults:
    url = f'/v1/watchlist/{id}' if id else '/v1/watchlist'
    res: Union[dict, list] = client.http_request(url=url, method='GET')

    human_readable_data = []
    # Handling case of only one record.
    if id:
        res = [res]
    total_num_of_watchlists = len(res)
    res = res[:arg_to_number(limit, 'limit')]
    for watchlist in res:
        human_readable_data.append({
            'Name': watchlist.get('name'),
            'ID': watchlist.get('id'),
            'Group ID': watchlist.get('group_id'),
            'Description': watchlist.get('description'),
            'Total Hits': watchlist.get('total_hits'),
            'Query': watchlist.get('search_query'),
        })

    md = f'{INTEGRATION_NAME} - Watchlists'