# -*- coding: utf-8 -*-
# vim: set et sts=2:
##############################################################################
#
# Copyright (c) 2010, 2011, 2012 Vifib SARL and Contributors.
# All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
"""
Simple, easy to (un)marshall classes for slap client/server communication
"""

__all__ = ["slap", "ComputerPartition", "Computer", "SoftwareRelease",
           "SoftwareInstance", "SoftwareProductCollection",
           "Supply", "OpenOrder", "NotFoundError",
           "ResourceNotReady", "ServerError", "ConnectionError"]

import os
import json
import logging
import re
import hashlib
from functools import wraps

import six
from six.moves.urllib import parse

from .util import xml2dict
from slapos.util import loads, dumps, bytes2str

import netaddr
from xml.sax import saxutils
from zope.interface import implementer
from .interface import slap as interface

from uritemplate import expand

import requests
# silence messages like 'Unverified HTTPS request is being made'
requests.packages.urllib3.disable_warnings()
# silence messages like 'Starting connection' that are logged with INFO
urllib3_logger = logging.getLogger('requests.packages.urllib3')
urllib3_logger.setLevel(logging.WARNING)


# XXX fallback_logger to be deprecated together with the old CLI entry points.
fallback_logger = logging.getLogger(__name__)
fallback_handler = logging.StreamHandler()
fallback_logger.setLevel(logging.INFO)
fallback_logger.addHandler(fallback_handler)


DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
COMPUTER_PARTITION_REQUEST_LIST_TEMPLATE_FILENAME = '.slapos-request-transaction-%s'

class SlapDocument:
  def __init__(self, connection_helper=None, hateoas_navigator=None):
    if connection_helper is not None:
      # Do not require connection_helper to be provided, but when it's not,
      # cause failures when accessing _connection_helper property.
      self._connection_helper = connection_helper
      self._hateoas_navigator = hateoas_navigator


class SlapRequester(SlapDocument):
  """
  Abstract class that allow to factor method for subclasses that use "request()"
  """

  def _requestComputerPartition(self, request_dict):
    try:
      xml = self._connection_helper.POST('requestComputerPartition', data=request_dict)
    except ResourceNotReady:
      return ComputerPartition(
        request_dict=request_dict,
        connection_helper=self._connection_helper,
      )
    software_instance = loads(xml)
    computer_partition = ComputerPartition(
      software_instance.slap_computer_id,
      software_instance.slap_computer_partition_id,
      connection_helper=self._connection_helper,
    )
    # Hack to give all object attributes to the ComputerPartition instance
    # XXX Should be removed by correctly specifying difference between
    # ComputerPartition and SoftwareInstance
    computer_partition.__dict__.update(software_instance.__dict__)
    # XXX not generic enough.
    if loads(request_dict['shared_xml']):
      computer_partition._synced = True
      computer_partition._connection_dict = software_instance._connection_dict
      computer_partition._parameter_dict = software_instance._parameter_dict
    return computer_partition


@implementer(interface.ISoftwareRelease)
class SoftwareRelease(SlapDocument):
  """
  Contains Software Release information
  """

  def __init__(self, software_release=None, computer_guid=None, requested_state='available', **kw):
    """
    Makes easy initialisation of class parameters

    XXX **kw args only kept for compatibility
    """
    SlapDocument.__init__(self, kw.pop('connection_helper', None),
                                kw.pop('hateoas_navigator', None))
    self._software_instance_list = []
    self._software_release = software_release
    self._computer_guid = computer_guid

  def __getinitargs__(self):
    return (self._software_release, self._computer_guid, )

  def getComputerId(self):
    if not self._computer_guid:
      raise NameError('computer_guid has not been defined.')
    else:
      return self._computer_guid

  def getURI(self):
    if not self._software_release:
      raise NameError('software_release has not been defined.')
    else:
      return self._software_release

  def error(self, error_log, logger=None):
    try:
      # Does not follow interface
      self._connection_helper.POST('softwareReleaseError', data={
        'url': self.getURI(),
        'computer_id': self.getComputerId(),
        'error_log': error_log})
    except Exception:
      (logger or fallback_logger).exception('')

  def available(self):
    self._connection_helper.POST('availableSoftwareRelease', data={
      'url': self.getURI(),
      'computer_id': self.getComputerId()})

  def building(self):
    self._connection_helper.POST('buildingSoftwareRelease', data={
      'url': self.getURI(),
      'computer_id': self.getComputerId()})

  def destroyed(self):
    self._connection_helper.POST('destroyedSoftwareRelease', data={
      'url': self.getURI(),
      'computer_id': self.getComputerId()})

  def getState(self):
    return getattr(self, '_requested_state', 'available')


