"""
Copyright (c) 2015 Alex Kramer <kramer.alex.kramer@gmail.com>

See the LICENSE.txt file at the top-level directory of this distribution.

This module implements JSON-serializable encrypted account storage.
"""

import re
import uuid

import crypto


class PassStore(dict):
    """
    Base password store class. This class is JSON-serializable, and provides
    encrypted account management.

    This class should not be used on its own, since important functionality
    such as enforcing password authentication prior to editing accounts, etc.
    is not provided by this class. Using the methods in this class directly
    will always allow for editing account information. However, the worst that
    can be done using the methods in this class is that account information may
    be modified to incorrect values; previously-encrypted usernames and
    passwords cannot be extracted absent the correct encryption key.
    """
    def __init__(self, master):
        """
        master ::: master password
        """
        self["master"] = crypto.encrypt(master, master)
        self["accounts"] = dict()

    def validate(self, password):
        """
        Check password against the master password.

        password ::: password to check
        """
        valid = True

        try:
            crypto.decrypt(password, self["master"])
        except Exception as exc:
            valid = False

        return valid

    def change_master(self, old_master, new_master):
        """
        Change master password for all accounts.

        old_master ::: old master password
        new_master ::: new master password
        """
        accounts = self.list_accounts()

        for account in accounts:
            id_ = account[0]
            account_dict = self.decrypt_account(old_master, id_)
            new_user = crypto.encrypt(new_master, account_dict["user"])
            new_pass = crypto.encrypt(new_master, account_dict["password"])

            self.change_user(id_, new_user, new_master)
            self.change_pass(id_, new_pass, new_master)

        self["master"] = crypto.encrypt(new_master, new_master)

    def get_account(self, id_):
        """
        Retrieve an (encrypted) account dictionary.

        id_ ::: ID of account to retrieve
        """
        return self["accounts"].get(id_)

    def list_accounts(self):
        """
        List all (encrypted) accounts. Returns an iterable collection of
        account dictionaries. The collection is formatted as a list of tuples:

        [ ... (account_id, {account}) ... ]
        """
        return self["accounts"].items()

    def search(self, re_, search_descriptions=False):
        """
        Find accounts whose labels / descriptions match some regular
        expression.


        We ignore the case of the regular expression so users don't need to
        always search by the proper name of the account (which gets tedious).

        re_ ::: regex search string
        search_descriptions ::: search account descriptions for matches, in
          addition to labels
        """
        accounts = self.list_accounts()
        results = list()

        for account in accounts:
            result = False

            if re.search(re_, account[1]["label"], re.IGNORECASE):
                result = True
            if search_descriptions and (not result) and \
                    re.search(re_, account[1]["description"]):
                result = True

            if result:
                results.append(account[0])

        return results

    def decrypt_account(self, master, id_):
        """
        Retrieve a (decrypted) account dictionary.

        master ::: master password
        id_ ::: ID of account to decrypt and retrieve
        """
        account = self.get_account(id_)

        user = crypto.decrypt(master, account["user"])
        password = crypto.decrypt(master, account["password"])

        plaintext = dict(label=account["label"], user=user, password=password,
                         description=account["description"])

        return plaintext

    def add_account(self, master, label=None, user=None, password=None,
                    description=None):
        """
        Add an account, encrypting the username and password

        master ::: master password
        label ::: account label
        user ::: account username
        password ::: account password
        description ::: account description
        """
        if description is None:
            description = str()

        id_ = str(uuid.uuid4())

        data = dict(label=label, description=description,
                    user=crypto.encrypt(master, user),
                    password=crypto.encrypt(master, password))

        self["accounts"][id_] = data

    def remove_account(self, id_):
        """
        Remove an account.

        id_ ::: ID of account to remove.
        """
        self["accounts"].pop(id_, None)

    def change_description(self, id_, description):
        """
        Change an account description.

        id_ ::: ID of account to modify
        description ::: new account description
        """
        account = self.get_account(id_)
        account["description"] = description

    def change_label(self, id_, label):
        """
        Change an account label.

        id_ ::: ID of account to modify
        label ::: new account label
        """
        account = self.get_account(id_)
        account["label"] = label

    def change_password(self, id_, master, password):
        """
        Change an account password.

        master ::: master password
        id_ ::: ID of account to modify
        password ::: new account password
        """
        account = self.get_account(id_)
        account["password"] = crypto.encrypt(master, password)

    def change_user(self, id_, master, user):
        """
        Change an account username

        master ::: master password
        id_ ::: ID of account to modify
        user ::: new username
        """
        account = self.get_account(id_)
        account["user"] = crypto.encrypt(master, user)
