"""
Contains sync flow implementation for Auto Dependency Layer
"""
import hashlib
import logging
import os
import tempfile
import uuid
from typing import TYPE_CHECKING, Dict, List, Optional, cast

from samcli.lib.bootstrap.nested_stack.nested_stack_builder import NestedStackBuilder
from samcli.lib.bootstrap.nested_stack.nested_stack_manager import NestedStackManager
from samcli.lib.build.build_graph import BuildGraph
from samcli.lib.package.utils import make_zip_with_lambda_permissions
from samcli.lib.providers.provider import Function, Stack
from samcli.lib.providers.sam_function_provider import SamFunctionProvider
from samcli.lib.sync.exceptions import (
    InvalidRuntimeDefinitionForFunction,
    MissingFunctionBuildDefinition,
    NoLayerVersionsFoundError,
)
from samcli.lib.sync.flows.layer_sync_flow import AbstractLayerSyncFlow
from samcli.lib.sync.flows.zip_function_sync_flow import ZipFunctionSyncFlow
from samcli.lib.sync.sync_flow import SyncFlow
from samcli.lib.utils.hash import file_checksum

if TYPE_CHECKING:  # pragma: no cover
    from samcli.commands.build.build_context import BuildContext
    from samcli.commands.deploy.deploy_context import DeployContext
    from samcli.commands.sync.sync_context import SyncContext

LOG = logging.getLogger(__name__)


class AutoDependencyLayerSyncFlow(AbstractLayerSyncFlow):
    """
    Auto Dependency Layer, Layer Sync flow.
    It creates auto dependency layer files out of function dependencies, and syncs layer code and then updates
    the function configuration with new layer version

    This flow is not instantiated from factory method, please see AutoDependencyLayerParentSyncFlow
    """

    _function_identifier: str
    _build_graph: Optional[BuildGraph]

    def __init__(
        self,
        function_identifier: str,
        build_graph: BuildGraph,
        build_context: "BuildContext",
        deploy_context: "DeployContext",
        sync_context: "SyncContext",
        physical_id_mapping: Dict[str, str],
        stacks: List[Stack],
    ):
        super().__init__(
            NestedStackBuilder.get_layer_logical_id(function_identifier),
            build_context,
            deploy_context,
            sync_context,
            physical_id_mapping,
            stacks,
        )
        self._function_identifier = function_identifier
        self._build_graph = build_graph

    def set_up(self) -> None:
        super().set_up()

        # find layer's physical id
        layer_name = NestedStackBuilder.get_layer_name(self._deploy_context.stack_name, self._function_identifier)
        layer_versions = self._lambda_client.list_layer_versions(LayerName=layer_name).get("LayerVersions", [])
        if not layer_versions:
            raise NoLayerVersionsFoundError(layer_name)
        self._layer_arn = layer_versions[0].get("LayerVersionArn").rsplit(":", 1)[0]

    def gather_resources(self) -> None:
        function_build_definitions = cast(BuildGraph, self._build_graph).get_function_build_definitions()
        if not function_build_definitions:
            raise MissingFunctionBuildDefinition(self._function_identifier)

        self._artifact_folder = NestedStackManager.update_layer_folder(
            self._build_context.build_dir,
            function_build_definitions[0].dependencies_dir,
            self._layer_identifier,
            self._function_identifier,
            self._get_compatible_runtimes()[0],
        )
        zip_file_path = os.path.join(tempfile.gettempdir(), "data-" + uuid.uuid4().hex)
        self._zip_file = make_zip_with_lambda_permissions(zip_file_path, self._artifact_folder)
        self._local_sha = file_checksum(cast(str, self._zip_file), hashlib.sha256())

    def _get_dependent_functions(self) -> List[Function]:
        function = SamFunctionProvider(cast(List[Stack], self._stacks)).get(self._function_identifier)
        return [function] if function else []

    def _get_compatible_runtimes(self) -> List[str]:
        function = SamFunctionProvider(cast(List[Stack], self._stacks)).get(self._function_identifier)
        if not function or not function.runtime:
            raise InvalidRuntimeDefinitionForFunction(self._function_identifier)
        return [function.runtime]


class AutoDependencyLayerParentSyncFlow(ZipFunctionSyncFlow):
    """
    Parent sync flow for auto dependency layer

    It builds function with regular ZipFunctionSyncFlow, and then adds _AutoDependencyLayerSyncFlow to start syncing
    dependency layer.
    """

    def gather_dependencies(self) -> List[SyncFlow]:
        """
        Return auto dependency layer sync flow along with parent dependencies
        """
        parent_dependencies = super().gather_dependencies()

        function_build_definitions = cast(BuildGraph, self._build_graph).get_function_build_definitions()
        if not function_build_definitions:
            raise MissingFunctionBuildDefinition(self._function.name)

        # don't queue up auto dependency layer, if dependencies are not changes
        need_dependency_layer_sync = function_build_definitions[0].download_dependencies
        if need_dependency_layer_sync:
            parent_dependencies.append(
                AutoDependencyLayerSyncFlow(
                    self._function_identifier,
                    cast(BuildGraph, self._build_graph),
                    self._build_context,
                    self._deploy_context,
                    self._sync_context,
                    self._physical_id_mapping,
                    cast(List[Stack], self._stacks),
                )
            )
        return parent_dependencies

    @staticmethod
    def _combine_dependencies() -> bool:
        return False
