from __future__ import unicode_literals, division, absolute_import
from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin

import argparse
import cgi
import copy
from datetime import datetime, timedelta
from json import JSONEncoder

from flask import jsonify, Response, request
from flask_restplus import inputs
from queue import Queue, Empty

from flexget.api import api, APIResource
from flexget.api.app import APIError, NotFoundError, Conflict, BadRequest, success_response, \
    base_message_schema, etag
from flexget.config_schema import process_config
from flexget.entry import Entry
from flexget.event import event
from flexget.options import get_parser
from flexget.task import task_phases
from flexget.utils import json
from flexget.utils import requests
from flexget.utils.lazy_dict import LazyLookup

# Tasks API
tasks_api = api.namespace('tasks', description='Manage Tasks')


class ObjectsContainer(object):
    tasks_list_object = {'oneOf': [
        {'type': 'array',
         'items': {'$ref': '#/definitions/tasks.task'}},
        {'type': 'array', 'items': {'type': 'string'}}
    ]
    }

    task_input_object = {
        'type': 'object',
        'properties': {
            'name': {'type': 'string'},
            'config': {'$ref': '/schema/plugins'}
        },
        'required': ['name', 'config'],
        'additionalProperties': False
    }

    task_return_object = copy.deepcopy(task_input_object)
    task_return_object['properties']['config'] = {'type': 'object'}

    task_queue_schema = {
        'type': 'array',
        'items': {
            'type': 'object',
            'properties': {
                'id': {'type': 'integer'},
                'name': {'type': 'string'},
                'current_phase': {'type': ['string', 'null']},
                'current_plugin': {'type': ['string', 'null']},
            }}
    }

    task_execution_results_schema = {
        'type': 'object',
        'properties': {
            'task': {
                'type': 'object',
                'properties': {
                    'id': {'type': 'integer'},
                    'name': {'type': 'string'},
                    'stream': {
                        'type': 'array',
                        'items': {
                            'progress': {
                                'type': 'object',
                                'properties': {
                                    'status': {'type': 'string', 'enum': ['pending', 'running', 'complete']},
                                    'phase': {'type': 'string', 'enum': task_phases},
                                    'plugin': {'type': 'string'},
                                    'percent': {'type': 'float'}
                                }
                            },
                            'summary': {
                                'type': 'object',
                                'properties': {
                                    'accepted': {'type': 'integer'},
                                    'rejected': {'type': 'integer'},
                                    'failed': {'type': 'integer'},
                                    'undecided': {'type': 'integer'},
                                    'aborted': {'type': 'boolean'},
                                    'abort_reason': {'type': 'string'},
                                }
                            },
                            'entry_dump': {'type': 'array', 'items': {'type': 'object'}},
                            'log': {'type': 'string'}
                        }
                    }
                }
            }
        }
    }

    inject_input = {
        'type': 'object',
        'properties': {
            'title': {'type': 'string',
                      'description': 'Title of the entry. If not supplied it will be attempted to retrieve it from '
                                     'URL headers'},
            'url': {'type': 'string',
                    'format': 'url',
                    'description': 'URL of the entry'},
            'force': {'type': 'boolean',
                      'description': 'Prevent any plugins from rejecting this entry'},
            'accept': {'type': 'boolean',
                       'description': 'Accept this entry immediately upon injection (disregard task filters)'},
            'fields': {'type': 'object',
                       'description': 'A array of objects that can contain any other value for the entry'}
        },
        'required': ['url']
    }

    task_execution_input = {
        'type': 'object',
        'properties': {
            'tasks': {'type': 'array',
                      'items': {'type': 'string'},
                      'minItems': 1,
                      'uniqueItems': True},
            'progress': {
                'type': 'boolean',
                'default': True,
                'description': 'Include task progress updates'},
            'summary': {
                'type': 'boolean',
                'default': True,
                'description': 'Include task summary'},
            'entry_dump': {
                'type': 'boolean',
                'default': True,
                'description': 'Include dump of entries including fields'},
            'inject': {'type': 'array',
                       'items': inject_input,
                       'description': 'A List of entry objects'},
            'loglevel': {'type': 'string',
                         'description': 'Specify log level',
                         'enum': ['critical', 'error', 'warning', 'info', 'verbose', 'debug', 'trace']}
        },
        'required': ['tasks']

    }

    params_return_schema = {'type': 'array', 'items': {'type': 'object'}}


