# -*- coding: utf-8 -*-
##############################################################################
#
# 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 advised 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 General Public License
# as published by the Free Software Foundation; either version 3
# 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 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.
#
##############################################################################

from flask import g, Flask, request, abort
import xml_marshaller
from lxml import etree
from slapos.slap.slap import Computer, ComputerPartition, \
    SoftwareRelease, SoftwareInstance, NotFoundError
import sqlite3

app = Flask(__name__)
DB_VERSION = app.open_resource('schema.sql').readline().strip().split(':')[1]


class UnauthorizedError(Exception):
  pass


def xml2dict(xml):
  result_dict = {}
  if xml is not None and xml != '':
    tree = etree.fromstring(xml.encode('utf-8'))
    for element in tree.iter(tag=etree.Element):
      if element.tag == 'parameter':
        key = element.get('id')
        value = result_dict.get(key, None)
        if value is not None:
          value = value + ' ' + element.text
        else:
          value = element.text
        result_dict[key] = value
  return result_dict


def dict2xml(dictionnary):
  instance = etree.Element('instance')
  for parameter_id, parameter_value in dictionnary.iteritems():
    # cast everything to string
    parameter_value = str(parameter_value)
    etree.SubElement(instance, "parameter",
                     attrib={'id': parameter_id}).text = parameter_value
  return etree.tostring(instance, pretty_print=True,
                                xml_declaration=True, encoding='utf-8')


def partitiondict2partition(partition):
  slap_partition = ComputerPartition(app.config['computer_id'],
      partition['reference'])
  slap_partition._software_release_document = None
  slap_partition._requested_state = 'destroyed'
  slap_partition._need_modification = 0

  if partition['software_release']:
    slap_partition._need_modification = 1
    slap_partition._requested_state = 'started'
    slap_partition._parameter_dict = xml2dict(partition['xml'])
    address_list = []
    for address in execute_db('partition_network',
                              'SELECT * FROM %s WHERE partition_reference=?',
                              [partition['reference']]):
      address_list.append((address['reference'], address['address']))
    slap_partition._parameter_dict['ip_list'] = address_list
    slap_partition._parameter_dict['slap_software_type'] = \
        partition['software_type']
    if not partition['slave_instance_list'] == None:
      slap_partition._parameter_dict['slave_instance_list'] = \
          xml_marshaller.xml_marshaller.loads(partition['slave_instance_list'])
    slap_partition._connection_dict = xml2dict(partition['connection_xml'])
    slap_partition._software_release_document = SoftwareRelease(
      software_release=partition['software_release'],
      computer_guid=app.config['computer_id'])

  return slap_partition


def execute_db(table, query, args=(), one=False):
  try:
    cur = g.db.execute(query % (table + DB_VERSION,), args)
  except:
    app.logger.error('There was some issue during processing query %r on table %r with args %r' % (query, table, args))
    raise
  rv = [dict((cur.description[idx][0], value)
    for idx, value in enumerate(row)) for row in cur.fetchall()]
  return (rv[0] if rv else None) if one else rv


def connect_db():
  return sqlite3.connect(app.config['DATABASE_URI'])


@app.before_request
def before_request():
  g.db = connect_db()
  schema = app.open_resource('schema.sql')
  schema = schema.read() % dict(version=DB_VERSION)
  g.db.cursor().executescript(schema)
  g.db.commit()

@app.after_request
def after_request(response):
  g.db.commit()
  g.db.close()
  return response

@app.route('/getComputerInformation', methods=['GET'])
def getComputerInformation():
  # Kept only for backward compatiblity
  return getFullComputerInformation()

@app.route('/getFullComputerInformation', methods=['GET'])
def getFullComputerInformation():
  computer_id = request.args['computer_id']
  if app.config['computer_id'] == computer_id:
    slap_computer = Computer(computer_id)
    slap_computer._software_release_list = []
    for sr in execute_db('software', 'select * from %s'):
      slap_computer._software_release_list.append(SoftwareRelease(
        software_release=sr['url'], computer_guid=computer_id))
    slap_computer._computer_partition_list = []
    for partition in execute_db('partition', 'SELECT * FROM %s'):
      slap_computer._computer_partition_list.append(partitiondict2partition(
        partition))
    return xml_marshaller.xml_marshaller.dumps(slap_computer)
  else:
    raise NotFoundError, "Only accept request for: %s" % \
                             app.config['computer_id']

