# -*- coding: utf-8 -*-

""" License

    Copyright (C) 2013 YunoHost

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program; if not, see http://www.gnu.org/licenses

"""

""" yunohost_hook.py

    Manage hooks
"""
import os
import sys
import re
import json
import errno
import subprocess
from shlex import split as arg_split

from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger

hook_folder = '/usr/share/yunohost/hooks/'
custom_hook_folder = '/etc/yunohost/hooks.d/'

logger = getActionLogger('yunohost.hook')


def hook_add(app, file):
    """
    Store hook script to filsystem

    Keyword argument:
        app -- App to link with
        file -- Script to add (/path/priority-file)

    """
    path, filename = os.path.split(file)
    priority, action = _extract_filename_parts(filename)

    try: os.listdir(custom_hook_folder + action)
    except OSError: os.makedirs(custom_hook_folder + action)

    finalpath = custom_hook_folder + action +'/'+ priority +'-'+ app
    os.system('cp %s %s' % (file, finalpath))
    os.system('chown -hR admin: %s' % hook_folder)

    return { 'hook': finalpath }


def hook_remove(app):
    """
    Remove hooks linked to a specific app

    Keyword argument:
        app -- Scripts related to app will be removed

    """
    try:
        for action in os.listdir(custom_hook_folder):
            for script in os.listdir(custom_hook_folder + action):
                if script.endswith(app):
                    os.remove(custom_hook_folder + action +'/'+ script)
    except OSError: pass


def hook_list(action, list_by='name', show_info=False):
    """
    List available hooks for an action

    Keyword argument:
        action -- Action name
        list_by -- Property to list hook by
        show_info -- Show hook information

    """
    result = {}

    # Process the property to list hook by
    if list_by == 'priority':
        if show_info:
            def _append_hook(d, priority, name, path):
                # Use the priority as key and a dict of hooks names
                # with their info as value
                value = { 'path': path }
                try:
                    d[priority][name] = value
                except KeyError:
                    d[priority] = { name: value }
        else:
            def _append_hook(d, priority, name, path):
                # Use the priority as key and the name as value
                try:
                    d[priority].add(name)
                except KeyError:
                    d[priority] = set([name])
    elif list_by == 'name' or list_by == 'folder':
        if show_info:
            def _append_hook(d, priority, name, path):
                # Use the name as key and a list of hooks info - the
                # executed ones with this name - as value
                l = d.get(name, list())
                for h in l:
                    # Only one priority for the hook is accepted
                    if h['priority'] == priority:
                        # Custom hooks overwrite system ones and they
                        # are appended at the end - so overwite it
                        if h['path'] != path:
                            h['path'] = path
                        return
                l.append({ 'priority': priority, 'path': path })
                d[name] = l
        else:
            if list_by == 'name':
                result = set()
            def _append_hook(d, priority, name, path):
                # Add only the name
                d.add(name)
    else:
        raise MoulinetteError(errno.EINVAL, m18n.n('hook_list_by_invalid'))

    def _append_folder(d, folder):
        # Iterate over and add hook from a folder
        for f in os.listdir(folder + action):
            path = '%s%s/%s' % (folder, action, f)
            priority, name = _extract_filename_parts(f)
            _append_hook(d, priority, name, path)

    try:
        # Append system hooks first
        if list_by == 'folder':
            result['system'] = dict() if show_info else set()
            _append_folder(result['system'], hook_folder)
        else:
            _append_folder(result, hook_folder)
    except OSError:
        logger.debug("system hook folder not found for action '%s' in %s",
                     action, hook_folder)

    try:
        # Append custom hooks
        if list_by == 'folder':
            result['custom'] = dict() if show_info else set()
            _append_folder(result['custom'], custom_hook_folder)
        else:
            _append_folder(result, custom_hook_folder)
    except OSError:
        logger.debug("custom hook folder not found for action '%s' in %s",
                     action, custom_hook_folder)

    return { 'hooks': result }


