# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""General utilties for Cloud Domains commands."""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import re

from apitools.base.py import encoding

from googlecloudsdk.api_lib.domains import operations
from googlecloudsdk.api_lib.domains import registrations
from googlecloudsdk.command_lib.domains import flags
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
from googlecloudsdk.core import yaml
from googlecloudsdk.core.console import console_io

import six

LOCATIONS_COLLECTION = 'domains.projects.locations'
OPERATIONS_COLLECTION = 'domains.projects.locations.operations'
REGISTRATIONS_COLLECTION = 'domains.projects.locations.registrations'
_PROJECT = lambda: properties.VALUES.core.project.Get(required=True)


def RegistrationsUriFunc(resource):
  return ParseRegistration(resource.name).SelfLink()


def AssertRegistrationOperational(registration):
  messages = registrations.GetMessagesModule()

  if registration.state not in [
      messages.Registration.StateValueValuesEnum.ACTIVE,
      messages.Registration.StateValueValuesEnum.SUSPENDED
  ]:
    raise exceptions.Error(
        'The registration resource must be in state ACTIVE or SUSPENDED, '
        'not \'{}\'.'.format(registration.state))


def ParseMessageFromYamlFile(path, message_type, error_message):
  """Parse a Yaml file.

  Args:
    path: Yaml file path. If path is None returns None.
    message_type: Message type to parse YAML into.
    error_message: Error message to print in case of parsing error.

  Returns:
    parsed message of type message_type.
  """
  if path is None:
    return None
  raw_message = yaml.load_path(path)
  try:
    parsed_message = encoding.PyValueToMessage(message_type, raw_message)
  except Exception as e:
    # This error may be slightly different in Py2 and Py3.
    raise exceptions.Error('{}: {}'.format(error_message, e))

  unknown_fields = []
  for message in encoding.UnrecognizedFieldIter(parsed_message):
    outer_message = ''.join([edge.field + '.' for edge in message[0]])
    unknown_fields += [outer_message + field for field in message[1]]
  unknown_fields.sort()
  if unknown_fields:
    raise exceptions.Error(
        ('{}.\nProblematic fields: \'{}\'').format(error_message,
                                                   ', '.join(unknown_fields)))

  return parsed_message


def NormalizeResourceName(domain):
  """Normalizes domain name in resource name."""
  parts = domain.split('/')
  parts[-1] = NormalizeDomainName(parts[-1])
  return '/'.join(parts)


def NormalizeDomainName(domain):
  """Normalizes domain name (including punycoding)."""
  if not domain:
    raise exceptions.Error('Empty domain name')
  try:
    normalized = domain.encode('idna').decode()  # To Punycode
    normalized = normalized.lower().rstrip('.')
  except UnicodeError as e:
    raise exceptions.Error('Invalid domain name \'{}\': {}.'.format(domain, e))
  return normalized


def PunycodeToUnicode(domain):
  return domain.encode('utf-8').decode('idna')


def ValidateDomainName(domain):
  if not domain:
    return False
  # TODO(b/146630685): Find a library function for FQDN validation.
  pattern = r'^[a-z0-9-]+(\.[a-z0-9-]+)+\.{0,1}$'
  if not re.match(pattern, domain) or '..' in domain:
    return False
  return True


def ValidateNonEmpty(s):
  return s is not None and bool(s.strip())


def ValidateRegionCode(rc):
  return rc is not None and len(rc) == 2 and rc.isalpha() and rc.isupper()


def ValidateEmail(email):
  if not email:
    return False
  # TODO(b/146630685): Find a library function for email validation.
  pattern = r'^[^@\s]+@[^@\s]+\.[^@\s]+$'
  return bool(re.match(pattern, email))


def PromptWithValidator(prompt_string,
                        validator,
                        error_message,
                        message=None,
                        default=None):
  """Prompt for user input and validate output.

  Args:
    prompt_string: Message to print in the line with prompt.
    validator: Validation function (str) -> bool.
    error_message: Message to print if provided value is not correct.
    message: Optional message to print before prompt.
    default: Optional default value.

  Returns:
    Valid user provided value or default if not None and user chose it.
  """
  # TODO(b/124427180): Use console_io.PromptWithValidator.
  if message:
    log.status.Print(message)
  while True:
    if default is not None:
      answer = console_io.PromptWithDefault(
          message=prompt_string, default=default)
      if not answer:
        return default
    else:
      answer = console_io.PromptResponse(prompt_string)
    if validator(answer):
      return answer
    else:
      log.status.Print(error_message)