@app.route('/setComputerPartitionConnectionXml', methods=['POST'])
def setComputerPartitionConnectionXml():
  slave_reference = request.form['slave_reference'].encode()
  computer_partition_id = request.form['computer_partition_id']
  connection_xml = request.form['connection_xml']
  connection_dict = xml_marshaller.xml_marshaller.loads(
                                            connection_xml.encode())
  connection_xml = dict2xml(connection_dict)
  if slave_reference == 'None':
    query = 'UPDATE %s SET connection_xml=? WHERE reference=?'
    argument_list = [connection_xml, computer_partition_id.encode()]
    execute_db('partition', query, argument_list)
    return 'done'
  else:
    query = 'UPDATE %s SET connection_xml=? , hosted_by=? WHERE reference=?'
    argument_list = [connection_xml, computer_partition_id.encode(),
                     slave_reference]
    execute_db('slave', query, argument_list)
    return 'done'

@app.route('/buildingSoftwareRelease', methods=['POST'])
def buildingSoftwareRelease():
  return 'Ignored'

@app.route('/availableSoftwareRelease', methods=['POST'])
def availableSoftwareRelease():
  return 'Ignored'

@app.route('/softwareReleaseError', methods=['POST'])
def softwareReleaseError():
  return 'Ignored'

@app.route('/buildingComputerPartition', methods=['POST'])
def buildingComputerPartition():
  return 'Ignored'

@app.route('/availableComputerPartition', methods=['POST'])
def availableComputerPartition():
  return 'Ignored'

@app.route('/softwareInstanceError', methods=['POST'])
def softwareInstanceError():
  return 'Ignored'

@app.route('/softwareInstanceBang', methods=['POST'])
def softwareInstanceBang():
  return 'Ignored'

@app.route('/startedComputerPartition', methods=['POST'])
def startedComputerPartition():
  return 'Ignored'

@app.route('/stoppedComputerPartition', methods=['POST'])
def stoppedComputerPartition():
  return 'Ignored'

@app.route('/destroyedComputerPartition', methods=['POST'])
def destroyedComputerPartition():
  return 'Ignored'

@app.route('/useComputer', methods=['POST'])
def useComputer():
  return 'Ignored'

@app.route('/loadComputerConfigurationFromXML', methods=['POST'])
def loadComputerConfigurationFromXML():
  xml = request.form['xml']
  computer_dict = xml_marshaller.xml_marshaller.loads(str(xml))
  if app.config['computer_id'] == computer_dict['reference']:
    execute_db('computer', 'INSERT OR REPLACE INTO %s values(:address, :netmask)',
        computer_dict)
    for partition in computer_dict['partition_list']:

      execute_db('partition', 'INSERT OR IGNORE INTO %s (reference) values(:reference)', partition)
      execute_db('partition_network', 'DELETE FROM %s WHERE partition_reference = ?', [partition['reference']])
      for address in partition['address_list']:
        address['reference'] = partition['tap']['name']
        address['partition_reference'] = partition['reference']
        execute_db('partition_network', 'INSERT OR REPLACE INTO %s (reference, partition_reference, address, netmask) values(:reference, :partition_reference, :addr, :netmask)', address)

    return 'done'
  else:
    raise UnauthorizedError, "Only accept request for: %s" % \
                             app.config['computer_id']

@app.route('/registerComputerPartition', methods=['GET'])
def registerComputerPartition():
  computer_reference = request.args['computer_reference']
  computer_partition_reference = request.args['computer_partition_reference']
  if app.config['computer_id'] == computer_reference:
    partition = execute_db('partition', 'SELECT * FROM %s WHERE reference=?',
      [computer_partition_reference.encode()], one=True)
    if partition is None:
      raise UnauthorizedError
    return xml_marshaller.xml_marshaller.dumps(
        partitiondict2partition(partition))
  else:
    raise UnauthorizedError, "Only accept request for: %s" % \
                             app.config['computer_id']

