import platform
import subprocess
import re
import binascii
import functools

from ..backend import KeyringBackend
from ..errors import PasswordSetError
from ..errors import PasswordDeleteError
from ..util import properties
from ..py27compat import unicode_str


class SecurityCommand(unicode_str):
    """
    A string suitable for passing as the 'command' parameter to the
    OS X 'security' command.
    """
    def __new__(cls, cmd, store='generic'):
        cmd = '%(cmd)s-%(store)s-password' % vars()
        return super(SecurityCommand, cls).__new__(cls, cmd)


class Keyring(KeyringBackend):
    """Mac OS X Keychain"""

    # regex for extracting password from security call
    password_regex = re.compile("""password:\s*(?:0x(?P<hex>[0-9A-F]+)\s*)?"""
                                """(?:"(?P<pw>.*)")?""")
    store = 'generic'

    keychain = None
    "Pathname to keychain filename, overriding default keychain."

    @properties.ClassProperty
    @classmethod
    def priority(cls):
        """
        Preferred for all OS X environments.
        """
        if platform.system() != 'Darwin':
            raise RuntimeError("OS X required")
        return 5

    def set_password(self, service, username, password):
        if username is None:
            username = ''
        try:
            # This two-step process is a stop-gap measure until a ctypes
            # implementation can be created. Fall back to the
            # command-line version when the username/service/password
            # strings contain characters (escapes, newlines, etc.) that
            # the interactive security session can't handle.
            interactive_call = functools.partial(self._interactive_set,
                service, username, password)
            direct_call = functools.partial(self._direct_set,
                service, username, password)
            code = interactive_call() and direct_call()
            # check return code
            if code:
                raise Exception()
        except Exception:
            raise PasswordSetError("Can't store password in keychain")

    def _interactive_set(self, service, username, password):
        """
        Call the security command, supplying parameters through
        the input stream to avoid revealing the password in the
        process status.
        """
        cmd = [
            'security',
            '-i'
        ]
        security_cmd = "{} -a '{}' -s '{}' -p '{}' -U\n".format(
            SecurityCommand('add', self.store),
            username, service, password)
        call = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE)
        stdoutdata, stderrdata = call.communicate(
            security_cmd.encode('utf-8'))
        return call.returncode

    def _direct_set(self, service, username, password):
        """
        Call the security command, supplying the parameters on
        the command line.
        """
        cmd = [
            'security',
            SecurityCommand('add', self.store),
            '-a', username,
            '-s', service,
            '-w', password,
            '-U',
        ]
        call = subprocess.Popen(
            cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        stdoutdata, stderrdata = call.communicate()
        return call.returncode

    def get_password(self, service, username):
        if username is None:
            username = ''
        try:
            # set up the call to security.
            cmd = [
                'security',
                SecurityCommand('find', self.store),
                '-g',
                '-a', username,
                '-s', service,
            ]
            if self.keychain:
                cmd.append(self.keychain)
            call = subprocess.Popen(
                cmd,
                stderr=subprocess.PIPE,
                stdout=subprocess.PIPE)
            stdoutdata, stderrdata = call.communicate()
            code = call.returncode
            if code != 0:
                raise OSError("Can't fetch password from system")
            output = stderrdata.decode()
            # check for empty password.
            if output == 'password: \n':
                return ''
            # search for special password pattern.
            matches = Keyring.password_regex.search(output)
            if matches:
                group_dict = matches.groupdict()
                hex = group_dict.get('hex')
                pw = group_dict.get('pw')
                if hex:
                    # it's a weird hex password, decode it.
                    return unicode_str(binascii.unhexlify(hex), 'utf-8')
                else:
                    # it's a normal password, send it back.
                    return pw
            # nothing was found, it doesn't exist.
        except:
            pass

    def delete_password(self, service, username):
        del_error = PasswordDeleteError("Can't delete password in keychain")
        if username is None:
            username = ''
        try:
            cmd = [
                'security',
                SecurityCommand('delete', self.store),
                '-a', username,
                '-s', service,
            ]
            # set up the call for security.
            call = subprocess.Popen(
                cmd,
                stderr=subprocess.PIPE,
                stdout=subprocess.PIPE)
            stdoutdata, stderrdata = call.communicate()
            code = call.returncode
            # check return code.
            if code != 0:
                raise del_error
        except:
            raise del_error
