"""Module encapsulating all requests to the database (PostgreSQL in this case).
"""

# Copyright (c) 2004 Infrae. All rights reserved.
# See also LICENSE.txt

try:
    from mod_python import apache
    no_apache = False
except ImportError:
    no_apache = True
    pass

import psycopg as SQL

###

class DBConn ( object ):

    def __init__ ( self, dbopts ):
        self._db = None
        self.connect(dbopts)
        return

    def connect ( self, dbopts ):
        if self._db:
            raise DataError, 'Connection already established!'
        dsn = ''
        for key, value in dbopts.items():
            if key == 'dbname':
                s = 'dbname=%s'
            elif key == 'dbhost':
                s = 'host=%s'
            elif key == 'dbuser':
                s = 'user=%s'
            elif key == 'dbpasswd':
                s = 'password=%s'
            else:
                # unknown, skip
                continue
            dsn += s % value + ' '
        min_conns = 1
        if not no_apache:
            min_conns = 2
            # calculate connection parameters
            if apache.mpm_query(apache.AP_MPMQ_IS_THREADED):
                for t in apache.config_tree():
                    if t[0] == 'MaxClients':
                        max_conns = int(t[1]) + 5
                        if max_conns >= 30:
                            min_conns = max_conns / 10 + 1
                        break
            else:
                max_conns = 3
            apache.log_error('DB: min=%d, max=%d' % (min_conns, max_conns))
        else:
            max_conns = 3
        _db = SQL.connect(dsn, minconn=min_conns, maxconn=max_conns, serialize=0)
        self._db = _db
        return

    def close ( self ):
        if self._db:
            self._db.close()
            self._db = None
        return
        
    def cursor ( self ):
        c = self._db.cursor()
        c.execute("SET client_encoding TO 'UTF-8';")
        return c
#
###

IntegrityError = SQL.IntegrityError
ProgrammingError = SQL.ProgrammingError
NotSupportedError = SQL.NotSupportedError
OperationalError = SQL.OperationalError
InterfaceError = SQL.InterfaceError
InternalError = SQL.InternalError
DataError = SQL.DataError
DatabaseError = SQL.DatabaseError

AnyDBError = (ProgrammingError, IntegrityError, NotSupportedError,
              OperationalError, InterfaceError, InterfaceError,
              DataError, DatabaseError)
###

_db = None

###

def connect ( dbopts ):
    """Connect to the database.

    dbopts is a dict with the following keys:
    dbname:   name of the database to use
    dbuser:   user name to use for the connect
    dbpasswd: password to use for the connect
    dbhost:   the host the database is running on
    """
    global _db
    if not _db:
        _db = DBConn(dbopts)
    return _db
#

###
# functions used by the rr_* modules from within Apache

def get_user_pw_for ( repo_name ):
    """Return user and password for master access to the repository.
    """
    con = _db
    q = "SELECT id, master_user, master_pw FROM rr.repositories WHERE name = %s;"
    curs = con.cursor()
    try:
        try:
            curs.execute(q, (repo_name,))
            res = curs.fetchone()
            curs.commit()
            if not res:
                return (None, None, None)
            return res
        except:
            raise
    finally:
        curs.close()
    return (None, None, None)
#

def get_resource_id ( req, path, reponame ):
    """Returns the internal # and the object id for the resource w/ the given path.

    If nothing is found, (None, None) is returned.

    This function simply queries the resource table w/o checking if the client
    owning the resource is disabled.
    """
    q = """SELECT res.id, res.object_id
             FROM rr.resources res, rr.repositories r
            WHERE r.name = %s
              AND res.repo_id = r.id
              AND res.path = %s ;"""
    con = _db
    curs = con.cursor()
    if type(path) == unicode:
        path = path.encode('utf-8')
    if type(reponame) == unicode:
        reponame = reponame.encode('utf-8')
    try:
        curs.execute(q, (reponame, path))
        res = curs.fetchone()
        curs.commit()
        if not res:
            curs.close()
            return (None, None)
        curs.close()
        return res
    except AnyDBError, ex:
        x = str(ex)
        req.log_error('DB ERROR: %s' % x)
        raise
    return (None, None)
