from typing import Dict, List, Tuple, Any, Optional

import urllib3

from CommonServerPython import *

# disable insecure warnings
urllib3.disable_warnings()

INTEGRATION_NAME = 'Intel 471 Malware Feed'
FEED_URL_INDICATORS = 'https://api.intel471.com/v1/indicators/stream'
FEED_URL_GIRS = 'https://api.intel471.com/v1/girs'
DEMISTO_VERSION = demisto.demistoVersion()
CONTENT_PACK = f'Intel471 Feed/{str(get_pack_version())}'
INTEGRATION = 'Intel471 Malware Indicator Feed'
USER_AGENT = f'XSOAR/{DEMISTO_VERSION["version"]}.{DEMISTO_VERSION["buildNumber"]} - {CONTENT_PACK} - {INTEGRATION}'
MAX_COUNT = 100
MAX_BATCH = 10000

INDICATOR_TYPES = {
    'file': FeedIndicatorType.File,
    'ipv4': FeedIndicatorType.IP,
    'url': FeedIndicatorType.URL
}


class Client():
    """
    Client to use in the Intel 471 Feed integration. Overrides BaseClient.
    """

    headers = {'user-agent': USER_AGENT}

    def __init__(self, auth: Tuple[str, str], insecure: bool = False, tags: Optional[list] = None,
                 tlp_color: Optional[str] = None, indicator: Optional[str] = '*',
                 indicator_type: Optional[str] = None, threat_type: Optional[str] = None,
                 malware_family: Optional[str] = None, confidence: Optional[str] = None,
                 fetch_time: Optional[str] = None):
        """
        Implements class for Intel 471 feed.
        :param auth: credentials for accessing the feed.
        :param insecure: boolean, if *false* feed HTTPS server certificate is verified. Default: *false*.
        :param tags: list of tags.
        :param tlp_color: Traffic Light Protocol color.
        :param indicator: str, free text indicator filter.
        :param indicator_type: str, indicator type filter.
        :param threat_type: str, threat type filter.
        :param malware_family: str, malware family filter.
        :param confidence: str, confidence filter.
        :param fetch_time: str, fetch time filter.
        """
        self.auth = auth
        self._verify: bool = insecure
        self.tags = [] if tags is None else tags
        self.tlp_color = tlp_color
        self.indicator = indicator
        self.indicator_type = indicator_type
        self.threat_type = threat_type
        self.malware_family = malware_family
        self.confidence = confidence
        self.fetch_time = fetch_time
        self._proxies = handle_proxy(proxy_param_name='proxy', checkbox_default_value=False)

    def get_girs(self) -> List:
        """Retrieves a list of General Intelligence Requirements (GIRs).
        Returns:
            GIRs.
        """
        result: List = []

        feed_url = FEED_URL_GIRS

        params = {}
        params['count'] = MAX_COUNT
        params['offset'] = 0

        should_continue: bool = True
        while should_continue:
            try:
                response = requests.get(
                    url=feed_url,
                    params=params,
                    verify=self._verify,
                    proxies=self._proxies,
                    headers=self.headers,
                    auth=self.auth,
                )
                response.raise_for_status()
                data = response.json()
                girs: List = data.get("girs", [])

                if girs:
                    result.extend(girs)
                else:
                    should_continue = False

                if len(girs) < MAX_COUNT:
                    should_continue = False

                params['offset'] = params['offset'] + MAX_COUNT

            except requests.exceptions.SSLError as err:
                demisto.debug(str(err))
                raise Exception(f'Connection error in the API call to {INTEGRATION_NAME}.\n'
                                f'Check your not secure parameter.\n\n{err}')
            except requests.ConnectionError as err:
                demisto.debug(str(err))
                raise Exception(f'Connection error in the API call to {INTEGRATION_NAME}.\n'
                                f'Check your Server URL parameter.\n\n{err}')
            except requests.exceptions.HTTPError as err:
                demisto.debug(f'Got an error from {feed_url} while fetching GIRs {(str(err))} ')
            except ValueError as err:
                demisto.debug(str(err))
                raise ValueError(f'Could not parse returned data to JSON. \n\nError massage: {err}')

        return result

    def build_iterator(self, save_state: bool, limit: int = -1) -> List:
        """Retrieves all entries from the feed.
        Args:
            save_state: save state (cursor).
            limit: limit the results.
        Returns:
            A list of objects, containing the indicators.
        """
        result = []

        feed_url = FEED_URL_INDICATORS

        integration_context = get_integration_context()

        params = {}
        if self.indicator:
            params['indicator'] = self.indicator
        if self.indicator_type:
            if 'All' not in self.indicator_type:
                params['indicatorType'] = ' || '.join(self.indicator_type).lower()
        if self.threat_type:
            params['threatType'] = self.threat_type
        if self.malware_family:
            params['malwareFamily'] = self.malware_family
        if self.confidence:
            params['confidence'] = self.confidence
        params['count'] = str(MAX_COUNT)

        last_updated_from = integration_context.get('last_updated_from', '')
        if not last_updated_from:
            start_date, end_date = parse_date_range(self.fetch_time, utc=True, to_timestamp=True)
            last_updated_from = str(start_date)
        params['lastUpdatedFrom'] = last_updated_from

        cursor = integration_context.get('cursor', '')
        if cursor:
            params['cursor'] = cursor

        should_continue: bool = True
        while should_continue:
            encoded_params = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)

            try:
                response = requests.get(
                    url=feed_url,
                    params=encoded_params,
                    verify=self._verify,
                    proxies=self._proxies,
                    headers=self.headers,
                    auth=self.auth,
                )
                response.raise_for_status()
                data = response.json()
                indicators: List = data.get("indicators", [])

                if indicators:
                    result.extend(indicators)
                else:
                    should_continue = False

                if len(indicators) < MAX_COUNT:
                    should_continue = False

                if limit > 0:
                    if len(result) >= limit:
                        should_continue = False
                        result = result[:limit]
                else:
                    if len(result) >= MAX_BATCH:
                        should_continue = False

                cursor_next = data.get('cursorNext', '')
                if cursor_next:
                    params['cursor'] = cursor_next

            except requests.exceptions.SSLError as err:
                demisto.debug(str(err))
                raise Exception(f'Connection error in the API call to {INTEGRATION_NAME}.\n'
                                f'Check your not secure parameter.\n\n{err}')
            except requests.ConnectionError as err:
                demisto.debug(str(err))
                raise Exception(f'Connection error in the API call to {INTEGRATION_NAME}.\n'
                                f'Check your Server URL parameter.\n\n{err}')
            except requests.exceptions.HTTPError as err:
                demisto.debug(f'Got an error from {feed_url} while fetching indicators {(str(err))} ')
                raise Exception(f'HTTP error in the API call to {INTEGRATION_NAME}.\n'
                                f'Check your configuration.\n\n{err}')
            except ValueError as err:
                demisto.debug(str(err))
                raise ValueError(f'Could not parse returned data to JSON. \n\nError massage: {err}')

        if save_state:
            set_integration_context({'last_updated_from': last_updated_from})
            set_integration_context({'cursor': cursor})

        return result


