import splogger as log
from subprocess import Popen, PIPE, CalledProcessError
import sys
import os
import shutil
import pytest
import requests
from colorama import Fore
import hashlib
from os.path import join
import fnmatch
import gnupg
import json
import getpass
from .config import NoFailReadOnlyDict
from datetime import datetime

from . import config

FNULL = open(os.devnull, 'w')


def call_with_stdout(args, ignore_err=False,
                     stdout=PIPE, inp=None, stderr=PIPE):
    with Popen(args.split(' ') if type(args) == str else args, stdout=stdout, stdin=PIPE if inp is not None else None, stderr=stderr) as proc:
        out, err = proc.communicate(input=inp)
        if proc.poll() != 0 and not ignore_err:
            if log.get_verbose():
                log.error('Error from subprocess')
                if err is not None and err != '':
                    print('err: ' + str(err), file=sys.stderr)
                if out is not None and out != '':
                    print('out: ' + str(out), file=sys.stderr)
            raise CalledProcessError(proc.poll(), args, out, err)

        if log.get_verbose():
            log.debug('Output of '+repr(args))
            if out is not None:
                print(out.decode())
            if err is not None:
                print(err.decode())

        if out is not None:
            return out.decode()
@log.clear()
def read_logins():
    if os.path.isfile('.logins'):
        log.success(Fore.GREEN+config.PADLOCK+" Found crypted logins file"+Fore.RESET)
        gpg = gnupg.GPG()

        cr = None
        with open('.logins', 'r') as fh:
            cr = fh.read()
        
        crypt = None
        while True:
            passphrase = getpass.getpass('Passphrase for login file: ')
            crypt = gpg.decrypt(cr, passphrase = passphrase)

            if not crypt.ok:
                log.error(Fore.RED + config.PADLOCK + 'Could not unlock the logins file, is the passphrase correct?'+Fore.RESET)
                # raise ValueError('Could not uncrypt the login file')
            else:
                break

        cr = json.loads(crypt.data)
        log.success('Logins creation time: '+cr['creation_date'])
        log.success(Fore.GREEN+config.OPEN_PADLOCK+' Got logins for '+', '.join(cr['data'])+Fore.RESET)
        
        return NoFailReadOnlyDict(cr['data'], default = None)
    return None

def get_date(): 
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def ask_logins():
    if os.path.isfile('.logins'):
        log.warning(Fore.YELLOW+'The logins file already exist, overwrite it ?'+Fore.RESET)
        yes = input('Enter \'yes\' to overwrite: ')
        if yes != 'yes':
            return

    cr = {
        'creation_date': get_date(),
        'data': {}
    }

    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    print(Fore.YELLOW+'\tLogins configuration\n'+Fore.RESET)
    print(Fore.GREEN+'You are going to be asked you credentials for this project\n'
          +'They will be stored under .logins crypted with AES and .logins append to .gitignore'+Fore.RESET)
          
    yes = input('Do you wish to continue? (Enter "yes" to continue): ')
    if yes != 'yes':
        log.error('Cancelled')
        return
    print('')

    def ask_login(arr, component):
        print(Fore.LIGHTGREEN_EX+'\nNow configuring login for: '+Fore.GREEN+component)
        print(Fore.RED+"Leave blank if Not Applicable"+Fore.RESET)
        login = input('Login: ')
        if login == '':
            return
        password = getpass.getpass()
        arr[component] = {'login': login, 'password': password}
    
    
    # print(Fore.RED+'Git login: '+Fore.CYAN+' Git login will no be asked, you are expected to use a credential helper for git '
    # +'if you wish to automatically push'+Fore.RESET)
    # print(Fore.GREEN+'This setup can launch the git credential helper for you <3'
    # +'\n'+Fore.YELLOW+'WARNING: The credentials are stored in clear text'+Fore.RESET)
    # yes = input('Save git credentials? (Enter "yes" to continue): ')
    # if yes == 'yes':
    #     call_git('config credential.helper store')
    #     call_git('push')

    ask_login(cr['data'], 'git')
    ask_login(cr['data'], 'pypi')
    ask_login(cr['data'], 'docker')

    gpg = gnupg.GPG()
    gpg.encrypt(json.dumps(cr), (), symmetric=True, output='.logins')

    try:
        call_git('check-ignore .logins')
    except CalledProcessError:
        with open('.gitignore', 'a') as fh:
            fh.write('\.logins')
        log.success('Appened .logins to .gitignore')

def call_python(module, args, stdout=None, stderr=None):
    mod = [] if module == '' else ['-m', module]
    return call_with_stdout(
        [sys.executable, *mod, *args.split(' ')], stdout=stdout, stderr=stderr)


def copy(a, b):
    assert os.path.isfile(a)
    with open(a, 'r') as ffh:
        with open(b, 'w+') as tfh:
            tfh.write(ffh.read())


def query_get(url, make_json=True):
    req = requests.get(url)
    if not req.ok:
        raise ValueError('Request Failed with code: ' + str(req.status_code))
    if make_json:
        return req.json()
    return req.content