@implementer(interface.ISoftwareProductCollection)
class SoftwareProductCollection(object):

  def __init__(self, logger, slap):
    self.logger = logger
    self.slap = slap
    self.get = self.__getattr__

  def __getattr__(self, software_product):
      self.logger.info('Getting best Software Release corresponding to '
                       'this Software Product...')
      software_release_list = \
          self.slap.getSoftwareReleaseListFromSoftwareProduct(software_product)
      try:
          software_release_url = software_release_list[0] # First is best one.
          self.logger.info('Found as %s.' % software_release_url)
          return software_release_url
      except IndexError:
          raise AttributeError('No Software Release corresponding to this '
                           'Software Product has been found.')


# XXX What is this SoftwareInstance class?
@implementer(interface.ISoftwareInstance)
class SoftwareInstance(SlapDocument):
  """
  Contains Software Instance information
  """

  def __init__(self, **kw):
    """
    Makes easy initialisation of class parameters
    """
    self.__dict__.update(kw)


"""Exposed exceptions"""
@implementer(interface.IResourceNotReady)
class ResourceNotReady(Exception):
  pass

@implementer(interface.IServerError)
class ServerError(Exception):
  pass

@implementer(interface.INotFoundError)
class NotFoundError(Exception):
  pass

class AuthenticationError(Exception):
  pass

@implementer(interface.IConnectionError)
class ConnectionError(Exception):
  pass

@implementer(interface.ISupply)
class Supply(SlapDocument):

  def supply(self, software_release, computer_guid=None, state='available'):
    try:
      self._connection_helper.POST('supplySupply', data={
        'url': software_release,
        'computer_id': computer_guid,
        'state': state})
    except NotFoundError:
      raise NotFoundError("Computer %s has not been found by SlapOS Master."
          % computer_guid)


@implementer(interface.IOpenOrder)
class OpenOrder(SlapRequester):

  def request(self, software_release, partition_reference,
              partition_parameter_kw=None, software_type=None,
              filter_kw=None, state=None, shared=False):
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    if filter_kw is None:
      filter_kw = {}
    request_dict = {
        'software_release': software_release,
        'partition_reference': partition_reference,
        'partition_parameter_xml': dumps(partition_parameter_kw),
        'filter_xml': dumps(filter_kw),
        # XXX Cedric: Why state and shared are marshalled? First is a string
        #             And second is a boolean.
        'state': dumps(state),
        'shared_xml': dumps(shared),
    }
    if software_type is not None:
      request_dict['software_type'] = software_type
    else:
      # Let's enforce a default software type
      request_dict['software_type'] = DEFAULT_SOFTWARE_TYPE
    return self._requestComputerPartition(request_dict)

  def getInformation(self, partition_reference):
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
    raw_information = self._hateoas_navigator.getHostingSubscriptionRootSoftwareInstanceInformation(partition_reference)
    software_instance = SoftwareInstance()
    # XXX redefine SoftwareInstance to be more consistent
    for key, value in six.iteritems(raw_information):
      if key in ['_links']:
        continue
      setattr(software_instance, '_%s' % key, value)
    setattr(software_instance, '_software_release_url', raw_information['_links']['software_release'])
    return software_instance

  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
    xml = self._connection_helper.POST('requestComputer', data={'computer_title': computer_reference})
    computer = loads(xml)
    computer._connection_helper = self._connection_helper
    computer._hateoas_navigator = self._hateoas_navigator
    return computer