def test_module(client: Client, *_) -> str:
    """Builds the iterator to check that the feed is accessible.
    Args:
        client: Client object.
    Returns:
        Outputs.
    """
    client.build_iterator(False, MAX_COUNT)

    return 'ok'


def build_relationships(type_: str, value_: str, malware_family: str) -> List:
    """Creates a list of relationships for the indicator.
    Args:
        type_ (str): relationship type.
        value_ (str): indicator value.
        malware_family (str): malware family.
    Returns:
        List: A list of relationships.
    """
    relationships: List = []

    relationships.append(
        EntityRelationship(
            name=EntityRelationship.Relationships.INDICATOR_OF,
            entity_a=value_,
            entity_a_type=type_,
            entity_b=malware_family,
            entity_b_type='Malware'
        ).to_indicator()
    )

    return relationships


def build_indicator(client: Client, raw_data: Dict[str, Any], girs: List) -> Dict[str, Any]:
    """Creates an indicator object.
    Args:
        raw_data: raw data of the indicator.
        girs: a list of GIRs.
    Returns:
        Dictionary representing the indicator object.
    """
    malware_family: str = raw_data.get('data', {}).get('threat', {}).get('data', {}).get('family', '')
    type_: str = INDICATOR_TYPES.get(raw_data.get('data', {}).get('indicator_type', ''), '')
    indicator_data = raw_data.get('data', {}).get('indicator_data', {})

    intel_requirements: List = []
    intel_requirement_paths: List = raw_data.get('data', {}).get('intel_requirements', [])
    for irp in intel_requirement_paths:
        gir: Dict = next(filter(lambda g: g.get('data', {}).get('gir', {}).get('path', {}) == irp, girs), {})
        if gir:
            name = gir.get('data', {}).get('gir', {}).get('name', '')
            intel_requirements.append(f'GIR: {irp} - {name}')

    value_: str = ''
    fields: Dict = {}
    if type_ == FeedIndicatorType.File:
        value_ = indicator_data.get('file', {}).get('sha256')
        fields['md5'] = indicator_data.get('file', {}).get('md5')
        fields['sha1'] = indicator_data.get('file', {}).get('sha1')
        fields['sha256'] = indicator_data.get('file', {}).get('sha256')
        fields['ssdeep'] = indicator_data.get('file', {}).get('ssdeep')
        fields['filetype'] = indicator_data.get('file', {}).get('type')
        fields['downloadurl'] = indicator_data.get('file', {}).get('download_url')
        fields['size'] = indicator_data.get('file', {}).get('size')
    elif type_ == FeedIndicatorType.IP:
        value_ = indicator_data.get('address')
    elif type_ == FeedIndicatorType.URL:
        value_ = indicator_data.get('url')

    first: str = datetime.fromtimestamp(raw_data.get('activity', {}).get('first') / 1000).isoformat('T')
    last: str = datetime.fromtimestamp(raw_data.get('activity', {}).get('last') / 1000).isoformat('T')

    fields['firstseenbysource'] = first
    fields['lastseenbysource'] = last

    fields['trafficlightprotocol'] = client.tlp_color

    fields['tags'] = [raw_data.get('data', {}).get('context', {}).get('description', '')]
    fields['tags'].append(raw_data.get('data', {}).get('mitre_tactics', ''))
    fields['tags'].extend(intel_requirements)
    fields['tags'].extend(client.tags)

    indicator_obj = {
        'value': value_,
        'type': type_,
        'rawJSON': raw_data,
        'fields': fields,
        'relationships': build_relationships(type_, value_, malware_family)
    }

    return indicator_obj