tasks_list_schema = api.schema('tasks.list', ObjectsContainer.tasks_list_object)
task_input_schema = api.schema('tasks.task', ObjectsContainer.task_input_object)
task_return_schema = api.schema('tasks.task', ObjectsContainer.task_return_object)
task_api_queue_schema = api.schema('task.queue', ObjectsContainer.task_queue_schema)
task_api_execute_schema = api.schema('task.execution', ObjectsContainer.task_execution_results_schema)
task_execution_schema = api.schema('task_execution_input', ObjectsContainer.task_execution_input)
task_execution_params = api.schema('tasks.execution_params', ObjectsContainer.params_return_schema)

task_api_desc = 'Task config schema too large to display, you can view the schema using the schema API'

tasks_parser = api.parser()
tasks_parser.add_argument('include_config', type=inputs.boolean, default=True, help='Include task config')


@tasks_api.route('/')
@api.doc(description=task_api_desc)
class TasksAPI(APIResource):
    @etag
    @api.response(200, model=tasks_list_schema)
    @api.doc(parser=tasks_parser)
    def get(self, session=None):
        """ List all tasks """

        args = tasks_parser.parse_args()
        if not args.get('include_config'):
            return jsonify(list(self.manager.user_config.get('tasks', {})))

        tasks = []
        for name, config in self.manager.user_config.get('tasks', {}).items():
            tasks.append({'name': name, 'config': config})
        return jsonify(tasks)

    @api.validate(task_input_schema, description='New task object')
    @api.response(201, description='Newly created task', model=task_return_schema)
    @api.response(Conflict)
    @api.response(APIError)
    def post(self, session=None):
        """ Add new task """
        data = request.json

        task_name = data['name']

        if task_name in self.manager.user_config.get('tasks', {}):
            raise Conflict('task already exists')

        if 'tasks' not in self.manager.user_config:
            self.manager.user_config['tasks'] = {}
        if 'tasks' not in self.manager.config:
            self.manager.config['tasks'] = {}

        task_schema_processed = copy.deepcopy(data)
        errors = process_config(task_schema_processed, schema=task_input_schema.__schema__, set_defaults=True)

        if errors:
            raise APIError('problem loading config, raise a BUG as this should not happen!')

        self.manager.user_config['tasks'][task_name] = data['config']
        self.manager.config['tasks'][task_name] = task_schema_processed['config']

        self.manager.save_config()
        self.manager.config_changed()
        rsp = jsonify({'name': task_name, 'config': self.manager.user_config['tasks'][task_name]})
        rsp.status_code = 201
        return rsp


@tasks_api.route('/<task>/')
@api.doc(params={'task': 'task name'}, description=task_api_desc)
@api.response(APIError, description='unable to read config')
class TaskAPI(APIResource):
    @etag
    @api.response(200, model=task_return_schema)
    @api.response(NotFoundError, description='task not found')
    def get(self, task, session=None):
        """ Get task config """
        if task not in self.manager.user_config.get('tasks', {}):
            raise NotFoundError('task `%s` not found' % task)

        return jsonify({'name': task, 'config': self.manager.user_config['tasks'][task]})

    @api.validate(task_input_schema)
    @api.response(200, model=task_return_schema)
    @api.response(NotFoundError)
    @api.response(BadRequest)
    def put(self, task, session=None):
        """ Update tasks config """
        data = request.json

        new_task_name = data['name']

        if task not in self.manager.user_config.get('tasks', {}):
            raise NotFoundError('task `%s` not found' % task)

        if 'tasks' not in self.manager.user_config:
            self.manager.user_config['tasks'] = {}
        if 'tasks' not in self.manager.config:
            self.manager.config['tasks'] = {}

        if task != new_task_name:
            # Rename task
            if new_task_name in self.manager.user_config['tasks']:
                raise BadRequest('cannot rename task as it already exist')

            del self.manager.user_config['tasks'][task]
            del self.manager.config['tasks'][task]

        # Process the task config
        task_schema_processed = copy.deepcopy(data)
        errors = process_config(task_schema_processed, schema=task_return_schema.__schema__, set_defaults=True)

        if errors:
            raise APIError('problem loading config, raise a BUG as this should not happen!')

        self.manager.user_config['tasks'][new_task_name] = data['config']
        self.manager.config['tasks'][new_task_name] = task_schema_processed['config']

        self.manager.save_config()
        self.manager.config_changed()

        rsp = jsonify({'name': new_task_name, 'config': self.manager.user_config['tasks'][new_task_name]})
        rsp.status_code = 200
        return rsp

    @api.response(200, model=base_message_schema, description='deleted task')
    @api.response(NotFoundError)
    def delete(self, task, session=None):
        """ Delete a task """
        try:
            self.manager.config['tasks'].pop(task)
            self.manager.user_config['tasks'].pop(task)
        except KeyError:
            raise NotFoundError('task does not exist')

        self.manager.save_config()
        self.manager.config_changed()
        return success_response('successfully deleted task')


