#!/usr/bin/python

import cache_acls
import getafsgroups
import re
import string
import dns.resolver
from invirt.database import Machine, NIC, Type, Disk, CDROM, Autoinstall
from invirt.config import structs as config
from webcommon import InvalidInput

MAX_MEMORY_TOTAL = 512
MAX_MEMORY_SINGLE = 256
MIN_MEMORY_SINGLE = 16
MAX_DISK_TOTAL = 50
MAX_DISK_SINGLE = 50
MIN_DISK_SINGLE = 0.1
MAX_VMS_TOTAL = 10
MAX_VMS_ACTIVE = 4

class Validate:
    def __init__(self, username, state, machine_id=None, name=None, description=None, owner=None,
                 admin=None, contact=None, memory=None, disksize=None,
                 vmtype=None, cdrom=None, autoinstall=None, strict=False):
        # XXX Successive quota checks aren't a good idea, since you
        # can't necessarily change the locker and disk size at the
        # same time.
        created_new = (machine_id is None)

        if strict:
            if name is None:
                raise InvalidInput('name', name, "You must provide a machine name.")
            if description is None:
                raise InvalidInput('description', description, "You must provide a description.")
            if memory is None:
                raise InvalidInput('memory', memory, "You must provide a memory size.")
            if disksize is None:
                raise InvalidInput('disk', disksize, "You must provide a disk size.")

        if machine_id is not None:
            self.machine = testMachineId(username, state, machine_id)
        machine = getattr(self, 'machine', None)

        owner = testOwner(username, owner, machine)
        if owner is not None:
            self.owner = owner
        admin = testAdmin(username, admin, machine)
        if admin is not None:
            self.admin = admin
        contact = testContact(username, contact, machine)
        if contact is not None:
            self.contact = contact
        name = testName(username, name, machine)
        if name is not None:
            self.name = name
        description = testDescription(username, description, machine)
        if description is not None:
            self.description = description
        if memory is not None:
            self.memory = validMemory(self.owner, state, memory, machine,
                                      on=not created_new)
        if disksize is not None:
            self.disksize = validDisk(self.owner, state, disksize, machine)
        if vmtype is not None:
            self.vmtype = validVmType(vmtype)
        if cdrom is not None:
            if not CDROM.query().get(cdrom):
                raise CodeError("Invalid cdrom type '%s'" % cdrom)
            self.cdrom = cdrom
        if autoinstall is not None:
            self.autoinstall = Autoinstall.query().get(autoinstall)


def getMachinesByOwner(owner, machine=None):
    """Return the machines owned by the same as a machine.

    If the machine is None, return the machines owned by the same
    user.
    """
    if machine:
        owner = machine.owner
    return Machine.query().filter_by(owner=owner)

def maxMemory(owner, g, machine=None, on=True):
    """Return the maximum memory for a machine or a user.

    If machine is None, return the memory available for a new
    machine.  Else, return the maximum that machine can have.

    on is whether the machine should be turned on.  If false, the max
    memory for the machine to change to, if it is left off, is
    returned.
    """
    if machine is not None and machine.memory > MAX_MEMORY_SINGLE:
        # If they've been blessed, let them have it
        return machine.memory
    if not on:
        return MAX_MEMORY_SINGLE
    machines = getMachinesByOwner(owner, machine)
    active_machines = [m for m in machines if m.name in g.xmlist_raw]
    mem_usage = sum([x.memory for x in active_machines if x != machine])
    return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)

def maxDisk(owner, machine=None):
    """Return the maximum disk that a machine can reach.

    If machine is None, the maximum disk for a new machine. Otherwise,
    return the maximum that a given machine can be changed to.
    """
    if machine is not None:
        machine_id = machine.machine_id
    else:
        machine_id = None
    disk_usage = Disk.query().filter(Disk.c.machine_id != machine_id).\
                     join('machine').\
                     filter_by(owner=owner).sum(Disk.c.size) or 0
    return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)

def cantAddVm(owner, g):
    machines = getMachinesByOwner(owner)
    active_machines = [m for m in machines if m.name in g.xmlist_raw]
    if machines.count() >= MAX_VMS_TOTAL:
        return 'You have too many VMs to create a new one.'
    if len(active_machines) >= MAX_VMS_ACTIVE:
        return ('You already have the maximum number of VMs turned on.  '
                'To create more, turn one off.')
    return False

def haveAccess(user, state, machine):
    """Return whether a user has administrative access to a machine"""
    return (user in cache_acls.accessList(machine)
            or (machine.adminable and state.isadmin))

def owns(user, machine):
    """Return whether a user owns a machine"""
    return user in expandLocker(machine.owner)

def validMachineName(name):
    """Check that name is valid for a machine name"""
    if not name:
        return False
    charset = string.lowercase + string.digits + '-'
    if '-' in (name[0], name[-1]) or len(name) > 63:
        return False
    for x in name:
        if x not in charset:
            return False
    return True

def validMemory(owner, g, memory, machine=None, on=True):
    """Parse and validate limits for memory for a given owner and machine.

    on is whether the memory must be valid after the machine is
    switched on.
    """
    try:
        memory = int(memory)
        if memory < MIN_MEMORY_SINGLE:
            raise ValueError
    except ValueError:
        raise InvalidInput('memory', memory,
                           "Minimum %s MiB" % MIN_MEMORY_SINGLE)
    max_val = maxMemory(owner, g, machine, on)
    if not g.isadmin and memory > max_val:
        raise InvalidInput('memory', memory,
                           'Maximum %s MiB for %s' % (max_val, owner))
    return memory

