# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Anthos command library functions and utilities for the anthoscli binary."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import base64
import copy
import json
import os

from googlecloudsdk.command_lib.anthos import flags
from googlecloudsdk.command_lib.anthos.common import file_parsers
from googlecloudsdk.command_lib.anthos.common import messages
from googlecloudsdk.command_lib.util.anthos import binary_operations
from googlecloudsdk.core import exceptions as c_except
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.credentials import store as c_store
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms

import requests
import six
from six.moves import urllib

DEFAULT_ENV_ARGS = {'COBRA_SILENCE_USAGE': 'true'}

DEFAULT_LOGIN_CONFIG_PATH = {
    platforms.OperatingSystem.LINUX.id:
        '~/.config/google/anthos/kubectl-anthos-config.yaml',
    platforms.OperatingSystem.MACOSX.id:
        '~/Library/Preferences/google/anthos/kubectl-anthos-config.yaml',
    platforms.OperatingSystem.WINDOWS.id:
        os.path.join('%APPDATA%', 'google', 'anthos',
                     'kubectl-anthos-config.yaml')
}


def GetEnvArgsForCommand(extra_vars=None, exclude_vars=None):
  """Return an env dict to be passed on command invocation."""
  env = copy.deepcopy(os.environ)
  env.update(DEFAULT_ENV_ARGS)
  if extra_vars:
    env.update(extra_vars)
  if exclude_vars:
    for k in exclude_vars:
      env.pop(k)
  return env


class AnthosAuthException(c_except.Error):
  """Base Exception for auth issues raised by gcloud anthos surface."""


def RelativePkgPathFromFullPath(path):
  """Splits full path into relative(basename) path and parent dir."""
  normpath = os.path.normpath(path)
  rel_path = os.path.basename(normpath)
  parent_dir = os.path.dirname(normpath) or rel_path
  return rel_path, parent_dir


class AnthosCliWrapper(binary_operations.StreamingBinaryBackedOperation):
  """Binary operation wrapper for anthoscli commands."""

  def __init__(self, **kwargs):
    custom_errors = {
        'MISSING_EXEC': messages.MISSING_BINARY.format(binary='anthoscli')
    }
    super(AnthosCliWrapper, self).__init__(
        binary='anthoscli', custom_errors=custom_errors, **kwargs)

  def _ParseGetArgs(self, repo_uri, local_dest, file_pattern=None, **kwargs):
    del kwargs  # Not Used Here
    exec_args = ['get', repo_uri, local_dest]

    if file_pattern:
      exec_args.extend(['--pattern', file_pattern])

    return exec_args

  def _ParseUpdateArgs(self,
                       local_dir,
                       repo_uri=None,
                       strategy=None,
                       dry_run=False,
                       verbose=False,
                       **kwargs):
    del kwargs  # Not Used here

    exec_args = ['update', local_dir]
    if repo_uri:
      exec_args.extend(['--repo', repo_uri])

    if dry_run:
      exec_args.append('--dry-run')

    if strategy:
      exec_args.extend(['--strategy', strategy])

    if verbose:
      exec_args.append('--verbose')

    return exec_args

  def _ParseDescribeArgs(self, local_dir, **kwargs):
    del kwargs  # Not Used here
    return ['desc', local_dir]

  def _ParseTags(self, tags):
    return ','.join(['{}={}'.format(x, y) for x, y in six.iteritems(tags)])

  def _ParseInitArgs(self,
                     local_dir,
                     description=None,
                     name=None,
                     tags=None,
                     info_url=None,
                     **kwargs):
    del kwargs  # Not Used here
    package_path = local_dir

    if not package_path.endswith('/'):
      package_path += '/'
    exec_args = ['init', package_path]

    if description:
      exec_args.extend(['--description', description])
    if name:
      exec_args.extend(['--name', name])
    if tags:
      exec_args.extend(['--tag', self._ParseTags(tags)])

    if info_url:
      exec_args.extend(['--url', info_url])

    return exec_args

  def _ParseApplyArgs(self, apply_dir, project, **kwargs):
    del kwargs  # Not Used Here
    exec_args = ['apply', '-f', apply_dir, '--project', project]

    return exec_args

  def _ParseExportArgs(self, cluster, project, location, output_dir, **kwargs):
    del kwargs  # Not Used here

    exec_args = ['export', '-c', cluster, '--project', project]
    if location:
      exec_args.extend(['--location', location])

    if output_dir:
      exec_args.extend(['--output-directory', output_dir])

    return exec_args

  def _ParseArgsForCommand(self, command, **kwargs):
    if command == 'get':
      return self._ParseGetArgs(**kwargs)
    if command == 'update':
      return self._ParseUpdateArgs(**kwargs)
    if command == 'desc':
      return self._ParseDescribeArgs(**kwargs)
    if command == 'init':
      return self._ParseInitArgs(**kwargs)
    if command == 'apply':
      return self._ParseApplyArgs(**kwargs)
    if command == 'export':
      return self._ParseExportArgs(**kwargs)

    raise binary_operations.InvalidOperationForBinary(
        'Invalid Operation [{}] for anthoscli'.format(command))