#

def get_resource_info ( req, path, reponame ):
    """Returns the internal #, the url to call and the object id for the
    resource w/ the given path in the given repository.

    If nothing is found, (None, None, None) is returned.

    This function does check if the client owning the resource is enabled.
    """
    q = """SELECT res.id, c.auth_url, res.object_id
             FROM rr.resources res, rr.clients c, rr.repositories r, rr.repoclient rc
            WHERE r.name = %s
              AND res.repo_id = r.id
              AND rc.repo_id = r.id
              AND c.id = res.owner_client_id
              AND rc.client_id = res.owner_client_id
              AND rc.state = 'A'
              AND res.path = %s ;"""
##     req.log_error('DB INFO: checking %s' % path) #, apache.APLOG_NOTICE)
    con = _db
    curs = con.cursor()
    if type(path) == unicode:
        path = path.encode('utf-8')
    if type(reponame) == unicode:
        reponame = reponame.encode('utf-8')
    try:
        curs.execute(q, (reponame, path))
        res = curs.fetchone()
        curs.commit()
        if not res:
            curs.close()
            return (None, None, None)
        curs.close()
        return res
    except AnyDBError, ex:
        x = str(ex)
        req.log_error('DB ERROR: %s' % x)
        raise
    return (None, None, None)
#

def get_client_auth_url ( req, client_name ):
    """Returns the authorization url for the given client name.

    If nothing is found, None is returned.
    """
    q = """SELECT c.auth_url as url
             FROM rr.clients c
            WHERE c.name = %s;""" 
    con = _db
    curs = con.cursor()
    if type(client_name) == unicode:
        client_name = client_name.encode('utf-8')
    try:
        curs.execute(q, (client_name, ))
        res = curs.fetchone()
        curs.commit()
        if not res:
            curs.close()
            return None
        curs.close()
        return res[0]
    except AnyDBError, ex:
        x = str(ex)
        req.log_error('DB ERROR: %s' % x)
        raise
    return None
#

def get_upload_data ( req, reponame, con=None ):
    """Returns master user/password, filesystem top dir, host and path.

    If nothing is found, (None,None,None,None) is returned.
    """
    q = """SELECT r.master_user, r.master_pw, r.fs_top_dir, r.host, r.path
             FROM rr.repositories r
            WHERE r.name = %s;"""
    if con is None:
        con = _db
    curs = con.cursor()
    if type(reponame) == unicode:
        reponame = reponame.encode('utf-8')
    try:
        curs.execute(q, (reponame, ))
        res = curs.fetchone()
        curs.commit()
        if not res:
            curs.close()
            return None
        curs.close()
        return res
    except AnyDBError, ex:
        x = str(ex)
        req.log_error('DB ERROR: %s' % x)
        raise
    return tuple([None]*4)
#

def store_new_resource ( request, path ):
    """Inserts a new resource w/ the given path into the database.
    """
    user = request.user
    object_id = request.object_id
    client_name = request.client_name
    q = """SELECT rc.client_id, rc.repo_id
             FROM rr.repoclient rc, rr.clients c
            WHERE rc.client_id = c.id
              AND c.name = %(client_name)s ;"""
    curs = _db.cursor()
    if type(path) == unicode:
        path = path.encode('utf-8')
    try:
        curs.execute(q, locals())
        res = curs.fetchone()
        curs.commit()
        if not res:
            curs.close()
            return None
    except AnyDBError, ex:
        x = str(ex)
        request.log_error('DB ERROR: %s' % x)
        raise
    client_id, repo_id = res
    q = """INSERT INTO rr.resources(repo_id, owner_client_id, path, owner, object_id)
           VALUES (%(repo_id)d, %(client_id)d, %(path)s, %(user)s, %(object_id)s);"""
##     curs.begin()
    try:
        curs.execute(q, locals())
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        curs.close()
        x = str(ex)
        request.log_error('DB ERROR: %s' % x)
        raise
    curs.close()
    return True
#