def GetRegistry():
  registry = resources.REGISTRY.Clone()
  registry.RegisterApiByName('domains', 'v1alpha2')
  return registry


def ParseRegistration(registration):
  return GetRegistry().Parse(
      registration,
      params={
          'projectsId': _PROJECT,
          'locationsId': 'global'
      },
      collection=REGISTRATIONS_COLLECTION)


def ParseOperation(operation):
  return GetRegistry().Parse(
      operation,
      params={
          'projectsId': _PROJECT,
          'locationsId': 'global'
      },
      collection=OPERATIONS_COLLECTION)


def DomainNamespace(domain):
  # Return everything after the first encountered dot.
  # This is needed to accommodate two-level domains like .co.uk.
  return domain[domain.find('.'):]


def ParseTransferLockState(transfer_lock_state):
  if transfer_lock_state is None:
    return None
  return flags.TRANSFER_LOCK_ENUM_MAPPER.GetEnumForChoice(transfer_lock_state)


def PromptForTransferLockState(transfer_lock=None):
  """Prompts the user for new transfer lock state."""
  if transfer_lock is not None:
    log.status.Print('Your current Transfer Lock state is: {}'.format(
        six.text_type(transfer_lock)))

  options = list(flags.TRANSFER_LOCK_ENUM_MAPPER.choices)
  index = console_io.PromptChoice(
      options=options,
      cancel_option=True,
      default=len(options),  # Additional 'cancel' option.
      message='Specify new transfer lock state')
  if index >= len(options):
    return None
  return ParseTransferLockState(options[index])


def TransformMoneyType(r):
  if r is None:
    return None
  dr = r
  if not isinstance(dr, dict):
    dr = encoding.MessageToDict(r)
  return '{}.{:02d} {}'.format(dr['units'], int(dr.get('nanos', 0) / (10**7)),
                               dr.get('currencyCode', ''))


def _ParseMoney(money):
  """Parses money string as tuple (units, cents, currency)."""
  match = re.match(r'^(\d+|\d+\.\d{2})\s*([A-Z]{3})$', money)
  if match:
    number, s = match.groups()
  else:
    raise ValueError('Value could not be parsed as number + currency code')
  if '.' in number:
    index = number.find('.')
    return int(number[:index]), int(number[index + 1:]), s
  else:
    return int(number), 0, s


def ParseYearlyPrice(price_string):
  """Parses money string as type Money."""
  if not price_string:
    return None
  try:
    units, cents, currency = _ParseMoney(price_string)
  except ValueError:
    raise exceptions.Error(
        'Yearly price \'{}\' is not valid.'.format(price_string))

  if currency == '$':
    currency = 'USD'

  messages = registrations.GetMessagesModule()
  return messages.Money(
      units=int(units), nanos=cents * 10**7, currencyCode=currency)


def EqualPrice(x, y):
  if x.nanos is None:
    x.nanos = 0
  if y.nanos is None:
    y.nanos = 0
  return x == y


def PromptForYearlyPriceAck(price):
  """Asks the user to accept the yearly price."""
  ack = console_io.PromptContinue(
      'Yearly price: {}\n'.format(TransformMoneyType(price)),
      prompt_string='Do you agree to pay this yearly price for your domain',
      throw_if_unattended=True,
      cancel_on_no=True,
      default=False)
  if ack:
    return price
  else:
    return None


def ParseRegisterNotices(notices):
  """Parses registration notices.

  Args:
    notices: list of notices (lowercase-strings).

  Returns:
    Pair (public privacy ack: bool, hsts ack: bool).
  """
  if not notices:
    return False, False
  return 'public-contact-data-acknowledgement' in notices, 'hsts-preloaded' in notices


def PromptForHSTSAck(domain):
  ack = console_io.PromptContinue(
      ('{} is a secure namespace. You may purchase {} now but it will '
       'require an SSL certificate for website connection.').format(
           DomainNamespace(domain), domain),
      throw_if_unattended=True,
      cancel_on_no=True,
      default=False)
  return ack


def WaitForOperation(response, asynchronous):
  """Handles waiting for the operation and printing information about it.

  Args:
    response: Response from the API call
    asynchronous: If true, do not wait for the operation

  Returns:
    The last information about the operation.
  """
  operation_ref = ParseOperation(response.name)
  if asynchronous:
    log.status.Print('Started \'{}\''.format(operation_ref.Name()))
  else:
    message = 'Waiting for \'{}\' to complete'
    operations_client = operations.Client.FromApiVersion('v1alpha2')
    response = operations_client.WaitForOperation(
        operation_ref, message.format(operation_ref.Name()))
  return response