default_start_date = (datetime.now() - timedelta(weeks=1)).strftime('%Y-%m-%d')

status_parser = api.parser()
status_parser.add_argument('succeeded', type=inputs.boolean, default=True, help='Filter by success status')
status_parser.add_argument('produced', type=inputs.boolean, default=True, store_missing=False,
                           help='Filter by tasks that produced entries')
status_parser.add_argument('start_date', type=inputs.datetime_from_iso8601, default=default_start_date,
                           help='Filter by minimal start date. Example: \'2012-01-01\'')
status_parser.add_argument('end_date', type=inputs.datetime_from_iso8601,
                           help='Filter by maximal end date. Example: \'2012-01-01\'')
status_parser.add_argument('limit', default=100, type=int,
                           help='Limit return of executions per task, as that number can be huge')


def _task_info_dict(task):
    return {
        'id': int(task.id),
        'name': task.name,
        'current_phase': task.current_phase,
        'current_plugin': task.current_plugin,
    }


@tasks_api.route('/queue/')
class TaskQueueAPI(APIResource):
    @api.response(200, model=task_api_queue_schema)
    def get(self, session=None):
        """ List task(s) in queue for execution """
        tasks = [_task_info_dict(task) for task in self.manager.task_queue.run_queue.queue]

        if self.manager.task_queue.current_task:
            tasks.insert(0, _task_info_dict(self.manager.task_queue.current_task))

        return jsonify(tasks)


class ExecuteLog(Queue):
    """ Supports task log streaming by acting like a file object """

    def write(self, s):
        self.put(json.dumps({'log': s}))


_streams = {}

# Another namespace for the same endpoint
inject_api = api.namespace('inject', description='Entry injection API')


@inject_api.route('/params/')
@tasks_api.route('/execute/params/')
@api.doc(description='Available payload parameters for task execute')
class TaskExecutionParams(APIResource):
    @etag
    @api.response(200, model=task_execution_params)
    def get(self, session=None):
        """ Execute payload parameters """
        return jsonify(ObjectsContainer.task_execution_input)


@inject_api.route('/')
@tasks_api.route('/execute/')
@api.doc(description='For details on available parameters query /params/ endpoint')
class TaskExecutionAPI(APIResource):
    @api.response(NotFoundError)
    @api.response(BadRequest)
    @api.response(200, model=task_api_execute_schema)
    @api.validate(task_execution_schema)
    def post(self, session=None):
        """ Execute task and stream results """
        data = request.json
        for task in data.get('tasks'):
            if task.lower() not in [t.lower() for t in self.manager.user_config.get('tasks', {}).keys()]:
                raise NotFoundError('task %s does not exist' % task)

        queue = ExecuteLog()
        output = queue if data.get('loglevel') else None
        stream = True if any(
            arg[0] in ['progress', 'summary', 'loglevel', 'entry_dump'] for arg in data.items() if arg[1]) else False
        loglevel = data.pop('loglevel', None)

        # This emulates the CLI command of using `--now` and `no-cache`
        options = {'interval_ignore': data.pop('now', None),
                   'nocache': data.pop('no_cache', None),
                   'allow_manual': True}

        for option, value in data.items():
            options[option] = value

        if data.get('inject'):
            entries = []
            for item in data.get('inject'):
                entry = Entry()
                entry['url'] = item['url']
                if not item.get('title'):
                    try:
                        value, params = cgi.parse_header(requests.head(item['url']).headers['Content-Disposition'])
                        entry['title'] = params['filename']
                    except KeyError:
                        raise BadRequest('No title given, and couldn\'t get one from the URL\'s HTTP response')

                else:
                    entry['title'] = item.get('title')
                if item.get('force'):
                    entry['immortal'] = True
                if item.get('accept'):
                    entry.accept(reason='accepted by API inject')
                if item.get('fields'):
                    for key, value in item.get('fields').items():
                        entry[key] = value
                entries.append(entry)
            options['inject'] = entries

        executed_tasks = self.manager.execute(options=options, output=output, loglevel=loglevel)

        tasks_queued = []

        for task_id, task_name, task_event in executed_tasks:
            tasks_queued.append({'id': task_id, 'name': task_name, 'event': task_event})
            _streams[task_id] = {
                'queue': queue,
                'last_update': datetime.now(),
                'args': data
            }

        if not stream:
            return jsonify({'tasks': [{'id': task['id'], 'name': task['name']} for task in tasks_queued]})

        def stream_response():
            # First return the tasks to execute
            yield '{"stream": ['
            yield json.dumps({'tasks': [{'id': task['id'], 'name': task['name']} for task in tasks_queued]}) + ',\n'

            while True:
                try:
                    yield queue.get(timeout=1) + ',\n'
                    continue
                except Empty:
                    pass

                if queue.empty() and all([task['event'].is_set() for task in tasks_queued]):
                    for task in tasks_queued:
                        del _streams[task['id']]
                    break
            yield '{}]}'

        return Response(stream_response(), mimetype='text/event-stream')