def store_properties ( resid, props ):
    q = """SELECT *
             FROM rr.properties
            WHERE resource_id = %(resid)s
            FOR UPDATE;"""
    curs = _db.cursor()
    try:
        curs.execute(q, locals())
        curs.fetchall()
        curs.execute("""DELETE FROM rr.properties WHERE resource_id = %(resid)s""", locals())
        # insert new propreties
        q = """INSERT INTO rr.properties(resource_id, nsuri, name, qname, value)
               VALUES (%(resid)s, %(nsuri)s, %(name)s, %(qname)s, %(value)s);"""
        for k, value in props.iteritems():
            if not value:
                # skip empty properties
                continue
            name, nsuri = k
            qname = "%s %s" % (nsuri, name)
            curs.execute(q, locals())
        curs.commit()
        curs.close()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

def delete_resource ( resid ):
    """Delete the specified resource from the database.

    Also deletes the properties for this resource from the db.
    """
    q = """DELETE FROM rr.resources WHERE id = %(resid)s;"""
    curs = _db.cursor()
    try:
        curs.execute(q, locals())
        curs.commit()
        curs.close()
    except AnyDBError, ex:
        curs.rollback()
        curs.close()
        raise
    return
#

def fetch_properties_for ( rid ):
    q = """SELECT name, value FROM rr.properties WHERE resource_id = %d;"""
    curs = _db.cursor()
    d = { 'id': rid }
    try:
        curs.execute(q, (rid,))
        res = curs.fetchall()
        for r in res:
            name, value = r
            d[name] = value.decode('utf-8')
    except AnyDBError, ex:
        curs.rollback()
        curs.close()
        raise
    return d
#

def find_unowned_resources ( start_time, end_time, mime_type ):
    """Query the db for unowned resouces that match the given parameters.

    Returns a list of dicts describing the found resources.
    """
    if not start_time:
        start_time = '2000-01-01T00:00:00Z'
    if not end_time:
        end_time = '3000-12-31T00:00:00Z'
    
    ret = []
    # first select all resources which match the given parameters
    q = """SELECT r.resource_id as id FROM rr.properties r
            WHERE name = 'lastmodified'
              AND value BETWEEN %(start_time)s AND %(end_time)s"""
    if mime_type:
        q += """
           INTERSECT
           SELECT r.resource_id as id FROM rr.properties r
            WHERE name = 'getcontenttype'
              AND value = %(mime_type)s"""
    q = """SELECT a.id, b.path FROM (%s) a, rr.resources b
            WHERE a.id = b.id
              AND b.owner IS NULL ;""" % q
    curs = _db.cursor()
    try:
        curs.execute(q, locals())
        res = curs.fetchall()
    except AnyDBError, ex:
        curs.rollback()
        curs.close()
        raise
    # get property values for selected resources
    for r in res:
        rid = r[0]
        path = r[1]
        props = fetch_properties_for(rid)
        props['path'] = path
        ret.append(props)
    return ret
#
###

###
# functions used by the *admin scripts

# Repositories

def get_all_repos ():
    """Return a list of dicts describing all repositories defined in the database.
    """
    q = """SELECT * FROM rr.repositories;"""
    con = _db
    ret = None
    curs = con.cursor()
    try:
        curs.execute(q)
        ret = curs.dictfetchall()
        curs.commit()
    except AnyDBError:
        curs.rollback()
        raise
    return ret
#

def add_repository (  name, host, path, uploadpath, muser, mpw, topdir ):
    """Adds a repository to the database.
    """
    con = _db
    q = """INSERT INTO rr.repositories (name, master_user, master_pw, fs_top_dir, host, path, upload_path) VALUES (%s, %s, %s, %s, %s, %s, %s);"""
    curs = con.cursor()
    try:
        curs.execute(q, (name, muser, mpw, topdir, host, path, uploadpath))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

def delete_repository ( name ):
    """Delete the named repository entry from the database.
    """
    con = _db
    curs = con.cursor()
    try:
        q = """SELECT id FROM rr.repositories WHERE name = %s;"""
        curs.execute(q, (name,))
        ret = curs.fetchall()
        try:
            repoid = ret[0][0]
            repoid = int(repoid)
        except IndexError:
            # no repository with the given name found
            # consider it deleted :)
            return
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """DELETE FROM rr.repositories WHERE id = %d;"""
        curs.execute(q, (repoid,))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

