# -*- coding: utf-8 -*-
# Copyright (c) 2017 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com

"""
Contains functions to reliably extract resource IDs from AWS ARNs.
This code is pretty hairy and makes you appreciate just how crazy the AWS identifier world is.
To best understand this module, it's best to consult the unit tests which cover this thing exhaustively.
At least the ugly can be kept here and out of the API!
"""

import re

from czid.common.resource_types import (RESOURCE_TYPE_PREFIXES,
                                        SERVICE_TYPE_TO_RESOURCE_TYPE)
from czid.czrn.exceptions import ResourceS3KeyError

# Transform/map for resource types
_transformable_resource_type_names = {
    'applicationversion': 'application-version',
    'autoscalinggroup': 'group',
    'batchprediction': 'batch-prediction',
    'bytematchset': 'byte-match-set',
    'channels': 'channel',
    'checks': 'check',
    'cloudformationstackrecord': 'cloudformation-stack',
    'configurations': 'configuration',
    'configurationtemplate': 'configuration-template',
    'datasource': 'data-source',
    'db': 'instance',
    'dbgroup': 'database-group',
    'dbname': 'database',
    'dbuser': 'database-user',
    'deliverystream': 'delivery-stream',
    'disksnapshot': 'disk-snapshot',
    'dxcon': 'connection',
    'dxlag': 'lag',
    'dxvif': 'virtual-interface',
    'eip': 'elastic-ip',
    'es': 'event-subscription',
    'event-source-mappings': 'event-source-mapping',
    'exportsnapshotrecord': 'export-snapshot',
    'healthcheck': 'health-check',
    'hostedzone': 'hosted-zone',
    'inputsecuritygroup': 'input-security-group',
    'instancesnapshot': 'instance-snapshot',
    'ipset': 'ip-set',
    'jobs': 'job',
    'jobtemplates': 'job-template',
    'keypair': 'key-pair',
    'launchconfiguration': 'launch-configuration',
    'loadbalancer': 'load-balancer',
    'loadbalancertlscertificate': 'load-balancer-tls-certificate',
    'maintenancewindow': 'maintenance-window',
    'mlmodel': 'learning-model',
    'natgateway': 'nat-gateway',
    'og': 'option-group',
    'origin_endpoints': 'origin-endpoint',
    'ou': 'organizational-unit',
    'parametergroup': 'parameter-group',
    'patchbaseline': 'baseline',
    'pg': 'parameter-group',
    'presets': 'preset',
    'queues': 'queue',
    'relationaldatabase': 'relational-database',
    'relationaldatabasesnapshot': 'relational-database-snapshot',
    'scalingpolicy': 'policy',
    'secgrp': 'security-group',
    'securitygroup': 'security-group',
    'solutionstack': 'solution-stack',
    'statemachine': 'state-machine',
    'staticip': 'static-ip',
    'subgrp': 'subnet-group',
    'subnetgroup': 'subnet-group',
    'threatintelset': 'threat-intel-set',
    'u2f': 'u2f-device',
    'userdefinedfunction': 'user-defined-function',
    'vaults': 'vault',
    'webacl': 'web-acl',
}


def normalize_resource_type_name(arn_resource):
    if not arn_resource:
        return arn_resource
    lowercase_resource = arn_resource.lower()
    m = re.fullmatch('^([a-z-_0-9/]+).*', lowercase_resource)
    name = m.groups()[0]
    return _transformable_resource_type_names.get(name) or name.replace('_', '-')


def get_resource_type_from_arn_components(service, account_id, resource):
    """
    Get a normalized resource type from the components of an ARN

    Args:
        service (str):
        account_id (str):
        resource (str):

    Returns:
        str - a canonical resource type name
    """
    # special case: managed policies are identifiable solely by aws account IDs
    if account_id == 'aws' and resource.startswith('policy'):
        return 'managed-policy'

    resource_type = get_resource_type_from_resource_string(service, resource)
    if resource_type:
        return resource_type
    elif '/' in resource:
        return normalize_resource_type_name(resource.split('/')[0])
    elif ':' in resource:
        return normalize_resource_type_name(resource.split(':')[0])

    return 'unknown'


def _extract_acm_pca_resource_types(arn_resource):
    segments = arn_resource.split('/')
    if len(segments) > 2:
        return 'certificate'
    return 'certificate-authority'


