"""
Context information passed to each CLI command
"""

import logging
import uuid
from typing import List, Optional, cast

import click
from rich.console import Console

from samcli.cli.formatters import RootCommandHelpTextFormatter
from samcli.commands.exceptions import AWSServiceClientError
from samcli.lib.utils.sam_logging import (
    LAMBDA_BULDERS_LOGGER_NAME,
    SAM_CLI_FORMATTER_WITH_TIMESTAMP,
    SAM_CLI_LOGGER_NAME,
    SamCliLogger,
)


class Context:
    """
    Top level context object for the CLI. Exposes common functionality required by a CLI, including logging,
    environment config parsing, debug logging etc.

    This object is passed by Click to every command that adds the proper annotation.
    Read this for more details on Click Context - http://click.pocoo.org/5/commands/#nested-handling-and-contexts
    Each command gets its own context object, but linked to both parent and child command's context, like a Linked List.

    This class itself does not rely on how Click works. It is just a plain old Python class that holds common
    properties used by every CLI command.
    """

    _session_id: str
    formatter_class = RootCommandHelpTextFormatter

    def __init__(self):
        """
        Initialize the context with default values
        """
        self._debug = False
        self._aws_region = None
        self._aws_profile = None
        self._session_id = str(uuid.uuid4())
        self._experimental = False
        self._exception = None
        self._console = Console()

    @property
    def console(self):
        return self._console

    @property
    def exception(self):
        return self._exception

    @exception.setter
    def exception(self, value: Exception):
        """
        Save the exception to handler in the future

        Parameter
        ---------
        value: Exception
            The exception to save for future handling
        """
        self._exception = value

    @property
    def debug(self):
        return self._debug

    @debug.setter
    def debug(self, value):
        """
        Turn on debug logging if necessary.

        :param value: Value of debug flag
        """
        self._debug = value

        if self._debug:
            # Turn on debug logging and display timestamps
            sam_cli_logger = logging.getLogger(SAM_CLI_LOGGER_NAME)
            lambda_builders_logger = logging.getLogger(LAMBDA_BULDERS_LOGGER_NAME)
            SamCliLogger.configure_logger(sam_cli_logger, SAM_CLI_FORMATTER_WITH_TIMESTAMP, logging.DEBUG)
            SamCliLogger.configure_logger(lambda_builders_logger, SAM_CLI_FORMATTER_WITH_TIMESTAMP, logging.DEBUG)

    @property
    def region(self):
        return self._aws_region

    @region.setter
    def region(self, value):
        """
        Set AWS region
        """
        self._aws_region = value
        self._refresh_session()

    @property
    def profile(self):
        return self._aws_profile

    @profile.setter
    def profile(self, value):
        """
        Set AWS profile for credential resolution
        """
        self._aws_profile = value
        self._refresh_session()

    @property
    def session_id(self) -> str:
        """
        Returns the ID of this command session. This is a randomly generated UUIDv4 which will not change until the
        command terminates.
        """
        return self._session_id

    @property
    def experimental(self):
        return self._experimental

    @experimental.setter
    def experimental(self, value):
        self._experimental = value

    @property
    def command_path(self):
        """
        Returns the full path of the command as invoked ex: "sam local generate-event s3 put". Wrapper to
        https://click.palletsprojects.com/en/7.x/api/#click.Context.command_path

        Returns
        -------
        str
            Full path of the command invoked
        """

        # Uses Click's Core Context. Note, this is different from this class, also confusingly named `Context`.
        # Click's Core Context object is the one that contains command path information.
        click_core_ctx = click.get_current_context()
        if click_core_ctx:
            return click_core_ctx.command_path

        return None

    @property
    def template_dict(self):
        """
        Returns the template_dictionary from click context.
        Returns
        -------
        dict
            Template as dictionary

        """
        click_core_ctx = click.get_current_context()
        try:
            if click_core_ctx:
                return click_core_ctx.template_dict
        except AttributeError:
            return None

        return None

    @staticmethod
    def get_current_context() -> Optional["Context"]:
        """
        Get the current Context object from Click's context stacks. This method is safe to run within the
        actual command's handler that has a ``@pass_context`` annotation. Outside of the handler, you run
        the risk of creating a new Context object which is entirely different from the Context object used by your
        command.
         .. code:
            @pass_context
            def my_command_handler(ctx):
                 # You will get the right context from within the command handler. This will also work from any
                # downstream method invoked as part of the handler.
                 this_context = Context.get_current_context()
                assert ctx == this_context
         Returns
        -------
        samcli.cli.context.Context
            Instance of this object, if we are running in a Click command. None otherwise.
        """

        # Click has the concept of Context stacks. Think of them as linked list containing custom objects that are
        # automatically accessible at different levels. We start from the Core Click context and discover the
        # SAM CLI command-specific Context object which contains values for global options used by all commands.
        #
        # https://click.palletsprojects.com/en/7.x/complex/#ensuring-object-creation
        #

        click_core_ctx = click.get_current_context()
        if click_core_ctx:
            return cast("Context", click_core_ctx.find_object(Context) or click_core_ctx.ensure_object(Context))

        return None

    def _refresh_session(self):
        """
        Update boto3's default session by creating a new session based on values set in the context. Some properties of
        the Boto3's session object are read-only. Therefore when Click parses new AWS session related properties (like
        region & profile), it will call this method to create a new session with latest values for these properties.
        """
        import boto3
        from botocore import credentials, exceptions, session

        try:
            botocore_session = session.get_session()
            boto3.setup_default_session(
                botocore_session=botocore_session, region_name=self._aws_region, profile_name=self._aws_profile
            )
            # get botocore session and setup caching for MFA based credentials
            botocore_session.get_component("credential_provider").get_provider(
                "assume-role"
            ).cache = credentials.JSONFileCache()

        except exceptions.ProfileNotFound as ex:
            raise AWSServiceClientError(str(ex)) from ex


def get_cmd_names(cmd_name, ctx) -> List[str]:
    """
    Given the click core context, return a list representing all the subcommands passed to the CLI

    Parameters
    ----------
    cmd_name : str
        name of current command
    ctx : click.Context
        click context

    Returns
    -------
    list(str)
        List containing subcommand names. Ex: ["local", "start-api"]

    """
    if not ctx:
        return []

    if ctx and not getattr(ctx, "parent", None):
        return [ctx.info_name]
    # Find parent of current context
    _parent = ctx.parent
    _cmd_names = []
    # Need to find the total set of commands that current command is part of.
    if cmd_name != ctx.info_name:
        _cmd_names = [cmd_name]
    _cmd_names.append(ctx.info_name)
    # Go through all parents till a parent of a context exists.
    while _parent.parent:
        info_name = _parent.info_name
        _cmd_names.append(info_name)
        _parent = _parent.parent

    # Make sure the output reads natural. Ex: ["local", "start-api"]
    _cmd_names.reverse()
    return _cmd_names
