# vim:ts=2:sw=2:expandtab
"""
A Python library to provide level-level Linode API interaction.

Copyright (c) 2010 Timothy J Fontaine <tjfontaine@gmail.com>
Copyright (c) 2010 Josh Wright <jshwright@gmail.com>
Copyright (c) 2010 Ryan Tucker <rtucker@gmail.com>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""

import logging

from os import environ

from api import Api, LowerCaseDict
from fields import *

_id_cache = {}

ActiveContext = None

class LinodeObject(object):
  fields = None
  update_method = None
  create_method = None
  primary_key   = None
  list_method   = None

  def __init__(self, entry={}):
    entry = dict([(str(k), v) for k,v in entry.items()])
    self.__entry = LowerCaseDict(entry)

  def __getattr__(self, name):
    name = name.replace('_LinodeObject', '')
    if name == '__entry':
      return self.__dict__[name]
    elif name not in self.fields:
      raise AttributeError
    else:
      f= self.fields[name]
      value = None
      if f.field.lower() in self.__entry:
        value = self.__entry[f.field.lower()]
      return f.to_py(value)

  def __setattr__(self, name, value):
    name = name.replace('_LinodeObject', '')
    if name == '__entry':
      object.__setattr__(self, name, value)
    elif name not in self.fields:
      raise AttributeError
    else:
      f = self.fields[name]
      self.__entry[f.field.lower()] = f.to_linode(value)

  def __str__(self):
    s = []
    for k,v in self.fields.items():
      if v.field in self.__entry:
        value = v.to_py(self.__entry[v.field])
        if isinstance(value, list):
          s.append('%s: [%s]' % (k, ', '.join([str(x) for x in value])))
        else:
          s.append('%s: %s' % (k, str(value)))
    return '['+', '.join(s)+']'

  def save(self):
    if self.id:
      self.update()
    else:
      self.id = self.create_method(ActiveContext, **self.__entry)[self.primary_key]

  def update(self):
    self.update_method(ActiveContext, **self.__entry)

  @classmethod
  def __resolve_kwargs(self, kw):
    kwargs = {}
    for k, v in kw.items():
      f = self.fields[k.lower()]
      kwargs[f.field] = f.to_linode(v)
    return kwargs

  @classmethod
  def list(self, **kw):
    kwargs = self.__resolve_kwargs(kw)

    """
    if self not in _id_cache:
      _id_cache[self] = {}
    """

    for l in self.list_method(ActiveContext, **kwargs):
      l = LowerCaseDict(l)
      o = self(l)
      o.cache_add()
      yield o

  @classmethod
  def get(self, **kw):
    kwargs = self.__resolve_kwargs(kw)

    """
    if self not in _id_cache:
      _id_cache[self] = {}
    """

    result = None
    """
    for k,v in _id_cache[self].items():
      found = True
      for i, j in kwargs.items():
        if i not in v or v[i] != j:
          found = False
          break
      if not found:
        continue
      else:
        result = v
        break
    """


    if not result:
      result = LowerCaseDict(self.list_method(ActiveContext, **kwargs)[0])
      o = self(result)
      o.cache_add()
      return o
    else:
      return self(result)

  def cache_remove(self):
    pass
    """
    del _id_cache[self.__class__][self.__entry[self.primary_key]]
    """

  def cache_add(self):
    pass
    """
    key = self.__class__
    if key not in _id_cache:
      _id_cache[key] = {}

    _id_cache[key][self.__entry[self.primary_key]] = self.__entry
    """

class Datacenter(LinodeObject):
  fields = {
    'id'        : IntField('DatacenterID'),
    'location'  : CharField('Location'),
    'name'      : CharField('Location'),
  }

  list_method = Api.avail_datacenters
  primary_key =  'DatacenterID'

class LinodePlan(LinodeObject):
  fields = {
    'id'      : IntField('PlanID'),
    'label'   : CharField('Label'),
    'price'   : FloatField('Price'),
    'ram'     : IntField('Ram'),
    'xfer'    : IntField('Xfer'),
  }

  list_method = Api.avail_linodeplans
  primary_key = 'PlanID'

class Linode(LinodeObject):
  fields = {
    'id'                : IntField('LinodeID'),
    'datacenter'        : ForeignField(Datacenter),
    'plan'              : ForeignField(LinodePlan),
    'term'              : ChoiceField('PaymentTerm', choices=[1, 12, 24]),
    'name'              : CharField('Label'),
    'label'             : CharField('Label'),
    'group'             : Field('lpm_displayGroup'),
    'cpu_enabled'       : BoolField('Alert_cpu_enabled'),
    'cpu_threshold'     : IntField('Alert_cpu_threshold'),
    'diskio_enabled'    : BoolField('Alert_diskio_enabled'),
    'diskio_threshold'  : IntField('Alert_diskio_enabled'),
    'bwin_enabled'      : BoolField('Alert_bwin_enabled'),
    'bwin_threshold'    : IntField('Alert_bwin_threshold'),
    'bwout_enabled'     : BoolField('Alert_bwout_enabeld'),
    'bwout_threshold'   : IntField('Alert_bwout_threshold'),
    'bwquota_enabled'   : BoolField('Alert_bwquota_enabled'),
    'bwquota_threshold' : IntField('Alert_bwquota_threshold'),
    'backup_window'     : IntField('backupWindow'),
    'backup_weekly_day' : ChoiceField('backupWeeklyDay', choices=list(range(6))),
    'watchdog'          : BoolField('watchdog'),
    'total_ram'         : IntField('TotalRam'),
    'total_diskspace'   : IntField('TotalHD'),
    'total_xfer'        : IntField('TotalXfer'),
    'status'            : IntField('Status'),
  }

  update_method = Api.linode_update
  create_method = Api.linode_create
  primary_key   = 'LinodeID'
  list_method   = Api.linode_list

  def boot(self):
    ### TODO XXX FIXME return LinodeJob
    return ActiveContext.linode_boot(linodeid=self.id)['JobID']

  def shutdown(self):
    ### TODO XXX FIXME return LinodeJob
    return ActiveContext.linode_shutdown(linodeid=self.id)['JobID']

  def reboot(self):
    ### TODO XXX FIXME return LinodeJob
    return ActiveContext.linode_reboot(linodeid=self.id)['JobID']

  def delete(self):
    ActiveContext.linode_delete(linodeid=self.id)
    self.cache_remove()

class LinodeJob(LinodeObject):
  fields = {
    'id'            : IntField('JobID'),
    'linode'        : ForeignField(Linode),
    'label'         : CharField('Label'),
    'name'          : CharField('Label'),
    'entered'       : DateTimeField('ENTERED_DT'),
    'started'       : DateTimeField('HOST_START_DT'),
    'finished'      : DateTimeField('HOST_FINISH_DT'),
    'message'       : CharField('HOST_MESSAGE'),
    'duration'      : IntField('DURATION'),
    'success'       : BoolField('HOST_SUCCESS'),
    'pending_only'  : BoolField('PendingOnly'),
  }

  list_method = Api.linode_job_list
  primary_key = 'JobID'

class Distribution(LinodeObject):
  fields = {
    'id'        : IntField('DistributionID'),
    'label'     : CharField('Label'),
    'name'      : CharField('Label'),
    'min'       : IntField('MinImageSize'),
    '64bit'     : BoolField('Is64Bit'),
    'created'   : DateTimeField('CREATE_DT'),
  }

  list_method = Api.avail_distributions
  primary_key = 'DistributionID'

class LinodeDisk(LinodeObject):
  fields = {
    'id'      : IntField('DiskID'),
    'linode'  : ForeignField(Linode),
    'type'    : ChoiceField('Type', choices=['ext3', 'swap', 'raw']),
    'size'    : IntField('Size'),
    'name'    : CharField('Label'),
    'label'   : CharField('Label'),
    'status'  : IntField('Status'),
    'created' : DateTimeField('Create_DT'),
    'updated' : DateTimeField('Update_DT'),
    'readonly': BoolField('IsReadonly'),
  }

  update_method = Api.linode_disk_update
  create_method = Api.linode_disk_create
  primary_key   = 'DiskID'
  list_method   = Api.linode_disk_list

  def duplicate(self):
    ret = ActiveContext.linode_disk_duplicate(linodeid=self.linode.id, diskid=self.id)
    disk = LinodeDisk.get(linode=self.linode, id=ret['DiskID'])
    job = LinodeJob(linode=self.linode, id=ret['JobID'])
    return (disk, job)

  def resize(self, size):
    ret = ActiveContext.linode_disk_resize(linodeid=self.linode.id, diskid=self.id, size=size)
    return LinodeJob.get(linode=self.linode, id=ret['JobID'])

  def delete(self):
    ret = ActiveContext.linode_disk_delete(linodeid=self.linode.id, diskid=self.id)
    job = LinodeJob.get(linode=self.linode, id=ret['JobID'])
    self.cache_remove()
    return job

  @classmethod
  def create_from_distribution(self, linode, distribution, root_pass, label, size, ssh_key=None):
    l = ForeignField(Linode).to_linode(linode)
    d = ForeignField(Distribution).to_linode(distribution)
    ret = ActiveContext.linode_disk_createfromdistribution(linodeid=l, distributionid=d,
            rootpass=root_pass, label=label, size=size, rootsshkey=ssh_key)
    disk = self.get(id=ret['DiskID'], linode=linode)
    job = LinodeJob(id=ret['JobID'], linode=linode)
    return (disk, job)

class Kernel(LinodeObject):
  fields = {
    'id'    : IntField('KernelID'),
    'label' : CharField('Label'),
    'name'  : CharField('Label'),
    'is_xen': BoolField('IsXen'),
  }

  list_method = Api.avail_kernels
  primary_key = 'KernelID'

class LinodeConfig(LinodeObject):
  fields = {
    'id'                  : IntField('ConfigID'),
    'linode'              : ForeignField(Linode),
    'kernel'              : ForeignField(Kernel),
    'disklist'            : ListField('DiskList', type=ForeignField(LinodeDisk)),
    'name'                : CharField('Label'),
    'label'               : CharField('Label'),
    'comments'            : CharField('Comments'),
    'ram_limit'           : IntField('RAMLimit'),
    'root_device_num'     : IntField('RootDeviceNum'),
    'root_device_custom'  : IntField('RootDeviceCustom'),
    'root_device_readonly': BoolField('RootDeviceRO'),
    'disable_updatedb'    : BoolField('helper_disableUpdateDB'),
    'helper_xen'          : BoolField('helper_xen'),
    'helper_depmod'       : BoolField('helper_depmod'),
  }

  update_method = Api.linode_config_update
  create_method = Api.linode_config_create
  primary_key   = 'ConfigID'
  list_method   = Api.linode_config_list

  def delete(self):
    self.cache_remove()
    ActiveContext.linode_config_delete(linodeid=self.linode.id, configid=self.id)

class LinodeIP(LinodeObject):
  fields = {
    'id'        : IntField('IPAddressID'),
    'linode'    : ForeignField(Linode),
    'address'   : CharField('IPADDRESS'),
    'is_public' : BoolField('ISPUBLIC'),
    'rdns'      : CharField('RDNS_NAME'),
  }

  list_method = Api.linode_ip_list
  primary_key = 'IPAddressID'

class Domain(LinodeObject):
  fields = {
    'id'        : IntField('DomainID'),
    'domain'    : CharField('Domain'),
    'name'      : CharField('Domain'),
    'type'      : ChoiceField('Type', choices=['master', 'slave']),
    'soa_email' : CharField('SOA_Email'),
    'refresh'   : IntField('Refresh_sec'),
    'retry'     : IntField('Retry_sec'),
    'expire'    : IntField('Expire_sec'),
    'ttl'       : IntField('TTL_sec'),
    'status'    : ChoiceField('Status', choices=['0', '1', '2']),
    'master_ips': ListField('master_ips', type=CharField('master_ips')),
  }

  update_method = Api.domain_update
  create_method = Api.domain_create
  primary_key   = 'DomainID'
  list_method   = Api.domain_list

  STATUS_OFF  = 0
  STATUS_ON   = 1
  STATUS_EDIT = 2

  def delete(self):
    self.cache_remove()
    ActiveContext.domain_delete(domainid=self.id)

class Resource(LinodeObject):
  fields = {
    'id'        : IntField('ResourceID'),
    'domain'    : ForeignField(Domain),
    'name'      : CharField('Name'),
    'type'      : CharField('Type'),
    'target'    : CharField('Target'),
    'priority'  : IntField('Priority'),
    'weight'    : IntField('Weight'),
    'port'      : IntField('Port'),
    'protocol'  : CharField('Protocol'),
    'ttl'       : IntField('TTL_sec'),
  }

  update_method = Api.domain_resource_update
  create_method = Api.domain_resource_create
  primary_key   = 'ResourceID'
  list_method   = Api.domain_resource_list

  def delete(self):
    self.cache_remove()
    ActiveContext.domain_resource_delete(domainid=self.domain.id, resourceid=self.id)

  @classmethod
  def list_by_type(self, domain, only=None):
    resources = self.list(domain=domain)
    r_by_type = {
      'A'     : [],
      'CNAME' : [],
      'MX'    : [],
      'SRV'   : [],
      'TXT'   : [],
    }

    for r in resources: r_by_type[r.type.upper()].append(r)

    if only:
      return r_by_type[only.upper()]
    else:
      return r_by_type

def _iter_class(self, results):
  _id_cache[self] = {}
  results = LowerCaseDict(results)

  d = results['data']
  for i in d: self(i).cache_add()

def fill_cache():
  _api.batching = True
  _api.linode_list()
  _api.avail_linodeplans()
  _api.avail_datacenters()
  _api.avail_distributions()
  _api.avail_kernels()
  _api.domain_list()
  ret = _api.batchFlush()

  for i,k in enumerate([Linode, LinodePlan, Datacenter, Distribution, Kernel, Domain]):
    _iter_class(k, ret[i])

  for k in _id_cache[Linode].keys():
    _api.linode_config_list(linodeid=k)
    _api.linode_disk_list(linodeid=k)

  for k in _id_cache[Domain].keys():
    _api.domain_resource_list(domainid=k)

  ret = _api.batchFlush()

  for r in ret:
    r = LowerCaseDict(r)
    if r['action'] == 'linode.config.list':
      _iter_class(LinodeConfig, r)
    elif r['action'] == 'linode.disk.list':
      _iter_class(LinodeDisk, r)
    elif r['action'] == 'domain.resource.list':
      _iter_class(Resource, r)

  _api.batching = False

def setup_logging():
  logging.basicConfig(level=logging.DEBUG)