def GetAuthToken(account, operation, impersonated=False):
  """Generate a JSON object containing the current gcloud auth token."""
  try:
    access_token = c_store.GetFreshAccessToken(
        account, allow_account_impersonation=impersonated)
    output = {
        'auth_token': access_token,
    }
  except Exception as e:  # pylint: disable=broad-except
    raise AnthosAuthException(
        'Error retrieving auth credentials for {operation}: {error}. '.format(
            operation=operation, error=e))
  return json.dumps(output, sort_keys=True)


class AnthosAuthWrapper(binary_operations.StreamingBinaryBackedOperation):
  """Binary operation wrapper for anthoscli commands."""

  def __init__(self, **kwargs):
    custom_errors = {
        'MISSING_EXEC':
            messages.MISSING_AUTH_BINARY.format(binary='kubectl-anthos')
    }
    super(AnthosAuthWrapper, self).__init__(
        binary='kubectl-anthos', custom_errors=custom_errors, **kwargs)

  @property
  def default_config_path(self):
    return files.ExpandHomeAndVars(
        DEFAULT_LOGIN_CONFIG_PATH[platforms.OperatingSystem.Current().id])

  def _ParseLoginArgs(
      self,
      cluster,
      kube_config=None,
      login_config=None,
      login_config_cert=None,
      user=None,
      ldap_user=None,
      ldap_pass=None,
      dry_run=None,
      preferred_auth=None,
      server_url=None,
      no_browser=None,
      remote_bootstrap=None,
      **kwargs
  ):
    del kwargs  # Not Used Here
    exec_args = ['login']
    if cluster:
      exec_args.extend(['--cluster', cluster])
    if kube_config:
      exec_args.extend(['--kubeconfig', kube_config])
    if login_config:
      exec_args.extend(['--login-config', login_config])
    if login_config_cert:
      exec_args.extend(['--login-config-cert', login_config_cert])
    if user:
      exec_args.extend(['--user', user])
    if dry_run:
      exec_args.extend(['--dry-run'])
    if ldap_pass and ldap_user:
      exec_args.extend(
          ['--ldap-username', ldap_user, '--ldap-password', ldap_pass])
    if preferred_auth:
      exec_args.extend(['--preferred-auth', preferred_auth])
    if server_url:
      exec_args.extend(['--server', server_url])
    if no_browser:
      exec_args.extend(['--remote-login'])
    if remote_bootstrap:
      exec_args.extend(['--remote-bootstrap', remote_bootstrap])

    return exec_args

  def _ParseCreateLoginConfigArgs(self,
                                  kube_config,
                                  output_file=None,
                                  merge_from=None,
                                  **kwargs):
    del kwargs  # Not Used Here
    exec_args = ['create-login-config']
    exec_args.extend(['--kubeconfig', kube_config])
    if output_file:
      exec_args.extend(['--output', output_file])
    if merge_from:
      exec_args.extend(['--merge-from', merge_from])
    return exec_args

  def _ParseTokenArgs(self, token_type, cluster, aws_sts_region, id_token,
                      access_token, access_token_expiry, refresh_token,
                      client_id, client_secret, idp_certificate_authority_data,
                      idp_issuer_url, kubeconfig_path, user, **kwargs):
    del kwargs  # Not Used Here
    exec_args = ['token']
    if token_type:
      exec_args.extend(['--type', token_type])
    if cluster:
      exec_args.extend(['--cluster', cluster])
    if aws_sts_region:
      exec_args.extend(['--aws-sts-region', aws_sts_region])
    if id_token:
      exec_args.extend(['--id-token', id_token])
    if access_token:
      exec_args.extend(['--access-token', access_token])
    if access_token_expiry:
      exec_args.extend(['--access-token-expiry', access_token_expiry])
    if refresh_token:
      exec_args.extend(['--refresh-token', refresh_token])
    if client_id:
      exec_args.extend(['--client-id', client_id])
    if client_secret:
      exec_args.extend(['--client-secret', client_secret])
    if idp_certificate_authority_data:
      exec_args.extend(
          ['--idp-certificate-authority-data', idp_certificate_authority_data])
    if idp_issuer_url:
      exec_args.extend(['--idp-issuer-url', idp_issuer_url])
    if kubeconfig_path:
      exec_args.extend(['--kubeconfig-path', kubeconfig_path])
    if user:
      exec_args.extend(['--user', user])

    return exec_args

  def _ParseArgsForCommand(self, command, **kwargs):
    if command == 'login':
      return self._ParseLoginArgs(**kwargs)
    elif command == 'create-login-config':
      return self._ParseCreateLoginConfigArgs(**kwargs)
    elif command == 'version':
      return ['version']
    elif command == 'token':
      return self._ParseTokenArgs(**kwargs)
    else:
      raise binary_operations.InvalidOperationForBinary(
          'Invalid Operation [{}] for kubectl-anthos'.format(command))