def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
    if not getattr(self, '_synced', 0):
      computer = self._connection_helper.getFullComputerInformation(
        self._computer_id)
      self.__dict__.update(computer.__dict__)
      self._synced = True
      for computer_partition in self.getComputerPartitionList():
        computer_partition._synced = True
    return func(self, *args, **kw)
  return wraps(func)(decorated)


@implementer(interface.IComputer)
class Computer(SlapDocument):

  def __init__(self, computer_id, connection_helper=None, hateoas_navigator=None):
    SlapDocument.__init__(self, connection_helper, hateoas_navigator)
    self._computer_id = computer_id

  def __getinitargs__(self):
    return (self._computer_id, )

  @_syncComputerInformation
  def getSoftwareReleaseList(self):
    """
    Returns the list of software release which has to be supplied by the
    computer.

    Raise an INotFoundError if computer_guid doesn't exist.
    """
    for software_relase in self._software_release_list:
      software_relase._connection_helper = self._connection_helper
      software_relase._hateoas_navigator = self._hateoas_navigator
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
      computer_partition._hateoas_navigator = self._hateoas_navigator
    return [x for x in self._computer_partition_list]

  def reportUsage(self, computer_usage):
    if computer_usage == "":
      return
    self._connection_helper.POST('useComputer', data={
      'computer_id': self._computer_id,
      'use_string': computer_usage})

  def updateConfiguration(self, xml):
    return self._connection_helper.POST('loadComputerConfigurationFromXML', data={'xml': xml})

  def bang(self, message):
    self._connection_helper.POST('computerBang', data={
      'computer_id': self._computer_id,
      'message': message})

  def getStatus(self):
    xml = self._connection_helper.GET('getComputerStatus', params={'computer_id': self._computer_id})
    return loads(xml)

  def revokeCertificate(self):
    self._connection_helper.POST('revokeComputerCertificate', data={
      'computer_id': self._computer_id})

  def generateCertificate(self):
    xml = self._connection_helper.POST('generateComputerCertificate', data={
      'computer_id': self._computer_id})
    return loads(xml)


def parsed_error_message(status, body, path):
  m = re.search('(Error Value:\n.*)', body, re.MULTILINE)
  if m:
    match = ' '.join(line.strip() for line in m.group(0).split('\n'))
    return '%s (status %s while calling %s)' % (
                saxutils.unescape(match),
                status,
                path
            )
  else:
    return 'Server responded with wrong code %s with %s' % (status, path)