@app.route('/supplySupply', methods=['POST'])
def supplySupply():
  url = request.form['url']
  computer_id = request.form['computer_id']
  if app.config['computer_id'] == computer_id:
    execute_db('software', 'INSERT OR REPLACE INTO %s VALUES(?)', [url])
  else:
    raise UnauthorizedError, "Only accept request for: %s" % \
                             app.config['computer_id']
  return '%r added' % url


@app.route('/requestComputerPartition', methods=['POST'])
def requestComputerPartition():
  shared_xml = request.form.get('shared_xml')
  share = xml_marshaller.xml_marshaller.loads(shared_xml)
  if not share:
    return request_not_shared()
  else:
    return request_slave()


@app.route('/softwareInstanceRename', methods=['POST'])
def softwareInstanceRename():
  new_name = request.form['new_name'].encode()
  computer_partition_id = request.form['computer_partition_id'].encode()

  q = 'UPDATE %s SET partition_reference = ? WHERE reference = ?'
  execute_db('partition', q, [new_name, computer_partition_id])
  return 'done'


def request_not_shared():
  software_release = request.form['software_release'].encode()
  # some supported parameters
  software_type = request.form.get('software_type').encode()
  partition_reference = request.form.get('partition_reference', '').encode()
  partition_id = request.form.get('computer_partition_id', '').encode()
  partition_parameter_kw = request.form.get('partition_parameter_xml', None)
  if partition_parameter_kw:
    partition_parameter_kw = xml_marshaller.xml_marshaller.loads(
                                              partition_parameter_kw.encode())
  else:
    partition_parameter_kw = {}

  instance_xml = dict2xml(partition_parameter_kw)
  args = []
  a = args.append
  q = 'SELECT * FROM %s WHERE partition_reference=?'
  a(partition_reference)

#
#  XXX the following filter breaks renaming asked by the bully script
#
#  if partition_id:
#    q += ' AND requested_by=?'
#    a(partition_id)

  partition = execute_db('partition', q, args, one=True)

  args = []
  a = args.append
  q = 'UPDATE %s SET slap_state="busy"'

  # If partition doesn't exist: create it and insert parameters
  if partition is None:
    partition = execute_db('partition',
        'SELECT * FROM %s WHERE slap_state="free"', (), one=True)
    if partition is None:
      app.logger.warning('No more free computer partition')
      abort(408)
    q += ' ,software_release=?'
    a(software_release)
    if partition_reference:
      q += ' ,partition_reference=?'
      a(partition_reference)
    if partition_id:
      q += ' ,requested_by=?'
      a(partition_id)
    if not software_type:
      software_type = 'RootSoftwareInstance'

  #
  # XXX change software_type when requested
  #
  if software_type:
    q += ' ,software_type=?'
    a(software_type)

  # Else: only update partition_parameter_kw
  if instance_xml:
    q += ' ,xml=?'
    a(instance_xml)
  q += ' WHERE reference=?'
  a(partition['reference'].encode())
  execute_db('partition', q, args)
  args = []
  partition = execute_db('partition', 'SELECT * FROM %s WHERE reference=?',
      [partition['reference'].encode()], one=True)
  address_list = []
  for address in execute_db('partition_network', 'SELECT * FROM %s WHERE partition_reference=?', [partition['reference']]):
    address_list.append((address['reference'], address['address']))

  # XXX it should be ComputerPartition, not a SoftwareInstance
  return xml_marshaller.xml_marshaller.dumps(SoftwareInstance(
                            xml=partition['xml'],
                            connection_xml=partition['connection_xml'],
                            slap_computer_id=app.config['computer_id'],
                            slap_computer_partition_id=partition['reference'],
                            slap_software_release_url=partition['software_release'],
                            slap_server_url='slap_server_url',
                            slap_software_type=partition['software_type'],
                            slave_instance_list=partition['slave_instance_list'],
                            instance_guid=partition['reference'],
                            ip_list=address_list
                            ))
  abort(408)
  raise NotImplementedError


