import sqlalchemy as sa
import simplejson
from twisted.python import log
from twisted.web import resource
from slavealloc import exceptions
from slavealloc.data import queries, model
from slavealloc.logic import allocate, buildbottac

# point your browser to /api/ to see the full set of docs
docs_tpl = """
<h1>REST Interface</h1>
<p>This is a JSON-based REST interface.  Rows are represented as JSON objects with
keys corersponding to database columns.</p>

<p>For each table, there is a similarly named sub-URL, e.g., <a
href="/api/slaves">/api/slaves</a>.  A GET to that URL will return a full dump
of the table.  As a convenience, the slaves and masters tables have all of
their normalized fields denormalized, so in addition to a <tt>poolid</tt> you
will get a <tt>pool</tt> string.</p>

<p>A particular row can be fetched by appending the primary key in the next URL
component, e.g., <a href="/api/pools/3">/api/pools/3</a>.  To fetch the row by
name instead, use the name in the URL and add <tt>?byname=1</tt>, e.g., <a
href="/api/masters/pm03?byname=1">/api/masters/pm03?byname=1</a>.  If a fetched
row does not exist, the API will return <tt>{}</tt> with a 404 status code.</p>

<p>You can PUT a JSON object with any subset of keys to either of these URLs to
modify the row.  The denormalized columns in the masters and slaves cannot be
PUT.</p>

<p>The entire set of available tables is:
    <ul>
    %(tables)s
    </ul>
</p>

<p>The <a href="/api/gettac/slavename">/api/gettac/slavename</a> URL is
different: it invokes the allocator, but does not "commit" the allocation --
meaning that the selected master is not recorded in the
<tt>current_masterid</tt> table for the requested slave.  The result is JSON --
either {success=False} or {success=True, tac='content of buildbot.tac'}.</p>

"""

# base classes

class Instance(resource.Resource):
    isLeaf = True
    okResponse = simplejson.dumps(dict(success=True))
    missingResponse = simplejson.dumps({})

    # a tuple of columns that can be updated via PUT
    update_keys = ()

    # the table to access
    table = None

    # the column containing unique id's
    id_column = None

    # the column containing unique names
    name_column = None

    def __init__(self, id=None, name=None):
        self.id = id
        self.name = name

    def whereClause(self, require_id=False):
        "Calculate a where clause and an args dict for this instance"
        if self.id is not None:
            return (self.id_column == sa.bindparam('id'),
                    dict(id=self.id))
        else:
            assert not require_id
            return (self.name_column == sa.bindparam('name'),
                    dict(name=self.name))

    def render_GET(self, request):
        wc, args = self.whereClause()
        res = self.table.select(wc).execute(args)
        row = res.fetchone()
        res.close()

        request.setHeader('content-type', 'application/json')

        # handle nonexistent rows
        if not row:
            request.setResponseCode(404)
            return self.missingResponse

        return simplejson.dumps(dict(row))

    def render_PUT(self, request):
        json = simplejson.load(request.content)

        sets = dict((k, json[k]) for k in self.update_keys if k in json)
        if not sets:
            return # nothing to do!

        log.msg("%s: updating id %s from %r" %
                (self.table.name, self.id, sets))

        wc, args = self.whereClause(require_id=True)
        args.update(sets)
        self.table.update(wc).execute(args)

        return self.okResponse

class Collection(resource.Resource):
    addSlash = True
    isLeaf = False

    # the query to return for GET requests; if None, this will use the instance
    # class's table and just select everything
    query = None

    def getChild(self, path_component, request):
        if not path_component:
            return

        # if we're looking up by name, try that instead
        if request.args.get('byname'):
            return self.instance_class(name=path_component)
        else:
            return self.instance_class(id=path_component)

    def render_GET(self, request):
        query = self.query
        if query is None:
            query = self.instance_class.table.select()
        res = query.execute()

        request.setHeader('content-type', 'application/json')
        return simplejson.dumps([ dict(r.items()) for r in res.fetchall() ])


# concrete classes

