import base64
import sqlite3
import time

from .encryption import (generate_key,
                         aes_cfb8_wrap, aes_cfb8_unwrap,
                         aes_cfb128_wrap, aes_cfb128_unwrap)
from .external_keys import load_ext_key, store_ext_key

class SecretsDatabase():
    def __init__(self, path, key_path):
        self.db = sqlite3.connect(path)
        self.kp = key_path
        self.mk = None
        self.dk = None
        self.ver = 0
        self.initialize()
        self.upgrade()
        self.load_keys()

    def initialize(self):
        cur = self.db.cursor()
        cur.execute("CREATE TABLE IF NOT EXISTS version (" \
                    "   version INTEGER" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS sequence (" \
                    "   next INTEGER" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS parameters (" \
                    "   name TEXT," \
                    "   value TEXT" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS collections (" \
                    "   object TEXT," \
                    "   label TEXT," \
                    "   created INTEGER," \
                    "   modified INTEGER" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS aliases (" \
                    "   alias TEXT," \
                    "   target TEXT" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS items (" \
                    "   object TEXT," \
                    "   label TEXT," \
                    "   created INTEGER," \
                    "   modified INTEGER" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS attributes (" \
                    "   object TEXT," \
                    "   attribute TEXT," \
                    "   value TEXT" \
                    ")")
        cur.execute("CREATE TABLE IF NOT EXISTS secrets (" \
                    "   object TEXT," \
                    "   secret TEXT," \
                    "   type TEXT" \
                    ")")
        self.db.commit()

    # Encryption keys

    def _store_mkey(self, key):
        print("DB: storing master key to %r" % (self.kp))
        store_ext_key(self.kp, base64.b64encode(key).decode())

    def _load_mkey(self):
        print("DB: loading master key from %r" % (self.kp))
        try:
            mkey = base64.b64decode(load_ext_key(self.kp))
            if len(mkey) != 32:
                raise IOError("wrong mkey length (expected 32 bytes)")
        except (KeyError, FileNotFoundError):
            raise RuntimeError("could not load the database key from %r" % (self.kp))
        self.mk = mkey

    def _load_dkey(self, *, v=0):
        if (v or self.ver) == 3:
            cur = self.db.cursor()
            cur.execute("SELECT value FROM parameters WHERE name = 'dkey'")
            dkey, = cur.fetchone()
            try:
                dkey = self._decrypt_buf(dkey, with_mkey=True, v=3)
            except IOError as e:
                raise IOError("wrong mkey (%s)" % e)
            if len(dkey) != 32:
                raise IOError("wrong dkey length (expected 32 bytes)")
            self.dk = dkey
        else:
            raise NotImplementedError("unknown schema version %r" % (v or self.ver))

    def load_keys(self):
        if self.ver >= 2:
            self._load_mkey()
            self._load_dkey()

    def _encrypt_buf(self, buf, *, with_mkey=False, v=0):
        key = self.mk if with_mkey else self.dk
        if (v or self.ver) >= 3:
            return aes_cfb128_wrap(buf, key)
        elif (v or self.ver) == 2:
            return aes_cfb8_wrap(buf, key)
        else:
            raise NotImplementedError("unknown schema version %r" % (v or self.ver))

    def _decrypt_buf(self, buf, *, with_mkey=None, v=0):
        key = self.mk if with_mkey else self.dk
        if (v or self.ver) >= 3:
            return aes_cfb128_unwrap(buf, key)
        elif (v or self.ver) == 2:
            return aes_cfb8_unwrap(buf, key)
        else:
            raise NotImplementedError("unknown schema version %r" % (v or self.ver))

    # Schema upgrades

    def _upgrade_v0_to_v1(self):
        # Undo commit affc514 "make items use bus paths underneath their collection"
        cur = self.db.cursor()
        cur.execute("SELECT object FROM items" \
                    " WHERE object LIKE '/org/freedesktop/secrets/collection/c%/i%'")
        res = cur.fetchall()
        for (old_object,) in res:
            item_id = old_object.split("/")[-1]
            new_object = "/org/freedesktop/secrets/item/%s" % item_id
            print("DB: moving object %r => %r" % (old_object, new_object))
            cur.execute("UPDATE items      SET object = ? WHERE object = ?",
                        (new_object, old_object))
            cur.execute("UPDATE secrets    SET object = ? WHERE object = ?",
                        (new_object, old_object))
            cur.execute("UPDATE attributes SET object = ? WHERE object = ?",
                        (new_object, old_object))

    def _upgrade_v1_to_v3(self):
        # Version 2 encrypts all secrets using the database master key
        cur = self.db.cursor()
        # Generate a "master key"
        print("DB: generating a master key")
        mkey = generate_key()
        self._store_mkey(mkey)
        self.mk = mkey
        # Generate a "data key"
        print("DB: generating a data key")
        dkey = generate_key()
        blob = self._encrypt_buf(dkey, with_mkey=True, v=3)
        cur.execute("INSERT INTO parameters VALUES ('dkey', ?)", (blob,))
        self.dk = dkey
        # Encrypt all currently stored secrets
        cur.execute("SELECT object, secret FROM secrets")
        res = cur.fetchall()
        for object, blob in res:
            print("DB: encrypting secret %r" % (object,))
            blob = self._encrypt_buf(blob, v=3)
            cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object))

    def _upgrade_v2_to_v3(self):
        # Version 3 uses AES-CFB128 instead of (badly chosen) AES-CFB8
        cur = self.db.cursor()
        # Re-encrypt the data key
        self._load_mkey()
        cur.execute("SELECT value FROM parameters WHERE name = 'dkey'")
        blob, = cur.fetchone()
        blob = self._decrypt_buf(blob, with_mkey=True, v=2)
        blob = self._encrypt_buf(blob, with_mkey=True, v=3)
        cur.execute("UPDATE parameters SET value = ? WHERE name = 'dkey'", (blob,))
        # Re-encrypt all currently stored secrets
        self._load_dkey(v=3)
        cur.execute("SELECT object, secret FROM secrets")
        res = cur.fetchall()
        for object, blob in res:
            print("DB: re-encrypting secret %r" % (object,))
            blob = self._decrypt_buf(blob, v=2)
            blob = self._encrypt_buf(blob, v=3)
            cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object))

    def upgrade(self):
        print("DB: current database version is %d" % self.get_version())
        if self.get_version() == 0:
            print("DB: upgrading to version %d" % (1,))
            self._upgrade_v0_to_v1()
            self.db.cursor().execute("UPDATE version SET version = ?", (1,))
            self.db.commit()
        if self.get_version() == 1:
            print("DB: upgrading to version %d" % (3,))
            self._upgrade_v1_to_v3()
            self.db.cursor().execute("UPDATE version SET version = ?", (3,))
            self.db.commit()
            print("DB: vacuuming database")
            self.db.cursor().execute("VACUUM")
        if self.get_version() == 2:
            print("DB: upgrading to version %d" % (3,))
            self._upgrade_v2_to_v3()
            self.db.cursor().execute("UPDATE version SET version = ?", (3,))
            self.db.commit()
        self.ver = self.get_version()
        print("DB: new database version is %d" % self.ver)

    def get_version(self):
        cur = self.db.cursor()
        cur.execute("SELECT version FROM version")
        res = cur.fetchone()
        if res:
            version = res[0]
        else:
            version = 0
            cur.execute("INSERT INTO version VALUES (?)", (version,))
            self.db.commit()
        return version

    def get_next_object_id(self):
        cur = self.db.cursor()
        cur.execute("SELECT next FROM sequence")
        res = cur.fetchone()
        if res:
            oid = res[0]
            cur.execute("UPDATE sequence SET next = next + 1")
        else:
            oid = 0
            cur.execute("INSERT INTO sequence VALUES (?)", (oid + 1,))
        self.db.commit()
        print("DB: allocated new object ID %r" % oid)
        return oid

    # Collections

    def add_collection(self, object, label):
        print("DB: adding collection %r with label %r" % (object, label))
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("INSERT INTO collections VALUES (?,?,?,?)", (object, label, now, now))
        self.db.commit()

    def list_collections(self):
        cur = self.db.cursor()
        cur.execute("SELECT object FROM collections")
        return [r[0] for r in cur.fetchall()]

    def collection_exists(self, object):
        return bool(self.get_collection_metadata(object))

    def get_collection_metadata(self, object):
        print("DB: getting collection metadata for %r" % (object,))
        cur = self.db.cursor()
        cur.execute("SELECT label, created, modified FROM collections WHERE object = ?",
                    (object,))
        return cur.fetchone()

    def set_collection_label(self, object, label):
        print("DB: setting label for %r to %r" % (object, label))
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("UPDATE collections SET label = ?, modified = ? WHERE object = ?",
                    (label, now, object))
        self.db.commit()

    def delete_collection(self, object):
        print("DB: deleting collection %r" % (object,))
        cur = self.db.cursor()
        subquery = "SELECT object FROM attributes" \
                   " WHERE attribute = 'xdg:collection' AND value = ?"
        cur.execute("DELETE FROM items WHERE object IN (" + subquery + ")", (object,))
        cur.execute("DELETE FROM secrets WHERE object IN (" + subquery + ")", (object,))
        cur.execute("DELETE FROM attributes WHERE object IN (" + subquery + ")", (object,))
        cur.execute("DELETE FROM aliases WHERE target = ?", (object,))
        cur.execute("DELETE FROM collections WHERE object = ?", (object,))
        self.db.commit()

    # Aliases

    def add_alias(self, alias, target):
        print("DB: adding alias %r -> %r" % (alias, target))
        cur = self.db.cursor()
        cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,))
        cur.execute("INSERT INTO aliases VALUES (?,?)", (alias, target))
        self.db.commit()

    def get_aliases(self):
        cur = self.db.cursor()
        cur.execute("SELECT alias, target FROM aliases")
        return cur.fetchall()

    def resolve_alias(self, alias):
        print("DB: resolving alias %r" % (alias,))
        cur = self.db.cursor()
        cur.execute("SELECT target FROM aliases WHERE alias = ?", (alias,))
        r = cur.fetchone()
        return r[0] if r else None

    def delete_alias(self, alias):
        print("DB: deleting alias %r" % (alias,))
        cur = self.db.cursor()
        cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,))
        self.db.commit()

    # Items

    def add_item(self, object, label, attrs, secret, sec_type):
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("INSERT INTO items VALUES (?,?,?,?)", (object, label, now, now))
        for key, val in attrs.items():
            cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val))
        cur.execute("INSERT INTO secrets VALUES (?,?,?)", (object, self._encrypt_buf(secret),
                                                           sec_type))
        self.db.commit()

    def find_items(self, match_attrs):
        qry = "SELECT object FROM attributes WHERE attribute = ? AND value = ?"
        qry = " INTERSECT ".join([qry] * len(match_attrs))
        parvs = []
        for k, v in match_attrs.items():
            parvs += [k, v]
        print("DB: searching for %r" % parvs)
        cur = self.db.cursor()
        cur.execute(qry, parvs)
        return [r[0] for r in cur.fetchall()]

    def item_exists(self, object):
        return bool(self.get_item_metadata(object))

    def get_item_metadata(self, object):
        print("DB: getting metadata for %r" % object)
        cur = self.db.cursor()
        cur.execute("SELECT label, created, modified FROM items WHERE object = ?",
                    (object,))
        return cur.fetchone()

    def set_item_label(self, object, label):
        print("DB: setting label for %r to %r" % (object, label))
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("UPDATE items SET label = ?, modified = ? WHERE object = ?",
                    (label, now, object))
        self.db.commit()

    def get_item_attributes(self, object):
        print("DB: getting attrs for %r" % object)
        cur = self.db.cursor()
        cur.execute("SELECT attribute, value FROM attributes WHERE object = ?", (object,))
        return {k: v for k, v in cur.fetchall()}

    def set_item_attributes(self, object, attrs):
        print("DB: setting attrs for %r to %r" % (object, attrs))
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("DELETE FROM attributes WHERE object = ?", (object,))
        for key, val in attrs.items():
            cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val))
        cur.execute("UPDATE items SET modified = ? WHERE object = ?", (now, object))
        self.db.commit()

    def get_secret(self, object):
        print("DB: getting secret for %r" % object)
        cur = self.db.cursor()
        cur.execute("SELECT secret, type FROM secrets WHERE object = ?", (object,))
        secret, sec_type = cur.fetchone()
        return self._decrypt_buf(secret), sec_type

    def set_secret(self, object, secret, sec_type):
        print("DB: updating secret for %r" % object)
        if hasattr(secret, "encode"):
            raise ValueError("secret needs to be bytes, not str")
        now = int(time.time())
        cur = self.db.cursor()
        cur.execute("UPDATE secrets SET secret = ?, type = ? WHERE object = ?",
                    (self._encrypt_buf(secret), sec_type, object))
        cur.execute("UPDATE items SET modified = ? WHERE object = ?",
                    (now, object))
        self.db.commit()

    def delete_item(self, object):
        print("DB: deleting item %r" % object)
        cur = self.db.cursor()
        cur.execute("DELETE FROM attributes WHERE object = ?", (object,))
        cur.execute("DELETE FROM secrets WHERE object = ?", (object,))
        cur.execute("DELETE FROM items WHERE object = ?", (object,))
        self.db.commit()