@implementer(interface.IComputerPartition)
class ComputerPartition(SlapRequester):

  def __init__(self, computer_id=None, partition_id=None,
               request_dict=None, connection_helper=None,
               hateoas_navigator=None):
    SlapDocument.__init__(self, connection_helper, hateoas_navigator)
    if request_dict is not None and (computer_id is not None or
        partition_id is not None):
      raise TypeError('request_dict conflicts with computer_id and '
        'partition_id')
    if request_dict is None and (computer_id is None or partition_id is None):
      raise TypeError('computer_id and partition_id or request_dict are '
        'required')
    self._computer_id = computer_id
    self._partition_id = partition_id
    self._request_dict = request_dict

    # Just create an empty file (for nothing requested yet)
    self._updateTransactionFile(partition_reference=None)

  def __getinitargs__(self):
    return (self._computer_id, self._partition_id, )

  def _updateTransactionFile(self, partition_reference=None):
    """
    Store reference to all Instances requested by this Computer Parition
    """
    # Environ variable set by Slapgrid while processing this partition
    instance_root = os.environ.get('SLAPGRID_INSTANCE_ROOT', '')
    if not instance_root or not self._partition_id:
      return

    transaction_file_name = COMPUTER_PARTITION_REQUEST_LIST_TEMPLATE_FILENAME % self._partition_id
    transaction_file_path = os.path.join(instance_root, self._partition_id,
                                        transaction_file_name)

    try:
      if partition_reference is None:
        if os.access(os.path.join(instance_root, self._partition_id), os.W_OK):
          if os.path.exists(transaction_file_path):
            return
          transac_file = open(transaction_file_path, 'w')
          transac_file.close()
      else:
        with open(transaction_file_path, 'a') as transac_file:
          transac_file.write('%s\n' % partition_reference)
    except OSError:
      return

  def request(self, software_release, software_type, partition_reference,
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" %
                       partition_parameter_kw)

    if filter_kw is None:
      filter_kw = {}
    elif not isinstance(filter_kw, dict):
      raise ValueError("Unexpected type of filter_kw '%s'" %
                       filter_kw)

    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

    request_dict = {
        'computer_id': self._computer_id,
        'computer_partition_id': self._partition_id,
        'software_release': software_release,
        'software_type': software_type,
        'partition_reference': partition_reference,
        'shared_xml': dumps(shared),
        'partition_parameter_xml': dumps(partition_parameter_kw),
        'filter_xml': dumps(filter_kw),
        'state': dumps(state),
    }
    self._updateTransactionFile(partition_reference)
    return self._requestComputerPartition(request_dict)

  def destroyed(self):
    self._connection_helper.POST('destroyedComputerPartition', data={
      'computer_id': self._computer_id,
      'computer_partition_id': self.getId(),
      })

  def started(self):
    self._connection_helper.POST('startedComputerPartition', data={
      'computer_id': self._computer_id,
      'computer_partition_id': self.getId(),
      })

  def stopped(self):
    self._connection_helper.POST('stoppedComputerPartition', data={
      'computer_id': self._computer_id,
      'computer_partition_id': self.getId(),
      })

  def error(self, error_log, logger=None):
    try:
      self._connection_helper.POST('softwareInstanceError', data={
        'computer_id': self._computer_id,
        'computer_partition_id': self.getId(),
        'error_log': error_log})
    except Exception:
      (logger or fallback_logger).exception('')

  def bang(self, message):
    self._connection_helper.POST('softwareInstanceBang', data={
      'computer_id': self._computer_id,
      'computer_partition_id': self.getId(),
      'message': message})

  def rename(self, new_name, slave_reference=None):
    post_dict = {
            'computer_id': self._computer_id,
            'computer_partition_id': self.getId(),
            'new_name': new_name,
            }
    if slave_reference:
      post_dict['slave_reference'] = slave_reference
    self._connection_helper.POST('softwareInstanceRename', data=post_dict)

  def getInformation(self, partition_reference):
    """
    Return all needed informations about an existing Computer Partition
    in the Instance tree of the current Computer Partition.
    """
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')

    raw_information = self._hateoas_navigator.getRelatedInstanceInformation(partition_reference)
    software_instance = SoftwareInstance()
    # XXX redefine SoftwareInstance to be more consistent
    for key, value in six.iteritems(raw_information):
      if key in ['_links']:
        continue
      setattr(software_instance, '_%s' % key, value)
    setattr(software_instance, '_software_release_url', raw_information['_links']['software_release'])
    return software_instance


  def getId(self):
    if not getattr(self, '_partition_id', None):
      raise ResourceNotReady()
    return self._partition_id

  def getInstanceGuid(self):
    """Return instance_guid. Raise ResourceNotReady if it doesn't exist."""
    if not getattr(self, '_instance_guid', None):
      raise ResourceNotReady()
    return self._instance_guid

  def getState(self):
    """return _requested_state. Raise ResourceNotReady if it doesn't exist."""
    if not getattr(self, '_requested_state', None):
      raise ResourceNotReady()
    return self._requested_state

  def getAccessStatus(self):
    """Get latest computer partition Access message state"""
    return getattr(self, '_access_status', None)

  def getType(self):
    """
    return the Software Type of the instance.
    Raise RessourceNotReady if not present.
    """
    # XXX: software type should not belong to the parameter dict.
    software_type = self.getInstanceParameterDict().get(
        'slap_software_type', None)
    if not software_type:
      raise ResourceNotReady()
    return software_type

  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

  def getConnectionParameterDict(self):
    connection_dict = getattr(self, '_connection_dict', None)
    if connection_dict is None:
      # XXX Backward compatibility for older slapproxy (<= 1.0.0)
      connection_dict = xml2dict(getattr(self, 'connection_xml', ''))

    return connection_dict or {}

  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
    if not getattr(self, '_software_release_document', None):
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

  def setConnectionDict(self, connection_dict, slave_reference=None):
    if self.getConnectionParameterDict() == connection_dict:
      return

    if slave_reference is not None:
      # check the connection parameters from the slave

      # Should we check existence?
      slave_parameter_list = self.getInstanceParameter("slave_instance_list")
      slave_connection_dict = {}
      connection_parameter_hash = None
      for slave_parameter_dict in slave_parameter_list:
        if slave_parameter_dict.get("slave_reference") == slave_reference:
          connection_parameter_hash = slave_parameter_dict.get("connection-parameter-hash", None)
          break

      # Skip as nothing changed for the slave
      if connection_parameter_hash is not None and \
        connection_parameter_hash == hashlib.sha256(str(connection_dict)).hexdigest():
        return

    self._connection_helper.POST('setComputerPartitionConnectionXml', data={
          'computer_id': self._computer_id,
          'computer_partition_id': self._partition_id,
          'connection_xml': dumps(connection_dict),
          'slave_reference': slave_reference})

  def getInstanceParameter(self, key):
    parameter_dict = getattr(self, '_parameter_dict', None) or {}
    if key in parameter_dict:
      return parameter_dict[key]
    else:
      raise NotFoundError("%s not found" % key)

  def getConnectionParameter(self, key):
    connection_dict = self.getConnectionParameterDict()
    if key in connection_dict:
      return connection_dict[key]
    else:
      raise NotFoundError("%s not found" % key)

  def setUsage(self, usage_log):
    # XXX: this implementation has not been reviewed
    self.usage = usage_log

  def getCertificate(self):
    xml = self._connection_helper.GET('getComputerPartitionCertificate',
            params={
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
    return loads(xml)

  def getStatus(self):
    xml = self._connection_helper.GET('getComputerPartitionStatus',
            params={
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
    return loads(xml)
  
  def getFullHostingIpAddressList(self):
    xml = self._connection_helper.GET('getHostingSubscriptionIpList',
            params={
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
    return loads(xml)

  def setComputerPartitionRelatedInstanceList(self, instance_reference_list):
    self._connection_helper.POST('updateComputerPartitionRelatedInstanceList',
        data={
          'computer_id': self._computer_id,
          'computer_partition_id': self._partition_id,
          'instance_reference_xml': dumps(instance_reference_list)
          }
        )

def _addIpv6Brackets(url):
  # if master_url contains an ipv6 without bracket, add it
  # Note that this is mostly to limit specific issues with
  # backward compatiblity, not to ensure generic detection.
  api_scheme, api_netloc, api_path, api_query, api_fragment = parse.urlsplit(url)
  try:
    ip = netaddr.IPAddress(api_netloc)
    port = None
  except netaddr.AddrFormatError:
    try:
      ip = netaddr.IPAddress(':'.join(api_netloc.split(':')[:-1]))
      port = api_netloc.split(':')[-1]
    except netaddr.AddrFormatError:
      ip = port = None
  if ip and ip.version == 6:
    api_netloc = '[%s]' % ip
    if port:
      api_netloc = '%s:%s' % (api_netloc, port)
    url = parse.urlunsplit((api_scheme, api_netloc, api_path, api_query, api_fragment))
  return url

class ConnectionHelper:
  def __init__(self, master_url, key_file=None,
               cert_file=None, master_ca_file=None, timeout=None):
    master_url = _addIpv6Brackets(master_url)
    if master_url.endswith('/'):
        self.slapgrid_uri = master_url
    else:
        # add a slash or the last path segment will be ignored by urljoin
        self.slapgrid_uri = master_url + '/'
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
    self.timeout = timeout

  def getComputerInformation(self, computer_id):
    xml = self.GET('getComputerInformation', params={'computer_id': computer_id})
    return loads(xml)

  def getFullComputerInformation(self, computer_id):
    """
    Retrieve from SlapOS Master Computer instance containing all needed
    informations (Software Releases, Computer Partitions, ...).
    """
    path = 'getFullComputerInformation'
    params = {'computer_id': computer_id}
    if not computer_id:
      # XXX-Cedric: should raise something smarter than "NotFound".
      raise NotFoundError('%r %r' % (path, params))
    try:
      xml = self.GET(path, params=params)
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
      xml = self.GET('getComputerInformation', params=params)

    return loads(xml)

  def do_request(self, method, path, params=None, data=None, headers=None):
    url = parse.urljoin(self.slapgrid_uri, path)
    if headers is None:
      headers = {}
    headers.setdefault('Accept', '*/*')
    if path.startswith('/'):
      path = path[1:]
#      raise ValueError('method path should be relative: %s' % path)

    try:
      if url.startswith('https'):
        cert = (self.cert_file, self.key_file)
      else:
        cert = None

      # XXX TODO: handle host cert verify

      # Old behavior was to pass empty parameters as "None" value.
      # Behavior kept for compatibility with old slapproxies (< v1.3.3).
      # Can be removed when old slapproxies are no longer in use.
      if data:
        for k, v in six.iteritems(data):
          if v is None:
            data[k] = 'None'

      req = method(url=url,
                   params=params,
                   cert=cert,
                   verify=False,
                   data=data,
                   headers=headers,
                   timeout=self.timeout)
      try:
        req.raise_for_status()
      except TypeError:
        # In Py3, a comparison between NoneType and int can occur if req has no
        # status_code (= None).
        pass

    except (requests.Timeout, requests.ConnectionError) as exc:
      raise ConnectionError("Couldn't connect to the server. Please "
                            "double check given master-url argument, and make sure that IPv6 is "
                            "enabled on your machine and that the server is available. The "
                            "original error was:\n%s" % exc)
    except requests.HTTPError as exc:
      if exc.response.status_code == requests.status_codes.codes.not_found:
        msg = url
        if params:
            msg += ' - %s' % params
        raise NotFoundError(msg)
      elif exc.response.status_code == requests.status_codes.codes.request_timeout:
        # this is explicitly returned by SlapOS master, and does not really mean timeout
        raise ResourceNotReady(path)
        # XXX TODO test request timeout and resource not found
      else:
        # we don't know how or don't want to handle these (including Unauthorized)
        req.raise_for_status()
    except requests.exceptions.SSLError as exc:
      raise AuthenticationError("%s\nCouldn't authenticate computer. Please "
                                "check that certificate and key exist and are valid." % exc)

#    XXX TODO parse server messages for client configure and node register
#    elif response.status != httplib.OK:
#      message = parsed_error_message(response.status,
#                                     response.read(),
#                                     path)
#      raise ServerError(message)

    return req

  def GET(self, path, params=None, headers=None):
    req = self.do_request(requests.get,
                          path=path,
                          params=params,
                          headers=headers)
    return req.text.encode('utf-8')

  def POST(self, path, params=None, data=None,
           content_type='application/x-www-form-urlencoded'):
    req = self.do_request(requests.post,
                          path=path,
                          params=params,
                          data=data,
                          headers={'Content-type': content_type})
    return req.text.encode('utf-8')


getHateoasUrl_cache = {}
@implementer(interface.slap)
class slap:

  def initializeConnection(self, slapgrid_uri,
                           key_file=None, cert_file=None,
                           master_ca_file=None,
                           timeout=60,
                           slapgrid_rest_uri=None):
    if master_ca_file:
      raise NotImplementedError('Master certificate not verified in this version: %s' % master_ca_file)

    self._connection_helper = ConnectionHelper(slapgrid_uri, key_file, cert_file, master_ca_file, timeout)

    if not slapgrid_rest_uri:
      getHateoasUrl_cache_key = (slapgrid_uri, key_file, cert_file, master_ca_file, timeout)
      try:
        slapgrid_rest_uri = getHateoasUrl_cache[getHateoasUrl_cache_key]
      except KeyError:
        pass
    if not slapgrid_rest_uri:
      try:
        slapgrid_rest_uri = getHateoasUrl_cache[getHateoasUrl_cache_key] = \
          bytes2str(self._connection_helper.GET('getHateoasUrl'))
      except:
        pass
    if slapgrid_rest_uri:
      self._hateoas_navigator = SlapHateoasNavigator(
          slapgrid_rest_uri,
          key_file, cert_file,
          master_ca_file, timeout
      )
    else:
      self._hateoas_navigator = None

  # XXX-Cedric: this method is never used and thus should be removed.
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
    return SoftwareRelease(software_release=software_release,
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
    )

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
    return Computer(computer_guid,
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
    )

  def registerComputerPartition(self, computer_guid, partition_id):
    """
    Registers connected representation of computer partition and
    returns Computer Partition class object
    """
    if not computer_guid or not partition_id:
      # XXX-Cedric: should raise something smarter than NotFound
      raise NotFoundError

    xml = self._connection_helper.GET('registerComputerPartition',
            params = {
                'computer_reference': computer_guid,
                'computer_partition_reference': partition_id,
                }
            )
    result = loads(xml)
    # XXX: dirty hack to make computer partition usable. xml_marshaller is too
    # low-level for our needs here.
    result._connection_helper = self._connection_helper
    result._hateoas_navigator = self._hateoas_navigator
    return result

  def registerOpenOrder(self):
    return OpenOrder(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )

  def registerSupply(self):
    return Supply(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )

  def getSoftwareReleaseListFromSoftwareProduct(self,
      software_product_reference=None, software_release_url=None):
    url = 'getSoftwareReleaseListFromSoftwareProduct'
    params = {}
    if software_product_reference:
      if software_release_url is not None:
        raise AttributeError('Both software_product_reference and '
                             'software_release_url parameters are specified.')
      params['software_product_reference'] = software_product_reference
    else:
      if software_release_url is None:
        raise AttributeError('None of software_product_reference and '
                             'software_release_url parameters are specified.')
      params['software_release_url'] = software_release_url

    xml = self._connection_helper.GET(url, params=params)
    result = loads(xml)
    assert(type(result) == list)
    return result

  def getOpenOrderDict(self):
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
    return self._hateoas_navigator.getHostingSubscriptionDict()

class HateoasNavigator(object):
  """
  Navigator for HATEOAS-style APIs.
  Inspired by
  https://git.erp5.org/gitweb/jio.git/blob/HEAD:/src/jio.storage/erp5storage.js
  """
  # XXX: needs to be designed for real. For now, just a non-maintainable prototype.
  # XXX: export to a standalone library, independant from slap.
  def __init__(self, slapgrid_uri,
               key_file=None, cert_file=None,
               master_ca_file=None, timeout=60):
    self.slapos_master_hateoas_uri = slapgrid_uri
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
    self.timeout = timeout

  def GET(self, uri, headers=None):
    connection_helper = ConnectionHelper(
        uri, self.key_file, self.cert_file, self.master_ca_file, self.timeout)
    return connection_helper.GET(uri, headers=headers)

  def hateoasGetLinkFromLinks(self, links, title):
    if type(links) == dict:
      if links.get('title') == title:
        return links['href']
      raise NotFoundError('Action %s not found.' % title)
    for action in links:
      if action.get('title') == title:
        return action['href']
    else:
      raise NotFoundError('Action %s not found.' % title)

  def getRelativeUrlFromUrn(self, urn):
    urn_schema = 'urn:jio:get:'
    try:
      _, url = urn.split(urn_schema)
    except ValueError:
      return
    return str(url)

  def getSiteDocument(self, url, headers=None):
    result = self.GET(url, headers)
    return json.loads(result)

  def getRootDocument(self):
    # XXX what about cache?
    cached_root_document = getattr(self, 'root_document', None)
    if cached_root_document:
      return cached_root_document
    self.root_document = self.getSiteDocument(
        self.slapos_master_hateoas_uri,
        headers={'Cache-Control': 'no-cache'}
    )
    return self.root_document

  def getDocumentAndHateoas(self, relative_url, view='view'):
    site_document = self.getRootDocument()
    return expand(
        site_document['_links']['traverse']['href'],
        dict(relative_url=relative_url, view=view)
    )

  def getMeDocument(self):
    person_relative_url = self.getRelativeUrlFromUrn(
        self.getRootDocument()['_links']['me']['href'])
    person_url = self.getDocumentAndHateoas(person_relative_url)
    return json.loads(self.GET(person_url))

class SlapHateoasNavigator(HateoasNavigator):
  def _hateoas_getHostingSubscriptionDict(self):
    action_object_slap_list = self.getMeDocument()['_links']['action_object_slap']
    for action in action_object_slap_list:
      if action.get('title') == 'getHateoasHostingSubscriptionList':
        getter_link = action['href']
        break
    else:
      raise Exception('Hosting subscription not found.')
    result = self.GET(getter_link)
    return json.loads(result)['_links']['content']

  # XXX rename me to blablaUrl(self)
  def _hateoas_getRelatedHostingSubscription(self):
    action_object_slap_list = self.getMeDocument()['_links']['action_object_slap']
    getter_link = self.hateoasGetLinkFromLinks(action_object_slap_list, 'getHateoasRelatedHostingSubscription')
    result = self.GET(getter_link)
    return json.loads(result)['_links']['action_object_jump']['href']

  def _hateoasGetInformation(self, url):
    result = self.GET(url)
    result = json.loads(result)
    object_link = self.hateoasGetLinkFromLinks(
      result['_links']['action_object_slap'],
      'getHateoasInformation'
    )
    result = self.GET(object_link)
    return json.loads(result)

  def getHateoasInstanceList(self, hosting_subscription_url):
    hosting_subscription = json.loads(self.GET(hosting_subscription_url))
    instance_list_url = self.hateoasGetLinkFromLinks(hosting_subscription['_links']['action_object_slap'], 'getHateoasInstanceList')
    instance_list = json.loads(self.GET(instance_list_url))
    return instance_list['_links']['content']

  def getHostingSubscriptionDict(self):
    hosting_subscription_link_list = self._hateoas_getHostingSubscriptionDict()
    hosting_subscription_dict = {}
    for hosting_subscription_link in hosting_subscription_link_list:
      raw_information = self.getHostingSubscriptionRootSoftwareInstanceInformation(hosting_subscription_link['title'])
      software_instance = SoftwareInstance()
      # XXX redefine SoftwareInstance to be more consistent
      for key, value in six.iteritems(raw_information):
        if key in ['_links']:
          continue
        setattr(software_instance, '_%s' % key, value)
      setattr(software_instance, '_software_release_url', raw_information['_links']['software_release'])
      hosting_subscription_dict[software_instance._title] = software_instance

    return hosting_subscription_dict

  def getHostingSubscriptionRootSoftwareInstanceInformation(self, reference):
    hosting_subscription_list = self._hateoas_getHostingSubscriptionDict()
    for hosting_subscription in hosting_subscription_list:
      if hosting_subscription.get('title') == reference:
        hosting_subscription_url = hosting_subscription['href']
        break
    else:
      raise NotFoundError('This document does not exist.')

    hosting_subscription = json.loads(self.GET(hosting_subscription_url))

    software_instance_url = self.hateoasGetLinkFromLinks(
        hosting_subscription['_links']['action_object_slap'],
        'getHateoasRootInstance'
    )
    response = self.GET(software_instance_url)
    response = json.loads(response)
    software_instance_url = response['_links']['content'][0]['href']
    return self._hateoasGetInformation(software_instance_url)

  def getRelatedInstanceInformation(self, reference):
    related_hosting_subscription_url = self._hateoas_getRelatedHostingSubscription()
    instance_list = self.getHateoasInstanceList(related_hosting_subscription_url)
    instance_url = self.hateoasGetLinkFromLinks(instance_list, reference)
    instance = self._hateoasGetInformation(instance_url)
    return instance
