# Copyright 2014 Google Inc. 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.

"""Credentials for use with the developer shell."""

import json
import os
import threading

from apitools.base.protorpclite import messages

from googlecloudsdk.core import config
from oauth2client import client


DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
DEVSHELL_ENV_IPV6_ENABLED = 'DEVSHELL_CLIENT_PORT_IPV6_ENABLED'


class Error(Exception):
  """Errors for this module."""
  pass


class CommunicationError(Error):
  """Errors for communication with the access token server."""


class NoDevshellServer(Error):
  """Error when no devshell server can be contacted."""


def MessageToPBLiteList(msg):
  """Convert a protorpc Message into a list suitable for PBLite.

  Args:
    msg: messages.Message, The Message to be turned into a list.

  Returns:
    [obj], A list that has only primitives or other lists suitable for
        PBLite serialization.
  """
  index_keys = dict([(f.number, f.name) for f in msg.all_fields()])
  if not index_keys:
    return []

  max_index = max(index_keys.keys())
  json_list = [None] * max_index

  for index, key in index_keys.iteritems():
    value = getattr(msg, key, None)
    if isinstance(value, messages.Message):
      value = MessageToPBLiteList(value)
    json_list[index-1] = value

  return json_list


def PBLiteListToMessage(pbl, message_class):
  """Convert a PBLite list into a type of the provided class.

  Args:
    pbl: [obj], A list deserialized from a PBLite message.
    message_class: type, The messages.Message subclass to be used to create
        the message.

  Returns:
    messages.Message, The deserialized message object.

  Raises:
    ValueError: If the list is too small for the message type.
  """
  if not isinstance(pbl, list):
    raise ValueError(
        '{obj} of type {type} is not a list'.format(
            obj=pbl, type=type(pbl)))
  kwargs = {}
  for field in message_class.all_fields():
    if field.number > len(pbl):
      raise ValueError(
          'PBLite list {list} is too small for {type}'.format(
              list=repr(pbl),
              type=message_class))
    value = pbl[field.number-1]
    if issubclass(field.type, messages.Message):
      value = PBLiteListToMessage(value, field.type)
    kwargs[field.name] = value
  return message_class(**kwargs)


def JSONToMessage(data, message_class):
  pbl = json.loads(data)
  return PBLiteListToMessage(pbl, message_class)


def MessageToJSON(msg):
  pbl = MessageToPBLiteList(msg)
  return json.dumps(pbl)


class CredentialInfoRequest(messages.Message):
  pass


class CredentialInfoResponse(messages.Message):
  user_email = messages.StringField(1, required=True)
  project_id = messages.StringField(2)
  access_token = messages.StringField(3)


def _SendRecv(request):
  """Communicate with the devshell access token service."""
  port = int(os.getenv(DEVSHELL_ENV, 0))
  if not port:
    raise NoDevshellServer()
  return _SendRecvPort(request, port)


def _Memoize(func):
  """Simple thread-safe memoization decorator.

  Uses a repr() of the params. This will be ok unless there is a custom __repr__
  that obscures important information.

  Args:
    func: The function to be memoized.

  Returns:
    A memoized version of func.

  """
  l = threading.Lock()
  cache = {}
  def _Caller(*args, **kwargs):
    with l:
      params = repr((args, kwargs))
      try:
        return cache[params]
      except KeyError:
        result = func(*args, **kwargs)
        cache[params] = result
        return result
  return _Caller


@_Memoize
def _SendRecvPort(request, port):
  """Communicate with the devshell access token service."""

  # pylint:disable=g-import-not-at-top, Delay for performance.
  import socket

  data = MessageToJSON(request)
  n = len(data)
  nstr = '%d' % n
  if len(nstr) > 5:
    raise ValueError('length too long')

  if socket.has_ipv6 and os.getenv(DEVSHELL_ENV_IPV6_ENABLED) is not None:
    s = socket.socket(socket.AF_INET6)
  else:
    s = socket.socket()

  s.connect(('localhost', port))
  msg = '%s\n%s' % (nstr, data)
  s.sendall(msg)

  resp_buffer = ''
  resp_1 = s.recv(6)
  if '\n' not in resp_1:
    raise CommunicationError('saw no newline in the first 6 bytes')
  nstr, extra = resp_1.split('\n', 1)
  resp_buffer = extra
  n = int(nstr)
  to_read = n-len(extra)
  if to_read > 0:
    resp_buffer += s.recv(to_read, socket.MSG_WAITALL)

  return JSONToMessage(resp_buffer, CredentialInfoResponse)


def Project():
  """Fetch the project from a devshell auth proxy.

  Returns:
    The project ID or None, if no devshell proxy was listening.
  """
  request = CredentialInfoRequest()
  try:
    response = _SendRecv(request)
  except Exception:  # pylint:disable=broad-except
    return None
  return response.project_id


def DefaultAccount():
  """Fetch the account from a devshell auth proxy.

  Returns:
    The project ID or None, if no devshell proxy was listening.
  """
  request = CredentialInfoRequest()
  try:
    response = _SendRecv(request)
  except Exception:  # pylint:disable=broad-except
    return None
  return response.user_email


class DevshellCredentials(client.OAuth2Credentials):

  def __init__(self, **kwargs):
    self.__dict__.update(**kwargs)
    self._refresh(None)

  def _refresh(self, http):
    request = CredentialInfoRequest()
    self.devshell_response = _SendRecv(request)
    self.access_token = self.devshell_response.access_token


def LoadDevshellCredentials():
  """Load devshell credentials from the proxy.

  Also sets various attributes on the credential object expected by other
  parties.

  Returns:
    DevshellCredentials, if available. If the proxy can't be reached or returns
    garbage data, this function returns None.
  """
  try:
    return DevshellCredentials(
        user_agent=config.CLOUDSDK_USER_AGENT,)
  except Exception:  # pylint:disable=broad-except, any problem means None
    return None


def IsDevshellEnvironment():
  port = int(os.getenv(DEVSHELL_ENV, 0))
  return port != 0