def _GetClusterConfig(all_configs, cluster):
  found_clusters = all_configs.FindMatchingItem(
      file_parsers.LoginConfigObject.CLUSTER_NAME_KEY, cluster)
  if len(found_clusters) != 1:
    raise AnthosAuthException(
        'Cluster [{}] not found for config path [{}]'.format(
            cluster, all_configs.file_path))
  return found_clusters.pop()


def _Base64EncodeLdap(username, passwd):
  """Base64 Encode Ldap username and password."""
  enc = lambda s: six.ensure_text(base64.b64encode(six.ensure_binary(s)))
  return enc(username), enc(passwd)


def _GetLdapUserAndPass(cluster_config, auth_name, cluster):
  """Prompt User for Ldap Username and Password."""
  ldap_user = None
  ldap_pass = None
  if not cluster_config.IsLdap():
    return None, None
  # do the prompting
  user_message = ('Please enter the ldap user for '
                  '[{}] on cluster [{}]: '.format(auth_name, cluster))
  pass_message = ('Please enter the ldap password for '
                  '[{}] on cluster [{}]: '.format(auth_name, cluster))
  ldap_user = console_io.PromptWithValidator(
      validator=lambda x: len(x) > 1,
      error_message='Error: Invalid username, please try again.',
      prompt_string=user_message)
  ldap_pass = console_io.PromptPassword(
      pass_message, validation_callable=lambda x: len(x) > 1)
  return _Base64EncodeLdap(ldap_user, ldap_pass)


