"""UIs for PIN/passphrase entry."""

import logging
import os
import subprocess

from .. import util

log = logging.getLogger(__name__)


class UI:
    """UI for PIN/passphrase entry (for SafeWISE devices)."""

    def __init__(self, device_type, config=None):
        """C-tor."""
        default_pinentry = 'pinentry'  # by default, use GnuPG pinentry tool
        if config is None:
            config = {}
        self.pin_entry_binary = config.get('pin_entry_binary',
                                           default_pinentry)
        self.passphrase_entry_binary = config.get('passphrase_entry_binary',
                                                  default_pinentry)
        self.options_getter = create_default_options_getter()
        self.device_name = device_type.__name__

    def get_pin(self, name=None):
        """Ask the user for (scrambled) PIN."""
        description = (
            'Use the numeric keypad to describe number positions.\n'
            'The layout is:\n'
            '    7 8 9\n'
            '    4 5 6\n'
            '    1 2 3')
        return interact(
            title='{} PIN'.format(name or self.device_name),
            prompt='PIN:',
            description=description,
            binary=self.pin_entry_binary,
            options=self.options_getter())

    def get_passphrase(self, name=None):
        """Ask the user for passphrase."""
        return interact(
            title='{} passphrase'.format(name or self.device_name),
            prompt='Passphrase:',
            description=None,
            binary=self.passphrase_entry_binary,
            options=self.options_getter())


def create_default_options_getter():
    """Return current TTY and DISPLAY settings for GnuPG pinentry."""
    options = []
    try:
        ttyname = subprocess.check_output(args=['tty']).strip()
        options.append(b'ttyname=' + ttyname)
    except subprocess.CalledProcessError as e:
        log.warning('no TTY found: %s', e)

    display = os.environ.get('DISPLAY')
    if display is not None:
        options.append('display={}'.format(display).encode('ascii'))
    else:
        log.warning('DISPLAY not defined')

    log.info('using %s for pinentry options', options)
    return lambda: options


def write(p, line):
    """Send and flush a single line to the subprocess' stdin."""
    log.debug('%s <- %r', p.args, line)
    p.stdin.write(line)
    p.stdin.flush()


class UnexpectedError(Exception):
    """Unexpected response."""


def expect(p, prefixes, confidential=False):
    """Read a line and return it without required prefix."""
    resp = p.stdout.readline()
    log.debug('%s -> %r', p.args, resp if not confidential else '********')
    for prefix in prefixes:
        if resp.startswith(prefix):
            return resp[len(prefix):]
    raise UnexpectedError(resp)


def interact(title, description, prompt, binary, options):
    """Use GPG pinentry program to interact with the user."""
    args = [binary]
    p = subprocess.Popen(args=args,
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         env=os.environ)
    p.args = args  # TODO: remove after Python 2 deprecation.
    expect(p, [b'OK'])

    title = util.assuan_serialize(title.encode('ascii'))
    write(p, b'SETTITLE ' + title + b'\n')
    expect(p, [b'OK'])

    if description:
        description = util.assuan_serialize(description.encode('ascii'))
        write(p, b'SETDESC ' + description + b'\n')
        expect(p, [b'OK'])

    if prompt:
        prompt = util.assuan_serialize(prompt.encode('ascii'))
        write(p, b'SETPROMPT ' + prompt + b'\n')
        expect(p, [b'OK'])

    log.debug('setting %d options', len(options))
    for opt in options:
        write(p, b'OPTION ' + opt + b'\n')
        expect(p, [b'OK', b'ERR'])

    write(p, b'GETPIN\n')
    pin = expect(p, [b'OK', b'D '], confidential=True)

    p.communicate()  # close stdin and wait for the process to exit
    exit_code = p.wait()
    if exit_code:
        raise subprocess.CalledProcessError(exit_code, binary)

    return pin.decode('ascii').strip()
