from __future__ import with_statement

import os
import sys
import json
import getpass
import configparser

from keyring.util import properties
from .escape import escape as escape_for_ini

from keyrings.alt.file_base import Keyring, decodebytes, encodebytes


class PlaintextKeyring(Keyring):
    """Simple File Keyring with no encryption"""

    priority = 0.5
    "Applicable for all platforms, but not recommended"

    filename = 'keyring_pass.cfg'
    scheme = 'no encyption'
    version = '1.0'

    def encrypt(self, password, assoc=None):
        """Directly return the password itself, ignore associated data.
        """
        return password

    def decrypt(self, password_encrypted, assoc=None):
        """Directly return encrypted password, ignore associated data.
        """
        return password_encrypted


class Encrypted(object):
    """
    PyCrypto-backed Encryption support
    """

    scheme = '[PBKDF2] AES256.CFB'
    version = '1.0'
    block_size = 32

    def _create_cipher(self, password, salt, IV):
        """
        Create the cipher object to encrypt or decrypt a payload.
        """
        from Crypto.Protocol.KDF import PBKDF2
        from Crypto.Cipher import AES

        pw = PBKDF2(password, salt, dkLen=self.block_size)
        return AES.new(pw[: self.block_size], AES.MODE_CFB, IV)

    def _get_new_password(self):
        while True:
            password = getpass.getpass("Please set a password for your new keyring: ")
            confirm = getpass.getpass('Please confirm the password: ')
            if password != confirm:  # pragma: no cover
                sys.stderr.write("Error: Your passwords didn't match\n")
                continue
            if '' == password.strip():  # pragma: no cover
                # forbid the blank password
                sys.stderr.write("Error: blank passwords aren't allowed.\n")
                continue
            return password


class EncryptedKeyring(Encrypted, Keyring):
    """PyCrypto File Keyring"""

    filename = 'crypted_pass.cfg'
    pw_prefix = 'pw:'.encode()

    @properties.ClassProperty
    @classmethod
    def priority(self):
        "Applicable for all platforms, but not recommended."
        try:
            __import__('Crypto.Cipher.AES')
            __import__('Crypto.Protocol.KDF')
            __import__('Crypto.Random')
        except ImportError:  # pragma: no cover
            raise RuntimeError("PyCrypto required")
        if not json:  # pragma: no cover
            raise RuntimeError("JSON implementation such as simplejson required.")
        return 0.6

    @properties.NonDataProperty
    def keyring_key(self):
        # _unlock or _init_file will set the key or raise an exception
        if self._check_file():
            self._unlock()
        else:
            self._init_file()
        return self.keyring_key

    def _init_file(self):
        """
        Initialize a new password file and set the reference password.
        """
        self.keyring_key = self._get_new_password()
        # set a reference password, used to check that the password provided
        #  matches for subsequent checks.
        self.set_password(
            'keyring-setting', 'password reference', 'password reference value'
        )
        self._write_config_value('keyring-setting', 'scheme', self.scheme)
        self._write_config_value('keyring-setting', 'version', self.version)

    def _check_file(self):
        """
        Check if the file exists and has the expected password reference.
        """
        if not os.path.exists(self.file_path):
            return False
        self._migrate()
        config = configparser.RawConfigParser()
        config.read(self.file_path)
        try:
            config.get(
                escape_for_ini('keyring-setting'), escape_for_ini('password reference')
            )
        except (configparser.NoSectionError, configparser.NoOptionError):
            return False
        try:
            self._check_scheme(config)
        except AttributeError:
            # accept a missing scheme
            return True
        return self._check_version(config)

    def _check_scheme(self, config):
        """
        check for a valid scheme

        raise ValueError otherwise
        raise AttributeError if missing
        """
        try:
            scheme = config.get(
                escape_for_ini('keyring-setting'), escape_for_ini('scheme')
            )
        except (configparser.NoSectionError, configparser.NoOptionError):
            raise AttributeError("Encryption scheme missing")

        # remove pointless crypto module name
        if scheme.startswith('PyCrypto '):
            scheme = scheme[9:]

        if scheme != self.scheme:
            raise ValueError(
                "Encryption scheme mismatch "
                "(exp.: %s, found: %s)" % (self.scheme, scheme)
            )

    def _check_version(self, config):
        """
        check for a valid version
        an existing scheme implies an existing version as well

        return True, if version is valid, and False otherwise
        """
        try:
            self.file_version = config.get(
                escape_for_ini('keyring-setting'), escape_for_ini('version')
            )
        except (configparser.NoSectionError, configparser.NoOptionError):
            return False
        return True

    def _unlock(self):
        """
        Unlock this keyring by getting the password for the keyring from the
        user.
        """
        self.keyring_key = getpass.getpass(
            'Please enter password for encrypted keyring: '
        )
        try:
            ref_pw = self.get_password('keyring-setting', 'password reference')
            assert ref_pw == 'password reference value'
        except AssertionError:
            self._lock()
            raise ValueError("Incorrect Password")

    def _lock(self):
        """
        Remove the keyring key from this instance.
        """
        del self.keyring_key

    def encrypt(self, password, assoc=None):
        # encrypt password, ignore associated data
        from Crypto.Random import get_random_bytes

        salt = get_random_bytes(self.block_size)
        from Crypto.Cipher import AES

        IV = get_random_bytes(AES.block_size)
        cipher = self._create_cipher(self.keyring_key, salt, IV)
        password_encrypted = cipher.encrypt(self.pw_prefix + password)
        # Serialize the salt, IV, and encrypted password in a secure format
        data = dict(salt=salt, IV=IV, password_encrypted=password_encrypted)
        for key in data:
            # spare a few bytes: throw away newline from base64 encoding
            data[key] = encodebytes(data[key]).decode()[:-1]
        return json.dumps(data).encode()

    def decrypt(self, password_encrypted, assoc=None):
        # unpack the encrypted payload, ignore associated data
        data = json.loads(password_encrypted.decode())
        for key in data:
            data[key] = decodebytes(data[key].encode())
        cipher = self._create_cipher(self.keyring_key, data['salt'], data['IV'])
        plaintext = cipher.decrypt(data['password_encrypted'])
        assert plaintext.startswith(self.pw_prefix)
        return plaintext[3:]

    def _migrate(self, keyring_password=None):
        """
        Convert older keyrings to the current format.
        """