# 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 for creating CloudZero Resource Name (CZRN) identifiers.
CZRNs are used to unambiguously and uniquely identify any resource from any cloud provider, and even covers cases
where a resource might be observed from the Internet, and resources which may not have explicit existence
in a cloud environment.
"""
import re
import string
from functools import lru_cache

import czid.czrn.azure as azure
from czid.aws.arn import get_arn_components, is_wildcard_arn
from czid.common.accounts import ACCOUNT_TRANSFORMS
from czid.common.regions import REGION_TRANSFORMS
from czid.common.resource_types import (RESOURCE_TYPE_SERVICE_TYPE_TRANSFORMS,
                                        is_cross_region_resource,
                                        is_public_resource)
from czid.czrn import gcp
from czid.czrn.exceptions import (ResourceS3KeyError,
                                  ResourceWildcardError,
                                  UnsupportedResourceTypeError)
from czid.czrn.internal.billing_lineitem import (
    get_account_id_from_billing_fields, get_billing_lineitem_components,
    get_region_from_billing_fields)
from czid.czrn.internal.cloud_local_id_from_arn import \
    get_cloud_local_id_from_arn_components
from czid.czrn.internal.resource_ids import \
    get_resource_type_from_arn_components
from czid.czrn.validate import is_valid

VALID_CZRN_PART_CHARS = set(string.ascii_lowercase) | set([str(n) for n in range(10)]) | set('_.-')
INVALID_CZRN_PART_CHARS_REGEX = re.compile(r'[^a-z0-9_.-]+')


def _czrn_from_parts(*parts: str) -> str:
    return ':'.join(('czrn', *parts))


def _normalize_czrn_part(part: str) -> str:
    part_lower = part.lower()
    invalid_chars = set(part_lower) - VALID_CZRN_PART_CHARS
    return INVALID_CZRN_PART_CHARS_REGEX.sub('-', part_lower) if invalid_chars else part_lower


@lru_cache(maxsize=1024)
def create_from_components(service_type, resource_type, cloud_local_id, provider=None, region=None, account_id=None,
                           allow_unknown_accounts=True, check_validity=True):
    """
    Create a CZRN identifier given its components, some of which are optional.
    All resources must have a CZRN that complies with this format.

    Args:
        service_type (str): The service e.g. EC2, VPC, RDS, etc. may also be a CloudZero-specific values for certain
                            special cases, like network addresses and DNS.
        resource_type (str): The exact and unambiguous resource type within a service. This is information
                             CloudZero provides that is never explicit in AWS, but must be teased out of the woodwork
                             like bucket, instance, role, or cluster.  Resource types are not globally unique, but are
                             guaranteed to be unique within a service.
        cloud_local_id (str): Identifying information for the resource within the scope of the
                              resource's service type.  For an AWS service, this would typically be an AWS-defined ID of
                              some sort.  In the case of CloudZero-specific services like IP addresses, the
                              cloud-local-id might be a bit more exotic; constructed from multiple pieces of information
                              gathered from the current context.
        provider (str): Cloud Provider where the resource was found, which can be either a named cloud provider such as
                        AWS or GCP (Google Compute Platform) or might be more generic such as if the resource was seen
                        on the Internet and can't be correlated or traced back to a specific provider.
                        Default is 'aws'.
        region (str): The cloud provider region where the service is located. For cross-region services
                      the region passed can be None, in which case the result is 'cross-region'.  Otherwise, the default
                      is 'no-region'.
        account_id (str): The account id as defined by the cloud provider, e.g. 123456789012 for AWS.  The value
                          'public' is also allowed (if known, usually requires resource enrichment).  Defaults to
                          'unknown-account' if no value is passed is specified.
        allow_unknown_accounts (Bool):
    Returns:
        str: the full CZRN that can be stored with the resource and used to uniquely identify it

    >>> create_from_components('ec2', 'instance', 'i-1234', region='us-east-2', account_id='123456789012')
    'czrn:aws:ec2:us-east-2:123456789012:instance:i-1234'

    >>> create_from_components(None, None, None, None, None)
    Traceback (most recent call last):
    ...
    ValueError: Service type, resource type, and cloud local id must be specified
        - service_type: None, resource_type: None, cloud_local_id: None
    """
    if not all({service_type, resource_type, cloud_local_id}):
        raise ValueError(f'Service type, resource type, and cloud local id must be specified - '
                         f'service_type: {service_type}, resource_type: {resource_type}, '
                         f'cloud_local_id: {cloud_local_id}')
    provider = provider or 'aws'

    # Check and validate region
    if service_type == 'cz-cloud-provider' and resource_type == 'region':
        # Special case - region resources always use the cloud local ID as the region to avoid conflicts.
        region = cloud_local_id
    elif service_type == 's3' and resource_type == 'bucket':
        # Special case - if this is an S3 bucket, we allow a region to be specified explicitly
        # Other resources won't allow this.
        region = region or 'cross-region'
    elif is_cross_region_resource(service_type, resource_type):
        region = 'cross-region'
    elif not region:
        region = 'no-region'
    else:
        region = REGION_TRANSFORMS.get(region) or region

    # Check Account ID
    if service_type == 'cz-cloud-provider' and resource_type == 'account':
        # Special case - account resources always use the cloud local ID as the account to avoid conflicts.
        account_id = cloud_local_id
    elif is_public_resource(service_type, resource_type):
        account_id = 'public'
    else:
        account_id = ACCOUNT_TRANSFORMS.get(account_id) or account_id

    # unknown-account should have never been allowed in here, a CZRN without an account ID is unsafe and essentially
    # worthless, but until I can be sure i'm not breaking something i'm leaving it in with a way to disable it
    if not account_id:
        if allow_unknown_accounts:
            account_id = 'unknown-account'
        else:
            raise ValueError("Can not create CZRN, missing account-id")

    # Check that service type is correct
    service_type = RESOURCE_TYPE_SERVICE_TYPE_TRANSFORMS.get(resource_type) or service_type

    if check_validity:
        resource_id = _czrn_from_parts(_normalize_czrn_part(provider), service_type, region, account_id, resource_type,
                                       cloud_local_id)
        if not is_valid(resource_id):
            raise ValueError(f'The created CZRN {resource_id} is not valid.')
        return resource_id

    return _czrn_from_parts(
        *(_normalize_czrn_part(x) for x in [provider, service_type, region, account_id, resource_type]),
        cloud_local_id)


def create_from_arn(arn, allow_unknown_accounts=True):
    """
    Shortcut to create a CZRN identifier given an AWS ARN as input instead of the usual 6 components.
    The end result, however, should be the same.

    Invalid ARNs will raise one of the following:
     - ResourceS3KeyError
     - ResourceWildcardError
     - ValueError.
    (Custom errors inherit from UnsupportedResourceTypeError which inherits from ValueError.)
    The former case for S3 Key resources, because they are simply too numerous for us to want to process.
    The latter for all other ARNs that we are unable to process in some way.

    Args:
        arn (str): The AWS ARN to use to create the CZRN
        allow_unknown_accounts (bool):

    Returns:
        str: the full CZRN that can be stored with the resource and used to uniquely identify it

    >>> create_from_arn('arn:aws:storagegateway:us-east-1:123456789012:gateway/sgw-12A3456B/target/iqn.1997-05.com.amazon:vol-1122AABB')
    'czrn:aws:storagegateway:us-east-1:123456789012:scsi-target:sgw-12A3456B|iqn.1997-05.com.amazon:vol-1122AABB'

    >>> create_from_arn('arn:aws:glue:us-east-1:479083393249:job/V2_EXT870')
    'czrn:aws:glue:us-east-1:479083393249:job:V2_EXT870'

    >>> create_from_arn(None)
    Traceback (most recent call last):
    ...
    ValueError: ARN must be specified - arn: None

    >>> create_from_arn('arn:aws:s3:::my_corporate_bucket/exampleobject.png')
    Traceback (most recent call last):
    ...
    czid.czrn.exceptions.ResourceS3KeyError:
    S3 key ARN arn:aws:s3:::my_corporate_bucket/exampleobject.png is not supported
    """
    if not arn:
        raise ValueError(f'ARN must be specified - arn: {arn}')

    try:
        service, region, account_id, resource = get_arn_components(arn)
        resource_type = get_resource_type_from_arn_components(service, account_id, resource)

        # We don't allow wildcards in ARNs except for certain special cases
        wildcard_supported = (service, resource_type) in {('logs', 'log-group')}
        if not wildcard_supported and is_wildcard_arn(arn):
            raise ResourceWildcardError

        cloud_local_id = get_cloud_local_id_from_arn_components(service, resource, resource_type)
        return create_from_components(service, resource_type, cloud_local_id, region=region, account_id=account_id,
                                      allow_unknown_accounts=allow_unknown_accounts)
    except ResourceS3KeyError:
        raise ResourceS3KeyError(f'S3 key ARN {arn} is not supported')
    except ResourceWildcardError:
        raise ResourceWildcardError(f'ARN must not use wildcards - arn: {arn}')
    except Exception as e:
        raise ValueError(f'Unable to convert ARN {arn} to CZRN, internal error occurred') from e


def create_from_billing_lineitem(lineitem_fields: dict) -> str:
    """
    Given a dict mapping billing line item fields to their values, creates a CZRN
    uniquely identifying the resource, if possible. If a CZRN can not be created
    from the information provided, None is returned.

    Args:
        lineitem_fields (dict): mapping of line item field names to values

    Returns:
        str or None: A CZRN uniquely identifying the resource if possible, else None.
    """
    billing_entity = lineitem_fields.get('bill_billingentity') or 'AWS'
    cloud_provider = 'AWS' if billing_entity in ('AWS', 'AISPL', 'AWS Marketplace') else billing_entity
    if cloud_provider == 'Azure':
        return azure.create_from_billing_lineitem(lineitem_fields)

    billing_resource_id = (lineitem_fields.get('lineitem_resourceid') or '').strip()

    try:
        # If the billing resource ID is a ARN
        # this comparison is 2.5x faster than .startswith and probably 10x faster than failing on obvious non arns
        # since CZID is called billions or maybe trillions of times a day, every ms matters
        if billing_resource_id[0:4] == 'arn:':
            service_type, region, account_id, resource = get_arn_components(billing_resource_id)
            account_id = account_id or get_account_id_from_billing_fields(lineitem_fields)
            region = region or get_region_from_billing_fields(lineitem_fields)
        elif cloud_provider == 'GCP' and billing_resource_id[0:2] == '//':
            service_type, region, account_id, resource = gcp.get_resource_global_name_components(lineitem_fields)
        else:
            raise ValueError()
    except (UnsupportedResourceTypeError, ValueError):
        # Or if the billing resource ID is empty or a cloud local ID
        service_type, region, account_id, resource = get_billing_lineitem_components(lineitem_fields)

    resource_type = None
    try:
        if not resource:
            raise UnsupportedResourceTypeError("Unable to extract resource id from billing lineitem")

        resource_type = get_resource_type_from_arn_components(service_type, account_id, resource)
        cloud_local_id = get_cloud_local_id_from_arn_components(service_type, resource, resource_type)

        return create_from_components(service_type=service_type,
                                      resource_type=resource_type,
                                      cloud_local_id=cloud_local_id,
                                      provider=cloud_provider,
                                      region=region,
                                      account_id=account_id,
                                      allow_unknown_accounts=False,
                                      check_validity=(cloud_provider == 'AWS'))

    except Exception as e:
        raise ValueError(f'Could not create CZRN. '
                         f'Service Type: {service_type} '
                         f'Region: {region} '
                         f'Resource Type: {resource_type} '
                         f'Cloud Account ID: {account_id}'
                         f'Cloud Local ID: {billing_resource_id}') from e