def install_packages(args, check_signatures=None):
    if check_signatures is None:
        check_signatures = config.config['signed']

    if not check_signatures:
        call_pip('install ' + args, verbose=True)
        return

    piptmp = 'piptmp'

    @log.element('Download Packages', log_entry=True)
    def download():
        for pack in args.split(' '):
            log.set_additional_info(pack)
            call_pip('download -d ' + piptmp + ' ' + pack)

    @log.element('Checking Packages')
    def check_packages(base_url='https://pypi.python.org/pypi/'):
        log.fine('Checking packages in: ' + piptmp)
        unchecked = 0
        for f in os.listdir(piptmp):
            try:
                log.set_additional_info(f)
                f_ = piptmp + os.sep + f
                if not os.path.isfile(f_):
                    continue

                splited = f.split('-')
                log.debug('Checking ' + splited[0])
                package_info = query_get(base_url + splited[0] + '/' + splited[1] + '/json')

                for f_info in package_info['releases'][splited[1]]:
                    if not os.path.isfile(os.path.join(piptmp, f_info['filename'])):
                        continue

                    if md5(f_) != f_info['md5_digest']:
                        log.error('Hash do not match')
                        exit(1)
                    # log.success(Fore.GREEN+'Hash checked for '+f)

                    if not f_info['has_sig']:
                        log.debug(Fore.YELLOW + 'No signature provided for ' + f_info['filename'])  # FIXME throw?
                        unchecked += 1
                        continue

                    sig = query_get(f_info['url'] + '.asc', False)
                    log.debug('File: ' + f_info['filename'] + ' has signature:\n ' + sig.decode())

                    # Check
                    q = '' if log.get_verbose() else ' --quiet'
                    try:
                        call_gpg('--no-default-keyring --keyring tmp.gpg' + q + ' --auto-key-retrieve --verify - ' + f_, inp=sig)  # FIXME Only use known keys?
                    except CalledProcessError as er:
                        if er.returncode == 1:
                            log.error(Fore.RED + config.OPEN_PADLOCK + ' Invalid signature for ' + f)
                            exit(1)

                        log.error('Could not check signature for ' + f + ' (' + repr(er) + ')')
                        unchecked += 1
                        continue

                    log.success(Fore.GREEN + config.PADLOCK + ' File ' + f + ' is verified')

            except KeyboardInterrupt:
                exit(2)
            except SystemExit as e:
                raise e
            except BaseException as be:
                log.error(Fore.RED + config.OPEN_PADLOCK + ' Failed to check ' + f + Fore.RESET)
                log.error(repr(be))
        log.warning(Fore.YELLOW + str(unchecked) + ' file(s) could not be verified')

    def clearup():
        shutil.rmtree(piptmp, True)
        log.success('Cleaned temporary download directory')

    @log.element('Install Packages')
    def install():
        for f in os.listdir(piptmp):
            if f.endswith('.whl'):
                log.set_additional_info(f)
                call_pip('install ' + piptmp + os.path.sep + f)
                # log.success('Installed ' + f.split('-')[0])

    clearup()
    download()
    check_packages()
    install()
    clearup()

def match_gitignore(name, ignore):
    for pattern in ignore.split(','):
        if fnmatch.fnmatch(name, pattern):
            return True
    return False

def call_pip(args, verbose=log.get_verbose()):
    fh = PIPE if verbose else FNULL
    return call_python('pip', args, stdout=fh, stderr=fh)


@log.clear()
def call_pytest(args):
    o = pytest.main(args.split(' '))
    if o != 0:
        raise CalledProcessError(o, 'pytest ' + args)


@log.clear()
def call_git(args):
    if type(args) == str:
        args = 'git '+args
    else: # array
        args = ['git', *args]

    return call_with_stdout(args)


@log.element('Commiting', log_entry=True)
def call_commit(message, key=''):
    args = ['git', 'commit', '--no-edit']
    if key != '':
        args.append('-S' + key)
    args.append('-m')
    args.append(message)

    return call_with_stdout(args)


def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()


def call_gpg(args, inp=None, verbose=log.get_verbose()):
    # log.error('Call to gpg deprecated')
    fh = PIPE if verbose else FNULL
    return call_with_stdout('gpg ' + args, inp=inp, stdout=fh, stderr=fh)


def call_twine(args):
    return call_with_stdout('twine ' + args, stdout=None) # FIXME import and use


@log.element('Checking code', log_entry=False)
def call_check(args, ignore="", exclude=''):
    flakes = ''
    for dirname, _, files in os.walk(args):
        if os.path.ismount(dirname) or os.path.islink(dirname):
            log.warning('Ignored mounted/link directory: '+dirname)
            continue

        if match_gitignore(dirname, exclude):
            continue

        for f in files:
            if not f.endswith('.py'):
                continue
            log.debug('Check: '+f)
            log.set_additional_info(f)
            if match_gitignore(join(dirname, f), exclude):
                continue 
            
            flak = call_with_stdout('python -m pyflakes ' + join(dirname, f), ignore_err=True)
            if flak is not None:
                flakes += flak

    # flakes = call_with_stdout('python -m pyflakes ' + args, ignore_err=True)
    pep8 = call_with_stdout(
        'python -m pycodestyle --ignore=' +
        ignore +     
        ' ' +
        ('' if exclude == '' else '--exclude='+exclude) +
        ' ' +
        args,
        ignore_err=True)
    return flakes, pep8


@log.clear()
def input_with_default(prompt, default=None, rtype=str):
    assert type(rtype) == type
    if default is None:
        default = rtype()

    while True:
        res = input(f'{prompt} ({default}): ')
        if res == 'null' or res == 'none':
            return ''
        if res is '':
            return default

        try:
            return rtype(res)
        except ValueError:
            log.error('An input of type ' + rtype.__name__ + " is expected")


__all__ = [
    'input_with_default',
    'call_check',
    'call_git',
    'call_pytest',
    'call_pip',
    'copy',
    'call_python',
    'call_with_stdout']