# Clients

def get_all_clients ():
    """Return a list of dicts describing all clients defined in the database.
    """
    q = """SELECT c.*, rc.state FROM rr.clients c LEFT OUTER JOIN rr.repoclient rc ON ( c.id = rc.client_id);"""
    con = _db
    ret = None
    curs = con.cursor()
    try:
        curs.execute(q)
        ret = curs.dictfetchall()
        curs.commit()
    except AnyDBError:
        curs.rollback()
        raise
    return ret
#

def add_client (  name, authurl, authdomain ):
    """Adds a client to the database.
    """
    con = _db
    curs = con.cursor()
    try:
        q = """INSERT INTO rr.clients(name, auth_url, auth_domain) VALUES (%s, %s, %s);"""
        curs.execute(q, (name, authurl, authdomain))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

def delete_client ( name ):
    """Delete the named client entry from the database.
    """
    con = _db
    curs = con.cursor()
    try:
        q = """SELECT id FROM rr.clients WHERE name = %s;"""
        curs.execute(q, (name,))
        ret = curs.fetchall()
        try:
            cid = ret[0][0]
            cid = int(cid)
        except IndexError:
            # no result found, means already deleted
            return
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """DELETE FROM rr.clients WHERE id = %d;"""
        curs.execute(q, (cid,))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

def activate_client ( cname, rname ):
    """Activate the client cname for repository rname.
    """
    con = _db
    curs = con.cursor()
    try:
        q = """SELECT id FROM rr.clients WHERE name = %s;"""
        curs.execute(q, (cname,))
        ret = curs.fetchall()
        try:
            cid = ret[0][0]
            cid = int(cid)
        except IndexError:
            raise IndexError, 'no result found'
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """SELECT id FROM rr.repositories WHERE name = %s;"""
        curs.execute(q, (rname,))
        ret = curs.fetchall()
        try:
            repoid = ret[0][0]
            repoid = int(repoid)
        except IndexError:
            raise IndexError, 'no result found'
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """SELECT state FROM rr.repoclient WHERE repo_id = %d AND client_id = %d FOR UPDATE;"""
        curs.execute(q, (repoid, cid))
        ret = curs.fetchall()
        if not ret:
            # no entry found, initial activation
            q = """INSERT INTO rr.repoclient(repo_id, client_id) VALUES(%d,%d);"""
        else:
            # entry found, do an update
            q = """UPDATE rr.repoclient SET state ='A' WHERE repo_id = %d AND client_id = %d;"""
        curs.execute(q, (repoid, cid))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#

def deactivate_client ( cname, rname ):
    """Deactivate the client cname for repository rname.
    """
    con = _db
    curs = con.cursor()
    try:
        q = """SELECT id FROM rr.clients WHERE name = %s;"""
        curs.execute(q, (cname,))
        ret = curs.fetchall()
        try:
            cid = ret[0][0]
            cid = int(cid)
        except IndexError:
            raise IndexError, 'no result found'
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """SELECT id FROM rr.repositories WHERE name = %s;"""
        curs.execute(q, (rname,))
        ret = curs.fetchall()
        try:
            repoid = ret[0][0]
            repoid = int(repoid)
        except IndexError:
            raise IndexError, 'no result found'
        except ValueError:
            raise
    except AnyDBError, ex:
        curs.rollback()
        raise
    try:
        q = """SELECT state FROM rr.repoclient WHERE repo_id = %d AND client_id = %d FOR UPDATE;"""
        curs.execute(q, (repoid, cid))
        ret = curs.fetchall()
        if not ret:
            # no entry found, do insert
            q = """INSERT INTO rr.repoclient(repo_id, client_id, state) VALUES(%d,%d,'D');"""
        else:
            # entry found, do an update
            q = """UPDATE rr.repoclient SET state ='D' WHERE repo_id = %d AND client_id = %d;"""
        curs.execute(q, (repoid, cid))
        curs.commit()
    except AnyDBError, ex:
        curs.rollback()
        raise
    return
#
