import boto3
import logging
import os
import re

from .common import DEFAULT_ECOSYSTEM
from .lang_utils import conjoined_list
from .misc_utils import PRINT, find_associations, find_association, snake_case_to_camel_case, ignored
from .env_utils import is_stg_or_prd_env, ecr_repository_for_env


logging.basicConfig()
logger = logging.getLogger(__name__)


def camelize(snake_name):
    """
    Converts a token from hyphen-separated pseudo-snake-case to CamelCase.
    This is similar to snake_case_to_camel_case, but treats the common situation of hyphens
    that comes up in CloudFormation orchestration.
    """
    return snake_case_to_camel_case(snake_name, separator='-')


def dehyphenate(s):
    """Removes hyphens from a given string."""
    return s.replace('-', '')


def hyphenify(s):
    """Turns the underscore form of snake_case into the hyphenated form."""
    return s.replace('_', '-')


def tokenify(s):
    """Strip out everything but alphanumerics from a given token"""
    return ''.join([ch for ch in s if ch.isalpha() or ch.isdigit()])


def make_required_key_for_ecs_application_url(env_name):
    """
    This pattern needs to remain fixed for various things to connect up.
    Use make_key_for_ecs_application_url to generate an appropriate key.
    """
    return f"ECSApplicationURL{dehyphenate(env_name)}"


def get_ecs_real_url(env):
    """
    Returns the ECS target URL for a given environment created in on orchestration from Cloudformation
    if proper conventions were followed in by tagging the environment's ECS stack with an output
    generated by make_required_key_for_ecs_application_url.

    NOTE: Users of this facility are advised that they should not rely on printing a message and returning ''
    in the case of failure. In the future, an error may be raised, even without a major version bump.
    The only advertised behavior for now is that you should only call this when you expect success,
    and you should be prepared for a bad value OR raised errorin the case of failure.
    """
    manager = C4OrchestrationManager()
    found = manager.find_stack_output(make_required_key_for_ecs_application_url(env), value_only=True)
    if found:
        return found
    else:  # Couldn't this just raise an error? -kmp 17-Aug-2021
        PRINT('Did not locate the server from Cloudformation! Check ECS Stack metadata.')
        return ''


# This is the default ecosystem that the orchestration is expected to make, although it might be rational
# in a shared environment situation to have several ecosystems with names like 'test', 'prod', 'demo', etc.
# Things that in-ecosystem sharing could allow:
#
#  * Shared use of an ES engine, separated only by namespace, in a 'test' ecosystem,
#    even as another 'prod' ecosystem and its own ES (whether or not it had its own account).
#    Probably we would do such sharing only for Fourfront, since CGAP security requirements
#    will require better isolation.
#
# * Shared use of common repos between various platforms without repeated uploads.

def get_ecr_repo_url(env_name=None, *, ecosystem=DEFAULT_ECOSYSTEM, from_stack_output_key=None):
    """
    Gets the ECR repository URL of the repository from which ECS pulls application image runtimes from.

    NOTE: Users of this facility are advised that they should not rely on printing a message and returning ''
    in the case of failure. In the future, an error may be raised, even without a major version bump.
    The only advertised behavior for now is that you should only call this when you expect success,
    and you should be prepared for a bad value OR raised errorin the case of failure.
    """
    ignored(from_stack_output_key)  # This argument was once needed but is no longer.
    manager = C4OrchestrationManager()
    found = manager.find_stack_outputs(lambda x: x.endswith("RepoURL"), value_only=True)

    # Pick the most specific place the environment might belong...

    # If there is a specific match to the env, use that.
    env_name_suffix = f"/{env_name}"
    for url in found:
        if url.endswith(env_name_suffix):
            return url

    # If there was not repo called 'fourfront-blue' or 'fourfront-green',
    # but there is a 'fourfront-webprod' use that. This could be helpful
    # some day if there is an orchestrated cgap, but in the interim it can
    # still be used to find the fourfront-production ecr repo for fourfront.
    if is_stg_or_prd_env:
        prod_bucket_suffix = f"/{ecr_repository_for_env(env_name)}"
        for url in found:
            if url.endswith(prod_bucket_suffix):
                return url

    # Finally, if there is no match for any of those, look for an ecosystem-appropriate result.
    ecosystem_suffix = f"/{ecosystem}"
    for url in found:
        if url.endswith(ecosystem_suffix):
            return url

    # At this point, we have to give up.
    # Should we really not err? This just copies the return value convention we had previously. -17-Aug-2021
    PRINT('Did not locate the Repo URL from Cloudformation! Check ECR Stack metadata.')
    return ''