def GetFileOrURL(cluster_config, certificate_file=True):
  """Parses config input to determine whether URL or File logic should execute.

     Determines whether the cluster_config is a file or URL. If it's a URL, it
     then pulls the contents of the file using a GET request. If it's a
     file, then it expands the file path and returns its contents.

  Args:
    cluster_config: str, A file path or URL for the login-config.
    certificate_file: str, Optional file path to the CA certificate to use with
      the GET request to the URL.

  Raises:
    AnthosAuthException: If the data could not be pulled from the URL.

  Returns:
    parsed_config_fileOrURL, config_contents, and is_url
    parsed_config_fileOrURL: str, returns either the URL that was passed or an
      expanded file path if a file was passed.
      config_contents: str, returns the contents of the file or URL.
    is_url: bool, True if the provided cluster_config input was a URL.
  """

  if not cluster_config:
    return None, None, None

  # Handle if input is URL.
  config_url = urllib.parse.urlparse(cluster_config)
  is_url = config_url.scheme == 'http' or config_url.scheme == 'https'
  if is_url:
    response = requests.get(cluster_config, verify=certificate_file or True)
    if response.status_code != requests.codes.ok:
      raise AnthosAuthException('Request to login-config URL failed with'
                                'response code [{}] and text [{}]: '.format(
                                    response.status_code, response.text))
    return cluster_config, response.text, is_url

  # Handle if input is file.
  expanded_config_path = flags.ExpandLocalDirAndVersion(cluster_config)
  contents = files.ReadFileContents(expanded_config_path)
  return expanded_config_path, contents, is_url


def GetPreferredAuthForCluster(cluster,
                               login_config,
                               config_contents=None,
                               force_update=False,
                               is_url=False):
  """Get preferredAuthentication value for cluster."""
  if not (cluster and login_config):
    return None, None, None

  configs = None
  # If URL, then pass contents directly.
  if is_url:
    if not config_contents:
      raise AnthosAuthException(
          'Config contents were not passed with URL [{}]'.format(login_config))
    configs = file_parsers.YamlConfigFile(
        file_contents=config_contents, item_type=file_parsers.LoginConfigObject)
  # If file, pass contents and location for updating.
  else:
    configs = file_parsers.YamlConfigFile(
        file_contents=config_contents,
        file_path=login_config,
        item_type=file_parsers.LoginConfigObject)

  cluster_config = _GetClusterConfig(configs, cluster)

  try:
    auth_method = cluster_config.GetPreferredAuth()
  except KeyError:
    auth_method = None
  except file_parsers.YamlConfigObjectFieldError:
    # gracefully quit for config versions older than v2alpha1 that
    # do not support 'preferredAuthentication' field.
    return None, None, None
  if not auth_method or force_update:
    providers = cluster_config.GetAuthProviders()
    if not providers:
      raise AnthosAuthException(
          'No Authentication Providers found in [{}]'.format(login_config))
    if len(providers) == 1:
      auth_method = providers.pop()
    else:  # do the prompting
      prompt_message = ('Please select your preferred authentication option '
                        'for cluster [{}]'.format(cluster))
      override_warning = ('. Note: This will overwrite current preferred auth '
                          'method [{}] in config file.')
      # Only print override warning in certain cases.
      if auth_method and force_update and not is_url:
        prompt_message = prompt_message + override_warning.format(auth_method)
      index = console_io.PromptChoice(
          providers, message=prompt_message, cancel_option=True)
      auth_method = providers[index]

    log.status.Print(
        'Setting Preferred Authentication option to [{}]'.format(auth_method))
    cluster_config.SetPreferredAuth(auth_method)
    # Only save to disk if file is specified. Don't want URL failure.
    if login_config and not is_url:
      configs.WriteToDisk()

  ldap_user, ldap_pass = _GetLdapUserAndPass(cluster_config, auth_method,
                                             cluster)
  return auth_method, ldap_user, ldap_pass


def LoginResponseHandler(response, list_clusters_only=False):
  """Handle Login Responses."""
  if response.stdout:
    log.status.Print(response.stdout)

  if response.stderr:
    log.status.Print(response.stderr)

  if response.failed:
    log.error(messages.LOGIN_CONFIG_FAILED_MESSAGE.format(response.stderr))
    return None
  if not list_clusters_only:
    log.status.Print(messages.LOGIN_CONFIG_SUCCESS_MESSAGE)
  return response.stdout