def fetch_indicators(client: Client, save_state: bool, limit: int = -1) -> List[Dict]:
    """Retrieves indicators from the feed.
    Args:
        client: Client object with request.
        save_state: save state (cursor).
        limit: limit the results.
    Returns:
        Indicators.
    """
    girs = client.get_girs()
    iterator = client.build_iterator(save_state, limit)
    indicators: List = []

    for item in iterator:
        indicator_obj = build_indicator(client, item, girs)

        indicators.append(indicator_obj)

    return indicators


def get_indicators_command(client: Client, args: Dict[str, str]) -> CommandResults:
    """Wrapper for retrieving indicators from the feed to the war-room.
    Args:
        client: Client object with request.
        args: demisto.args().
    Returns:
        CommandResults object containing the indicators retrieved.
    """
    limit = arg_to_number(demisto.args().get('limit')) or 100
    indicators = fetch_indicators(client, False, limit)

    hr_indicators = []
    for indicator in indicators:
        hr_indicators.append({
            'Value': indicator.get('value'),
            'Type': indicator.get('type'),
            'rawJSON': indicator.get('rawJSON'),
            'fields': indicator.get('fields'),
        })

    human_readable = tableToMarkdown("Indicators from Intel 471:", hr_indicators,
                                     headers=['Value', 'Type', 'rawJSON', 'fields'], removeNull=True)
    return CommandResults(
        readable_output=human_readable,
        outputs_prefix='Intel471Feed.Indicators',
        outputs_key_field='value',
        raw_response=indicators,
    )


def fetch_indicators_command(client: Client, args: Dict[str, str]) -> List[Dict]:
    """Wrapper for fetching indicators from the feed to the Indicators tab.
    Args:
        client: Client object with request.
    Returns:
        Indicators.
    """
    indicators = fetch_indicators(client, True)
    return indicators


def main():
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """
    args = demisto.args()
    params = demisto.params()
    use_ssl = not params.get('insecure', False)
    tags = argToList(params.get('feedTags'))
    tlp_color = params.get('tlp_color')
    indicator = params.get('indicator')
    indicator_type = params.get('indicator_type')
    threat_type = params.get('threat_type')
    malware_family = params.get('malware_family')
    confidence = params.get('confidence')
    fetch_time = params.get('fetch_time')
    credentials = params.get('credentials', {})
    if not credentials:
        raise DemistoException("Integration credentials not entered.")
    else:
        auth = (credentials.get('identifier', ''), credentials.get('password', ''))

    command = demisto.command()
    demisto.info(f'Command being called is {command}')

    try:
        client = Client(auth, use_ssl, tags, tlp_color, indicator, indicator_type,
                        threat_type, malware_family, confidence, fetch_time)

        if command == 'test-module':
            return_results(test_module(client, params))
        elif command == 'intel471-indicators-get-indicators':
            return_results(get_indicators_command(client, args))
        elif command == 'fetch-indicators':
            indicators = fetch_indicators_command(client, args)
            for iter_ in batch(indicators, batch_size=2000):
                demisto.createIndicators(iter_)
        else:
            raise NotImplementedError(f'Command {command} is not implemented.')

    except Exception as err:
        err_msg = f'Error in {INTEGRATION_NAME} Integration. [{err}]'
        return_error(err_msg)


if __name__ in ['__main__', 'builtin', 'builtins']:
    main()