class AbstractOrchestrationManager:
    """This class consolidates some packaged functionality for osing around in in cloudformation."""

    COMMON_STACK_PREFIX = None  # Must be customized in a subclass. This is an abstract class.

    STACK_NAME_TOKEN_EXTRACTION_PATTERN = re.compile("^(.*)$")

    def __init__(self):
        self.cloudformation = boto3.resource('cloudformation')

    @classmethod
    def get_stack_output(cls, stack, output_key_name_or_predicate):
        """Utility function to get a specified output out of a given stack."""
        entry = find_association(stack.outputs or [], OutputKey=output_key_name_or_predicate)
        if not entry:
            return None
        return entry['OutputValue']

    def all_stacks(self):
        """Returns a list of all stacks whose names start with our COMMON_STACK_PREFIX (a class variable)."""
        return [
            stack
            for stack in self.cloudformation.stacks.all()
            if stack.name.startswith(self.COMMON_STACK_PREFIX)
        ]

    def _extract_stack_name_token(self, stack):
        """Internal function. Our name-related searches need to match a name that this function stracts."""
        m = self.STACK_NAME_TOKEN_EXTRACTION_PATTERN.match(stack.name)
        if m:
            return m.group(1)

    def find_stack(self, name_token):
        """
        Searches for a stack whose name matches the specified name_token.

        The name_token is not the full name, and is a bit more sophisticated than just a substring.
        This glosses over the fact that stacks have names like c4-network-trial-alpha-stack or
        c4-network-trial-stack, and allows us to see a particular part of the actual name as the
        name to search for. See STACK_NAME_TOKEN_EXTRACTION_PATTERN.
        """
        candidates = []
        for stack in self.all_stacks():
            if name_token == self._extract_stack_name_token(stack):
                candidates.append(stack)
        [candidate] = candidates or [None]
        return candidate

    def find_stack_outputs(self, key_or_pred, value_only=False):
        """
        If value_only is False, this finds all outputs of all stacks (see .all_stacks() for what "all" means),
        that match key_or_pred, returning a dictionary of {key1: val1, key2: val2, ...}.

        If value_only is True, then a list, rather than a dictionary, of the result values is returned.
        This would be odd in general, but serves cases where the result might be
        {"Mask1": "10.0.0.0", "Mask2": "192.168.0.0"} and where ["10.0.0.0", "192.168.0.0"] is preferred.

        NOTE WELL: We make the strong assumption about cross-stack output tag uniqueness, that all keys are distinct.
        If two stacks had the same output key, only one of them would be required.
        """
        results = {}
        for stack in self.all_stacks():
            for found in find_associations(stack.outputs or [], OutputKey=key_or_pred):
                results[found['OutputKey']] = found['OutputValue']
        if value_only:
            return list(results.values())
        else:
            return results

    def find_stack_output(self, key_or_pred, value_only=False):
        """
        This is like find_stack_outputs, but assumes that only zero or one values will be returned.
        If one value, it returns the first of what find_stack_outputs would return as a list.
        If no value, it returns None.
        """
        results = self.find_stack_outputs(key_or_pred, value_only=value_only)
        n = len(results)
        if n == 0:
            return None
        elif n == 1:
            return (results[0]  # in this case, result is a list, so take its first element
                    if value_only
                    else results)   # if not value-only, result is a dictionary, which is fine
        else:
            raise ValueError(f"Too many matches for {key_or_pred}: {results}")

    def find_stack_resource(self, stack_name_token, resource_logical_id, attr=None, default=None):
        """
        Looks up a resource or resource attribute in a given stack.

        This is intended to use our mechanisms for looking up a stack in spite of its complex name,
        and then returning a given resource or some attribute of it. For example:

          ConfigManager.find_stack_resource('foursight', 'CheckRunner', 'physical_resource_id')

        might find a stack named 'c4-foursight-trial-alpha-stack' and then would look up
        the physical_resource_id of its CheckRunner resource. Without the final argument, the
        resource itself is returned.

        The default is returned if the stack doesn't exist, the named resource doesn't exist,
        or (if a resource attribute is requested) no such attribute exists in the resource.
        """
        stack = self.find_stack(stack_name_token)
        if not stack:
            return default
        for summary in stack.resource_summaries.all():
            if summary.logical_id == resource_logical_id:
                if attr is None:
                    return summary
                else:
                    return getattr(summary, attr, default)
        return default

    CHECK_RUNNER_DEV_PATTERN = re.compile(".*foursight.*development.*CheckRunner.*")
    CHECK_RUNNER_PROD_PATTERN = re.compile(".*foursight.*production.*CheckRunner.*")

    ENCACHE_RUNNER_NAME = True

    @classmethod
    def discover_foursight_check_runner_name(cls, stage, encache=ENCACHE_RUNNER_NAME):

        check_runner = os.environ.get('CHECK_RUNNER')

        if check_runner:
            return check_runner

        # Prod has its own check runner, distinct from dev and test, though .get_stage() will have converted
        # 'test' to 'dev' by this point anyway. Still, this code is tolerant of not doing that...
        name_pattern = cls.CHECK_RUNNER_PROD_PATTERN if stage == 'prod' else cls.CHECK_RUNNER_DEV_PATTERN
        lambda_client = boto3.client('lambda')
        candidates = []
        chunk = lambda_client.list_functions()
        while True:
            entries = chunk['Functions']
            for entry in entries:
                name = entry['FunctionName']
                if name_pattern.match(name):
                    candidates.append(name)
            next_marker = chunk.get('NextMarker')
            if not next_marker:
                break
            chunk = lambda_client.list_functions(Marker=next_marker)
        if len(candidates) == 1:
            check_runner = candidates[0]
            logger.warning(f"CHECK_RUNNER inferred to be {check_runner}")
        else:
            logger.error(f"CHECK_RUNNER cannot be inferred"
                         f" from {conjoined_list(candidates, nothing='no matches')}.")

        if encache:
            # Discovery is slow. We could encache this value so it's faster next time. I defaulted this to off for now.
            logger.warning(f"Setting environment variable CHECK_RUNNER={check_runner}.")
            os.environ['CHECK_RUNNER'] = check_runner

        return check_runner


class C4OrchestrationManager(AbstractOrchestrationManager):
    """This will manage stacks like we use in the CGAP orchestration."""

    COMMON_STACK_PREFIX = 'c4-'

    STACK_NAME_TOKEN_EXTRACTION_PATTERN = re.compile(f"^{re.escape(COMMON_STACK_PREFIX)}([^-]*)(?:-.*|)$")


class AwsebOrchestrationManager(AbstractOrchestrationManager):
    # NOTE: This is not needed by CGAP orchestration, but illustrates how to generalize this facility for other uses.

    COMMON_STACK_PREFIX = 'awseb-e-'

    STACK_NAME_TOKEN_EXTRACTION_PATTERN = re.compile(f"^{re.escape(COMMON_STACK_PREFIX)}(.*)-stack$")