def _extract_api_gateway_resource_types(arn_resource):
    if any([f'/{t}' in arn_resource for t in {'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'ANY'}]):
        return 'deployed-api-endpoint'  # TODO: figure out if this is the same thing as a `deployment`
    segments = arn_resource.split('/')
    if len(segments) == 3 and segments[1].endswith('apis'):
        return 'rest-api'
    if len(segments) == 5 and segments[1].endswith('apis') and segments[3] == 'stages':
        return 'stage'
    elif '/authorizers/' in arn_resource:
        return 'authorizer'


def _extract_appsync_resource_types(arn_resource):
    if '/types/Query/' in arn_resource:
        return 'query-field'
    if '/types/Mutation/' in arn_resource:
        return 'mutation-field'
    if '/types/Subscription/' in arn_resource:
        return 'subscription-field'
    else:
        return 'api'


def _extract_logs_resource_types(arn_resource):
    if arn_resource.startswith('log-group'):
        if ':log-stream:' in arn_resource:
            return 'log-stream'
        else:
            return 'log-group'
    elif arn_resource.startswith('destination'):
        return 'destination'


def _extract_cognito_sync_resource_types(arn_resource):
    if arn_resource.startswith('identitypool'):
        if '/identity/' not in arn_resource and '/dataset/' not in arn_resource:
            return 'identity-pool'
        if '/identity/' in arn_resource and '/dataset/' not in arn_resource:
            return 'identity'
        if '/identity/' in arn_resource and '/dataset/' in arn_resource:
            return 'dataset'


def _extract_datasync_resource_types(arn_resource):
    segments = arn_resource.split('/')
    if len(segments) > 2:
        return 'execution'
    return normalize_resource_type_name(segments[0])


def _extract_dynamodb_resource_types(arn_resource):
    if '/stream/' not in arn_resource and '/index/' not in arn_resource and '/backup/' not in arn_resource:
        return 'table'
    if '/stream/' in arn_resource:
        return 'stream'
    if '/index/' in arn_resource:
        return 'index'
    if '/backup/' in arn_resource:
        return 'backup'


def _extract_elasticloadbalancing_resource_types(arn_resource):
    # These are wacky special cases because the resource type includes a delimiter
    if len(arn_resource.split('/')) == 4:
        if arn_resource.startswith('loadbalancer/app'):
            return 'app-load-balancer'
        if arn_resource.startswith('loadbalancer/net'):
            return 'net-load-balancer'
    if arn_resource.startswith('loadbalancer/'):
        return 'classic-load-balancer'
    if arn_resource.startswith('listener/app'):
        return 'app-load-balancer-listener'
    if arn_resource.startswith('listener/net'):
        return 'net-load-balancer-listener'
    if arn_resource.startswith('listener-rule/app'):
        return 'app-load-balancer-listener-rule'
    if arn_resource.startswith('listener-rule/net'):
        return 'net-load-balancer-listener-rule'
    if arn_resource.startswith('targetgroup'):
        return 'target-group'


def _extract_glue_resource_types(arn_resource):
    if arn_resource == 'catalog':
        return 'root-catalog'
    elif arn_resource.split('/')[0] in {'database', 'table', 'connection', 'mlTransform'}:
        return arn_resource.split('/')[0].lower()
    elif arn_resource.split('/')[0] == 'devEndpoint':
        return 'dev-endpoint'
    elif 'job' in arn_resource:
        return 'job'
    elif 'crawler' in arn_resource:
        return 'crawler'
    return 'user-defined-function'


def _extract_iam_resource_types(arn_resource):
    if arn_resource == 'root':
        return 'aws-account'
    if arn_resource.startswith('role/service-role/'):
        return 'service-role'
    if arn_resource.startswith('mfa/'):
        return 'mfa-device'
    if arn_resource.startswith('u2f/'):
        return 'u2f-device'

    # This one may not be valid, but is referenced here:
    # https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html
    if arn_resource.startswith('role/aws-service-role/'):
        return 'aws-service-role'

    if '/' in arn_resource:
        return arn_resource.split('/')[0]


def _extract_lambda_resource_types(arn_resource):
    parts = arn_resource.split(':')
    if len(parts) == 3:
        if 'layer' == parts[0]:
            return 'layer-version'
        return 'function-version'
    if 'event-source-mappings' == parts[0]:
        return 'event-source-mapping'
    if 'layer' == parts[0]:
        return 'layer'
    return 'function'


def _extract_kinesis_resource_types(arn_resource):
    # stream --> stream/example-stream-name
    # stream-consumer --> stream/example-stream-name/consumer/example-consumer-name:123456789
    parts = arn_resource.split(':')
    if len(parts) > 1:
        return 'stream-consumer'

    return 'stream'


def _extract_macie_resource_types(arn_resource):
    segments = arn_resource.split('/')
    if len(segments) > 2:
        return 'alert'
    return 'trigger'


def _extract_mobile_targeting_resource_types(arn_resource):
    if arn_resource.startswith('apps'):
        if '/segments/' not in arn_resource and '/campaigns/' not in arn_resource:
            return 'app'
        if '/segments/' in arn_resource and '/campaigns/' not in arn_resource:
            return 'segment'
        if '/segments/' not in arn_resource and '/campaigns/' in arn_resource:
            return 'campaign'


def _extract_rds_resource_types(arn_resource):
    if arn_resource.startswith('cluster-snapshot'):
        return 'cluster-snapshot'
    if arn_resource.startswith('cluster-pg'):
        return 'cluster-parameter-group'
    return normalize_resource_type_name(arn_resource)


def _extract_serverlessrepo_resource_types(arn_resource):
    if arn_resource.startswith('applications'):
        if '/versions/' not in arn_resource:
            return 'application'
        if '/versions/' in arn_resource:
            return 'semantic-version'


def _extract_sns_resource_types(arn_resource):
    slash_parts = arn_resource.split('/')
    colon_parts = arn_resource.split(':')
    if len(slash_parts):
        if slash_parts[0] == 'endpoint':
            return 'platform-endpoint'
        if slash_parts[0] == 'app':
            return 'platform-application'
    if len(colon_parts) == 2:
        return 'subscription'
    return 'topic'


def _extract_s3_resource_types(arn_resource):
    parts = arn_resource.split('/')
    if parts[0] == 'storage-lens':
        return parts[0]

    if len(parts) >= 2:
        raise ResourceS3KeyError
    return 'bucket'


def _extract_ecs_resource_types(arn_resource):
    if ':' in arn_resource:
        return 'task-definition'
    elif arn_resource.split('/')[0] == 'task-definition':
        return 'task-definition-family'
    return arn_resource.split('/')[0]


def _extract_route53resolver_resource_types(arn_resource):
    slash_parts = arn_resource.split('/')
    if len(slash_parts) == 1:
        return 'bound-network-interface'
    return arn_resource.split('/')[0]


def _extract_batch_resource_types(arn_resource):
    if ':' in arn_resource:
        return 'job-definition-version'
    return arn_resource.split('/')[0]


def _extract_storage_gateway_resource_types(arn_resource):
    prefix = arn_resource.split('/')[0]
    if prefix != 'gateway':
        return normalize_resource_type_name(prefix)
    if '/volume/' not in arn_resource and '/target/' not in arn_resource and '/device/' not in arn_resource:
        return 'gateway'
    if '/volume/' in arn_resource and '/target/' not in arn_resource and '/device/' not in arn_resource:
        return 'volume'
    if '/volume/' not in arn_resource and '/target/' in arn_resource and '/device/' not in arn_resource:
        return 'scsi-target'
    if '/volume/' not in arn_resource and '/target/' not in arn_resource and '/device/' in arn_resource:
        return 'device'


def _extract_timestream_resource_types(arn_resource):
    parts = arn_resource.split('/')
    if len(parts) >= 4:
        return parts[2]

    return parts[0]


def get_resource_type_from_cloud_local_id(cloud_local_id):
    """
    Attempt to determine the resource type from a cloud_local_id (sometimes called a resource_id, but NOT an ARN)

    For some service types, like EC2, cloud_local_id's are formatted in special
    ways that identify the service and resource type. For example, an ec2 instance cloud local id
    will look like 'i-444444', its instance-ness betrayed by its 'i-' prefix.

    Args:
        cloud_local_id (str):

    Returns:
        str or None

    """
    if not cloud_local_id:
        return None

    # If we can split the cloud local id on a dash at least once, check if it's a know resource prefix
    split_local_id = cloud_local_id.split('-', 1)
    if split_local_id[0] and len(split_local_id) > 1:
        return RESOURCE_TYPE_PREFIXES.get(split_local_id[0])
    else:
        return None


def get_resource_type_from_service(service_type):
    """
    Some service types (not many) have only 1 resource type. In the case that we have one of those,
    return the resource type.

    This is useful when we only have a few pieces of data with which to construct a CZRN and we are
    trying to do the best we can

    Args:
        service_type (str):

    Returns:
        str or None

    """
    if not service_type:
        return None
    return SERVICE_TYPE_TO_RESOURCE_TYPE.get(service_type)


resource_type_custom_handlers = {
    'acm-pca': _extract_acm_pca_resource_types,
    'apigateway': _extract_api_gateway_resource_types,
    'appsync': _extract_appsync_resource_types,
    'artifact': lambda r: 'report',
    'batch': _extract_batch_resource_types,
    'cassandra': lambda r: 'keyspace',
    'codecommit': lambda r: 'repository',
    'codepipeline': lambda r: 'pipeline',
    'codestar': lambda r: 'project',
    'cognito-identity': lambda r: 'identity-pool',
    'cognito-idp': lambda r: 'user-pool',
    'cognito-sync': _extract_cognito_sync_resource_types,
    'datasync': _extract_datasync_resource_types,
    'dynamodb': _extract_dynamodb_resource_types,
    'ecs': _extract_ecs_resource_types,
    'elasticloadbalancing': _extract_elasticloadbalancing_resource_types,
    'emr-containers': lambda r: 'virtualcluster',
    'emr-serverless': lambda r: 'application',
    'glue': _extract_glue_resource_types,
    'grafana': lambda r: 'workspace',
    'iam': _extract_iam_resource_types,
    'kinesis': _extract_kinesis_resource_types,
    'lambda': _extract_lambda_resource_types,
    'logs': _extract_logs_resource_types,
    'macie': _extract_macie_resource_types,
    'medialive': lambda r: normalize_resource_type_name(r),
    'mobiletargeting': _extract_mobile_targeting_resource_types,
    'opsworks-cm': lambda r: 'server',
    'phonenumber': lambda r: 'phonenumber',
    'rds': _extract_rds_resource_types,
    'route53resolver': _extract_route53resolver_resource_types,
    's3': _extract_s3_resource_types,
    'secretsmanager': lambda r: 'secret',
    'serverlessrepo': _extract_serverlessrepo_resource_types,
    'sns': _extract_sns_resource_types,
    'sqs': lambda r: 'queue',
    'storagegateway': _extract_storage_gateway_resource_types,
    'swf': lambda r: 'domain',
    'timestream': _extract_timestream_resource_types,
}


def get_resource_type_from_resource_string(service, resource):
    """
    Define a mapping of service types to custom functions used for extracting resource types from resource string.

    A number of services (e.g. EC2) have a very regular, simple structure and can be processed without special code.
    For example, consider the following two examples:
        - arn:aws:cloudtrail:us-east-1:123456789012:trail/mytrailname
        - arn:aws:medialive:us-east-1:111111111111:channel:3456789
    In both of these cases, the ARN contains strings that are easily converted to resource_type and cloud_local_id in a
    CZRN.  The examples contain "trail/mytrailname" and "channel:3456789" which easily convert by splitting on the
    delimiter.

    The remaining ARNs (the ones on the list below) are more complex or even unique in how they express resources.
    There are several flavors:
    1) ARNs with no identifiable type token (e.g. apigateway, sns).  These ARNs represent resources, but the resource
       type is implied and appears nowhere in the token.
       Example: arn:aws:sns:us-east-1:123456789012:something_something <-- this is a topic resource
    2) ARNs whose resource types are expressed using a RESTful approach, where the type may be buried in a path.
       Example: arn:aws:mobiletargeting:us-east-1:123456789012:apps/abc/segments/123' <-- segment resource type
    3) ARNs like RDS where the resource type is present, but needs to be transformed in some way.
       Example: arn:aws:rds:us-east-1:123456789012:pg:mysql-repl-pg1 <-- parameter-group resource type
    4) True oddballs like elasticloadbalancing where the full resource type is determined by looking at multiple
       segments of the ARN delimited by slashes.
       Example: arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188
       This is an app-load-balancer ^^^

    As a general approach, consider this example:
    arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/my-lb/50dc6c495c0c9188

    If you split up the ARN naively, you are left with "loadbalancer/net/my-lb/50dc6c495c0c9188" as the source for the
    resource type; however the desired resource type (net-load-balancer) does not actually exist in the string.
    Hence, for many services we need custom functions to pull out these details.

    Each of these custom functions, whether named or anonymous, take a single argument: `arn_resource`.
    This represents this unpredictable part of the ARN, e.g. "loadbalancer/net/my-lb/50dc6c495c0c9188".
    Despite looking really arcane, all these custom functions are doing is trying to figure out where the useful
    resource type information is in the ARN.
    """
    # Special case to Handle 'usage' CZRN's, which reference usage metrics (cost, etc...) for an entire service
    if resource[0:6] == 'usage/':
        return 'service-usage'
    elif service in resource_type_custom_handlers:
        return resource_type_custom_handlers[service](resource)
    else:
        # ok try best effort using resource (as a cloud_local_id) or guessing based on service type
        return get_resource_type_from_cloud_local_id(resource) or get_resource_type_from_service(service)