# slaves
class SlaveResource(Instance):
    table = model.slaves
    id_column = model.slaves.c.slaveid
    name_column = model.slaves.c.name
    update_keys = ('distroid', 'dcid', 'bitsid', 'purposeid', 'trustid',
                   'envid', 'poolid', 'basedir', 'locked_masterid', 'notes',
                   'enabled', 'custom_tplid')

class SlavesResource(Collection):
    instance_class = SlaveResource
    query = queries.denormalized_slaves

# masters
class MasterResource(Instance):
    table = model.masters
    id_column = model.masters.c.masterid
    name_column = model.masters.c.nickname
    update_keys = ('nickname', 'fqdn', 'pb_port', 'http_port', 'poolid',
                   'dcid', 'notes', 'enabled')

class MastersResource(Collection):
    instance_class = MasterResource
    query = queries.denormalized_masters

# TAC templates
class TACTemplateResource(Instance):
    table = model.tac_templates
    id_column = model.tac_templates.c.tplid
    name_column = model.tac_templates.c.name
    update_keys = () # view only via API

class TACTemplatesResource(Collection):
    instance_class = TACTemplateResource

# simple
def simple_table_resource(tbl, id_column_name, name_column_name='name'):
    "make instance and collection classes for a simple id/name table"
    class SimpleInstance(Instance):
        table = tbl
        id_column = table.c[id_column_name]
        name_column = table.c[name_column_name]
        update_keys = (name_column_name,)

    class SimpleCollection(Collection):
        instance_class = SimpleInstance

    return SimpleCollection

DistrosResource = simple_table_resource(model.distros, 'distroid')
DatacentersResource = simple_table_resource(model.datacenters, 'dcid')
BitlengthsResource = simple_table_resource(model.bitlengths, 'bitsid')
SpeedsResource = simple_table_resource(model.speeds, 'speedid')
PurposesResource = simple_table_resource(model.purposes, 'purposeid')
TrustlevelsResource = simple_table_resource(model.trustlevels, 'trustid')
EnvironmentsResource = simple_table_resource(model.environments, 'envid')
PoolsResource = simple_table_resource(model.pools, 'poolid')

# allocator

class BuildbotTacResource(resource.Resource):
    isLeaf = True

    def __init__(self, slave):
        self.slave = slave

    def render_GET(self, request):
        try:
            alloc = allocate.Allocation(self.slave)
        except exceptions.NoAllocationError:
            alloc = None

        request.setHeader('content-type', 'application/json')

        if not alloc:
            request.setResponseCode(404)
            return simplejson.dumps(dict(success=False))
        else:
            return simplejson.dumps(dict(
                success=True,
                tac=buildbottac.make_buildbot_tac(alloc)))

class BuildbotTacRootResource(resource.Resource):
    "A JSON-style allocator that will get a TAC file, but not record it"
    isLeaf = False

    def getChild(self, path_component, request):
        return BuildbotTacResource(path_component)

# root URI

class ApiRoot(resource.Resource):
    addSlash = True
    isLeaf = False

    def __init__(self):
        resource.Resource.__init__(self)
        self.tables = []

        self.addTable('slaves', SlavesResource)
        self.addTable('masters', MastersResource)
        self.addTable('distros', DistrosResource)
        self.addTable('datacenters', DatacentersResource)
        self.addTable('bitlengths', BitlengthsResource)
        self.addTable('speeds', SpeedsResource)
        self.addTable('purposes', PurposesResource)
        self.addTable('trustlevels', TrustlevelsResource)
        self.addTable('environments', EnvironmentsResource)
        self.addTable('pools', PoolsResource)
        self.addTable('tac_templates', TACTemplatesResource)
        self.addTable('gettac', BuildbotTacRootResource)

    def addTable(self, name, coll_class):
        self.tables.append(name)
        self.putChild(name, coll_class())

    def getChild(self, path_component, request):
        # allow '/api/' to mean the same as '/api'
        if not path_component:
            return self

    def render_GET(self, request):
        tables_list = '\n'.join([ '<li>%s</li>' % t for t in self.tables ])
        return docs_tpl % dict(tables=tables_list)

def makeRootResource():
    return ApiRoot()