def validDisk(owner, g, disk, machine=None):
    """Parse and validate limits for disk for a given owner and machine."""
    try:
        disk = float(disk)
        if not g.isadmin and disk > maxDisk(owner, machine):
            raise InvalidInput('disk', disk,
                               "Maximum %s G" % maxDisk(owner, machine))
        disk = int(disk * 1024)
        if disk < MIN_DISK_SINGLE * 1024:
            raise ValueError
    except ValueError:
        raise InvalidInput('disk', disk,
                           "Minimum %s GiB" % MIN_DISK_SINGLE)
    return disk

def validVmType(vm_type):
    if vm_type is None:
        return None
    t = Type.query().get(vm_type)
    if t is None:
        raise CodeError("Invalid vm type '%s'"  % vm_type)
    return t

def testMachineId(user, state, machine_id, exists=True):
    """Parse, validate and check authorization for a given user and machine.

    If exists is False, don't check that it exists.
    """
    if machine_id is None:
        raise InvalidInput('machine_id', machine_id,
                           "Must specify a machine ID.")
    try:
        machine_id = int(machine_id)
    except ValueError:
        raise InvalidInput('machine_id', machine_id, "Must be an integer.")
    machine = Machine.query().get(machine_id)
    if exists and machine is None:
        raise InvalidInput('machine_id', machine_id, "Does not exist.")
    if machine is not None and not haveAccess(user, state, machine):
        raise InvalidInput('machine_id', machine_id,
                           "You do not have access to this machine.")
    return machine

def testAdmin(user, admin, machine):
    """Determine whether a user can set the admin of a machine to this value.

    Return the value to set the admin field to (possibly 'system:' +
    admin).  XXX is modifying this a good idea?
    """
    if admin is None:
        return None
    if machine is not None and admin == machine.administrator:
        return None
    if admin == user:
        return admin
    if ':' not in admin:
        if cache_acls.isUser(admin):
            return admin
        admin = 'system:' + admin
    try:
        if user in getafsgroups.getAfsGroupMembers(admin, config.authz[0].cell):
            return admin
    except getafsgroups.AfsProcessError, e:
        errmsg = str(e)
        if errmsg.startswith("pts: User or group doesn't exist"):
            errmsg = 'The group "%s" does not exist.' % admin
        raise InvalidInput('administrator', admin, errmsg)
    #XXX Should we require that user is in the admin group?
    return admin

def testOwner(user, owner, machine=None):
    """Determine whether a user can set the owner of a machine to this value.

    If machine is None, this is the owner of a new machine.
    """
    if owner == user:
        return owner
    if machine is not None and owner in (machine.owner, None):
        return machine.owner
    if owner is None:
        raise InvalidInput('owner', owner, "Owner must be specified")
    try:
        if user not in cache_acls.expandLocker(owner):
            raise InvalidInput('owner', owner, 'You do not have access to the '
                               + owner + ' locker')
    except getafsgroups.AfsProcessError, e:
        raise InvalidInput('owner', owner, str(e))
    return owner

def testContact(user, contact, machine=None):
    if contact is None or (machine is not None and contact == machine.contact):
        return None
    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
        raise InvalidInput('contact', contact, "Not a valid email.")
    return contact

def testDisk(user, disksize, machine=None):
    return disksize

def testName(user, name, machine=None):
    if name is None:
        return None
    name = name.lower()
    if machine is not None and name == machine.name:
        return None
    try:
        hostname = '%s.%s.' % (name, config.dns.domains[0])
        try:
            dns.resolver.query(hostname, 'A')
        except dns.resolver.NoAnswer, e:
            # If we can get the TXT record, then we can verify it's
            # reserved. If this lookup fails, let it bubble up and be
            # dealt with
            answer = dns.resolver.query(hostname, 'TXT')
            txt = answer[0].strings[0]
            if txt.startswith('reserved'):
                raise InvalidInput('name', name, 'The name you have requested has been %s. For more information, contact us at %s' % (txt, config.dns.contact))

        # If the hostname didn't exist, it would have thrown an
        # exception by now - error out
        raise InvalidInput('name', name, 'Name is already taken.')
    except dns.resolver.NXDOMAIN, e:
        if not validMachineName(name):
            raise InvalidInput('name', name, 'You must provide a machine name.  Max 63 chars, alnum plus \'-\', does not begin or end with \'-\'.')
        return name
    except InvalidInput:
        raise
    except:
        # Any other error is a validation failure
        raise InvalidInput('name', name, 'We were unable to verify that this name is available. If you believe this is in error, please contact us at %s' % config.dns.contact)

def testDescription(user, description, machine=None):
    if description is None or description.strip() == '':
        return None
    return description.strip()

def testHostname(user, hostname, machine):
    for nic in machine.nics:
        if hostname == nic.hostname:
            return hostname
    # check if doesn't already exist
    if NIC.select_by(hostname=hostname):
        raise InvalidInput('hostname', hostname,
                           "Already exists")
    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
                           "must only use number, letters, and dashes.")
    return hostname