@event('manager.daemon.started')
def setup_params(mgr):
    parser = get_parser('execute')

    for action in parser._optionals._actions:
        # Ignore list for irrelevant actions
        ignore = ['help', 'verbose', 'silent', 'try-regexp', 'dump-config', 'dump']

        name = action.option_strings[-1].strip('--')
        if name in ignore or action.help == '==SUPPRESS==':
            continue

        name = name.replace('-', '_')
        property_data = {'description': action.help.capitalize()}
        if isinstance(action, argparse._StoreConstAction):
            property_data['type'] = 'boolean'
        elif isinstance(action, argparse._StoreAction):
            if action.nargs in ['+', '*']:
                property_data['type'] = 'array'
                property_data['items'] = {'type': 'string'}
                property_data['minItems'] = 1
            else:
                property_data['type'] = 'string'
        else:
            # Unknown actions should not be added to schema
            property_data = None

        # Some options maybe pre-added to schema with additional options, don't override them
        if property_data and name not in ObjectsContainer.task_execution_input['properties']:
            ObjectsContainer.task_execution_input['properties'][name] = property_data

    ObjectsContainer.task_execution_input['additionalProperties'] = False


class EntryDecoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, LazyLookup):
            return '<LazyField>'

        try:
            return JSONEncoder.default(self, o)
        except TypeError:
            return str(o)


_phase_percents = {
    'input': 5,
    'metainfo': 10,
    'filter': 30,
    'download': 40,
    'modify': 65,
    'output': 75,
    'exit': 100,
}


def update_stream(task, status='pending'):
    if task.current_phase in _phase_percents:
        task.stream['percent'] = _phase_percents[task.current_phase]

    progress = {
        'status': status,
        'phase': task.current_phase,
        'plugin': task.current_plugin,
        'percent': task.stream.get('percent', 0)
    }

    task.stream['queue'].put(json.dumps({'progress': progress}))


@event('task.execute.started')
def start_task(task):
    task.stream = _streams.get(task.id)

    if task.stream and task.stream['args'].get('progress'):
        update_stream(task, status='running')


@event('task.execute.completed')
def finish_task(task):
    if task.stream:
        if task.stream['args'].get('progress'):
            update_stream(task, status='complete')

        if task.stream['args'].get('entry_dump'):
            entries = [entry.store for entry in task.entries]
            task.stream['queue'].put(EntryDecoder().encode({'entry_dump': entries}))

        if task.stream['args'].get('summary'):
            task.stream['queue'].put(json.dumps({
                'summary': {
                    'accepted': len(task.accepted),
                    'rejected': len(task.rejected),
                    'failed': len(task.failed),
                    'undecided': len(task.undecided),
                    'aborted': task.aborted,
                    'abort_reason': task.abort_reason,
                }
            }))


@event('task.execute.before_plugin')
def track_progress(task, plugin_name):
    if task.stream and task.stream['args'].get('progress'):
        update_stream(task, status='running')