def request_slave():
  """
  Function to organise link between slave and master.
  Slave information are stored in places:
  1. slave table having information such as slave reference,
      connection information to slave (given by slave master),
      hosted_by and asked_by reference.
  2. A dictionnary in slave_instance_list of selected slave master
      in which are stored slave_reference, software_type, slave_title and
      partition_parameter_kw stored as individual keys.
  """
  software_release = request.form['software_release'].encode()
  # some supported parameters
  software_type = request.form.get('software_type').encode()
  partition_reference = request.form.get('partition_reference', '').encode()
  partition_id = request.form.get('computer_partition_id', '').encode()
  # Contain slave parameters to be given to slave master
  partition_parameter_kw = request.form.get('partition_parameter_xml', None)
  if partition_parameter_kw :
    partition_parameter_kw = xml_marshaller.xml_marshaller.loads(
                                              partition_parameter_kw.encode())
  else:
    partition_parameter_kw = {}

  filter_kw = xml_marshaller.xml_marshaller.loads(request.form.get('filter_xml').encode())

  instance_xml = dict2xml(partition_parameter_kw)
  # We will search for a master corresponding to request
  args = []
  a = args.append
  q = 'SELECT * FROM %s WHERE software_release=?'
  a(software_release)
  if software_type:
    q += ' AND software_type=?'
    a(software_type)
  if 'instance_guid' in filter_kw:
    q += ' AND reference=?'
    a(filter_kw['instance_guid'])

  partition = execute_db('partition', q, args, one=True)
  if partition is None:
    app.logger.warning('No partition corresponding to slave request: %s' % \
        args)
    abort(408)

  # We set slave dictionnary as described in docstring
  new_slave = {}
  slave_reference = partition_id + '_' + partition_reference
  new_slave['slave_title'] = slave_reference
  new_slave['slap_software_type'] = software_type
  new_slave['slave_reference'] = slave_reference

  for key in partition_parameter_kw :
    if partition_parameter_kw[key] is not None :
      new_slave[key] = partition_parameter_kw[key]

  # Add slave to partition slave_list if not present else replace information
  slave_instance_list = partition['slave_instance_list']
  if slave_instance_list == None:
    slave_instance_list = []
  else:
    slave_instance_list = xml_marshaller.xml_marshaller.loads(slave_instance_list)
    for x in slave_instance_list:
      if x['slave_reference'] == slave_reference:
        slave_instance_list.remove(x)

  slave_instance_list.append(new_slave)

  # Update slave_instance_list in database
  args = []
  a = args.append
  q = 'UPDATE %s SET slave_instance_list=?'
  a(xml_marshaller.xml_marshaller.dumps(slave_instance_list))
  q += ' WHERE reference=?'
  a(partition['reference'].encode())
  execute_db('partition', q, args)
  args = []
  partition = execute_db('partition', 'SELECT * FROM %s WHERE reference=?',
      [partition['reference'].encode()], one=True)

  # Add slave to slave table if not there
  slave = execute_db('slave', 'SELECT * FROM %s WHERE reference=?',
                     [slave_reference], one=True)
  if slave is None :
    execute_db('slave',
               'INSERT OR IGNORE INTO %s (reference,asked_by,hosted_by) values(:reference,:asked_by,:hosted_by)',
               [slave_reference,partition_id,partition['reference']])
    slave = execute_db('slave','SELECT * FROM %s WHERE reference=?',
                     [slave_reference], one = True)

  address_list = []
  for address in execute_db('partition_network',
                            'SELECT * FROM %s WHERE partition_reference=?',
                            [partition['reference']]):
    address_list.append((address['reference'], address['address']))

  # XXX it should be ComputerPartition, not a SoftwareInstance
  return xml_marshaller.xml_marshaller.dumps(SoftwareInstance(
                                _connection_dict=xml2dict(slave['connection_xml']),
                                xml = instance_xml,
                                slap_computer_id=app.config['computer_id'],
                                slap_computer_partition_id=slave['hosted_by'],
                                slap_software_release_url=partition['software_release'],
                                slap_server_url='slap_server_url',
                                slap_software_type=partition['software_type'],
                                ip_list=address_list
                                ))