# -*- coding: utf-8 -*- #
# Copyright 2023 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Deploys a function locally."""


import json
import textwrap
import typing

from googlecloudsdk.api_lib.functions.v2 import client as client_v2
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.calliope import parser_extensions
from googlecloudsdk.command_lib.functions import flags as flag_util
from googlecloudsdk.command_lib.functions import source_util
from googlecloudsdk.command_lib.functions.local import flags as local_flags
from googlecloudsdk.command_lib.functions.local import util
from googlecloudsdk.command_lib.functions.v2.deploy import env_vars_util
from googlecloudsdk.command_lib.util.args import map_util
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import files as file_utils


_LOCAL_DEPLOY_MESSAGE = textwrap.dedent("""\
    Your function {name} is serving at localhost:{port}.

    To call this locally deployed function using gcloud:
    gcloud alpha functions local call {name} [--data=DATA] | [--cloud-event=CLOUD_EVENT]

    To call local HTTP functions using curl:
    curl -m 60 -X POST localhost:{port} -H "Content-Type: application/json" -d '{{}}'

    To call local CloudEvent and Background functions using curl, please see:
    https://cloud.google.com/functions/docs/running/calling
    """)
_DETAILED_HELP = {
    'DESCRIPTION': """
        `{command}` Deploy a Google Cloud Function locally.
        """,
}
_REGION = 'us-central1'


@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Deploy(base.Command):
  """Deploy a Google Cloud Function locally."""

  detailed_help = _DETAILED_HELP

  @staticmethod
  def Args(parser):
    local_flags.AddDeploymentNameFlag(parser)
    local_flags.AddPortFlag(parser)
    local_flags.AddBuilderFlag(parser)

    flag_util.AddEntryPointFlag(parser)
    flag_util.AddRuntimeFlag(parser)
    flag_util.AddIgnoreFileFlag(parser)
    # TODO(b/296916846): Add memory and CPU flags
    flag_util.AddSourceFlag(parser)

    env_vars_util.AddBuildEnvVarsFlags(parser)
    env_vars_util.AddUpdateEnvVarsFlags(parser)

    # Add NO-OP gen2 flag for user familiarity
    flag_util.AddGen2Flag(parser, hidden=True)

  def Run(self, args):
    util.ValidateDependencies()
    labels = self._CreateAndUpdateLabels(args)

    client = client_v2.FunctionsClient(release_track=base.ReleaseTrack.ALPHA)
    runtimes = sorted({r.name for r in client.ListRuntimes(_REGION).runtimes})
    flags = labels.get('flags')
    self._ValidateFlags(flags, runtimes)

    name = args.NAME[0]

    with file_utils.TemporaryDirectory() as tmp_dir:
      path = source_util.CreateSourcesZipFile(
          tmp_dir,
          source_path=flags.get('source', '.'),
          ignore_file=flags.get('ignore-file')
      )
      util.RunPack(name=name,
                   builder=flags.get('--builder'),
                   runtime=flags.get('--runtime'),
                   entry_point=flags.get('--entry-point'),
                   path=path,
                   build_env_vars=labels.get('build-env-vars'))

    util.RunDockerContainer(name=name,
                            port=flags.get('--port', '8080'),
                            env_vars=labels.get('env-vars'),
                            labels=labels)

    log.status.Print(_LOCAL_DEPLOY_MESSAGE.format(
        name=name, port=flags.get('--port', '8080')))

  def _CreateAndUpdateLabels(
      self, args: parser_extensions.Namespace) -> typing.Dict[str, typing.Any]:
    labels = {}

    old_labels = util.GetDockerContainerLabels(args.NAME[0])
    old_flags = json.loads(old_labels.get('flags', '{}'))
    old_env_vars = json.loads(old_labels.get('env-vars', '{}'))
    old_build_env_vars = json.loads(old_labels.get('build-env-vars', '{}'))

    labels['flags'] = self._ApplyNewFlags(args, old_flags)

    env_vars = map_util.GetMapFlagsFromArgs('env-vars', args)
    labels['env-vars'] = map_util.ApplyMapFlags(old_env_vars, **env_vars)

    build_env_vars = map_util.GetMapFlagsFromArgs('build-env-vars', args)
    labels['build-env-vars'] = map_util.ApplyMapFlags(
        old_build_env_vars, **build_env_vars)

    return labels

  def _ApplyNewFlags(self, args: parser_extensions.Namespace,
                     old_flags: typing.Dict[str, str]) -> typing.Dict[str, str]:
    flags = {**old_flags, **args.GetSpecifiedArgs()}
    flags = {k: v for (k, v) in flags.items()
             if not('NAME' in k or 'env-vars' in k)}
    return flags

  def _ValidateFlags(self, flags: typing.Dict[str, str],
                     runtimes: typing.Set[str]) -> None:
    if '--entry-point' not in flags:
      raise exceptions.RequiredArgumentException(
          '--entry-point', 'Flag `--entry-point` required.'
      )
    # Require runtime if builder not specified.
    if '--builder' not in flags and '--runtime' not in flags:
      flags['--runtime'] = self._PromptUserForRuntime(runtimes)
    if flags.get('--runtime') not in runtimes:
      log.out.Print('--runtime must be one of the following:')
      flags['--runtime'] = self._PromptUserForRuntime(runtimes)

  def _PromptUserForRuntime(self, runtimes: typing.Set[str]) -> str:
    if not console_io.CanPrompt():
      raise exceptions.RequiredArgumentException(
          '--runtime', 'Flag `--runtime` required when builder not specified.'
      )
    idx = console_io.PromptChoice(
        runtimes, message='Please select a runtime:\n'
    )
    return runtimes[idx]