def hook_callback(action, hooks=[], args=None):
    """
    Execute all scripts binded to an action

    Keyword argument:
        action -- Action name
        hooks -- List of hooks names to execute
        args -- Ordered list of arguments to pass to the script

    """
    result = { 'succeed': list(), 'failed': list() }
    hooks_dict = {}

    # Retrieve hooks
    if not hooks:
        hooks_dict = hook_list(action, list_by='priority',
                               show_info=True)['hooks']
    else:
        hooks_names = hook_list(action, list_by='name',
                                show_info=True)['hooks']
        # Iterate over given hooks names list
        for n in hooks:
            try:
                hl = hooks_names[n]
            except KeyError:
                raise MoulinetteError(errno.EINVAL,
                                      m18n.n('hook_name_unknown', n))
            # Iterate over hooks with this name
            for h in hl:
                # Update hooks dict
                d = hooks_dict.get(h['priority'], dict())
                d.update({ n: { 'path': h['path'] }})
                hooks_dict[h['priority']] = d
    if not hooks_dict:
        return result

    # Format arguments
    if args is None:
        args = []
    elif not isinstance(args, list):
        args = [args]

    # Iterate over hooks and execute them
    for priority in sorted(hooks_dict):
        for name, info in iter(hooks_dict[priority].items()):
            filename = '%s-%s' % (priority, name)
            try:
                hook_exec(info['path'], args=args)
            except:
                logger.exception("error while executing hook '%s'",
                                 info['path'])
                result['failed'].append(filename)
            else:
                result['succeed'].append(filename)
    return result


def hook_check(file):
    """
    Parse the script file and get arguments

    Keyword argument:
        file -- File to check

    """
    try:
        with open(file[:file.index('scripts/')] + 'manifest.json') as f:
            manifest = json.loads(str(f.read()))
    except:
        raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid'))

    action = file[file.index('scripts/') + 8:]
    if 'arguments' in manifest and action in manifest['arguments']:
        return manifest['arguments'][action]
    else:
        return {}


def hook_exec(file, args=None):
    """
    Execute hook from a file with arguments

    Keyword argument:
        file -- Script to execute
        args -- Arguments to pass to the script

    """
    from moulinette.helpers import NonBlockingStreamReader
    from yunohost.app import _value_for_locale

    if isinstance(args, list):
        arg_list = args
    else:
        required_args = hook_check(file)
        if args is None:
            args = {}

        arg_list = []
        for arg in required_args:
            if arg['name'] in args:
                if 'choices' in arg and args[arg['name']] not in arg['choices']:
                    raise MoulinetteError(errno.EINVAL,
                        m18n.n('hook_choice_invalid', args[arg['name']]))
                arg_list.append(args[arg['name']])
            else:
                if os.isatty(1) and 'ask' in arg:
                    # Retrieve proper ask string
                    ask_string = _value_for_locale(arg['ask'])

                    # Append extra strings
                    if 'choices' in arg:
                        ask_string += ' ({:s})'.format('|'.join(arg['choices']))
                    if 'default' in arg:
                        ask_string += ' (default: {:s})'.format(arg['default'])

                    input_string = msignals.prompt(ask_string)

                    if input_string == '' and 'default' in arg:
                        input_string = arg['default']

                    arg_list.append(input_string)
                elif 'default' in arg:
                    arg_list.append(arg['default'])
                else:
                    raise MoulinetteError(errno.EINVAL,
                        m18n.n('hook_argument_missing', arg['name']))

    file_path = "./"
    if "/" in file and file[0:2] != file_path:
        file_path = os.path.dirname(file)
        file = file.replace(file_path +"/", "")

    #TODO: Allow python script

    arg_str = ''
    if arg_list:
        # Concatenate arguments and escape them with double quotes to prevent
        # bash related issue if an argument is empty and is not the last
        arg_str = '\\"{:s}\\"'.format('\\" \\"'.join(arg_list))

    msignals.display(m18n.n('executing_script'))

    p = subprocess.Popen(
            arg_split('su - admin -c "cd \\"{:s}\\" && ' \
                '/bin/bash -x \\"{:s}\\" {:s}"'.format(
                    file_path, file, arg_str)),
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            shell=False)

    # Wrap and get process ouput
    stream = NonBlockingStreamReader(p.stdout)
    while True:
        line = stream.readline(True, 0.1)
        if not line:
            # Check if process has terminated
            returncode = p.poll()
            if returncode is not None:
                break
        else:
            msignals.display(line.rstrip(), 'log')
    stream.close()

    return returncode


def _extract_filename_parts(filename):
    """Extract hook parts from filename"""
    if '-' in filename:
        priority, action = filename.split('-', 1)
    else:
        priority = '50'
        action = filename
    return priority, action