import os
import sys

from parallels.core import messages
from parallels.core.actions.base.common_action import CommonAction
from parallels.core.actions.utils.logging_properties import LoggingProperties
from parallels.core.logging import get_logger
from parallels.core.migration_list.reader.json_reader import MigrationListReaderJSON
from parallels.core.registry import Registry
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.task_manager.manager import TaskManager, TaskOperations
from parallels.core.task_manager.task import Task
from parallels.core.task_manager.task_runner import TaskRunner
from parallels.core.task_manager.task_selector import FirstTaskSelector, TaskSelector
from parallels.core.utils import unix_utils
from parallels.core.utils import windows_utils
from parallels.core.utils.common import is_run_on_windows, open_no_inherit
from parallels.core.utils.entity import Entity
from parallels.core.utils.json_utils import read_json
from parallels.core.utils.migration_progress import SubscriptionMigrationStatus
from parallels.core.utils.subscription_operations import command_name_to_operation

logger = get_logger(__name__)


class RunTasksAction(CommonAction):
    """Daemon command that takes tasks from the queue and executes them one by one in a separate process

    Only one instance of that command could be running for the same queue.

    For more details on queue tasks you could refer to:
    parallels.core.actions.task_manager.add_task.AddTaskAction
    """

    def get_description(self):
        """Get short description of action as string

        :rtype: str
        """
        return messages.ACTION_RUN_TASKS_DESCRIPTION

    def get_failure_message(self, global_context):
        """Get message for situation when action failed

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        return messages.ACTION_RUN_TASKS_FAILURE

    def get_logging_properties(self):
        """Get how action should be logged to migration tools end-user

        :rtype: parallels.core.actions.utils.logging_properties.LoggingProperties
        """
        return LoggingProperties(info_log=False)

    def run(self, global_context):
        """Run action

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        task_manager = TaskManager(global_context.session_files.get_file_path('tasks'))
        task_manager.run_tasks_sequential(ServerMigrationTaskRunner(global_context, task_manager), FirstTaskSelector())


class ServerMigrationTaskRunner(TaskRunner):
    def __init__(self, global_context, task_manager):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type task_manager: parallels.core.task_manager.manager.TaskManager
        """
        self._global_context = global_context
        self._task_manager = task_manager

    def run(self, task):
        """
        :type task: parallels.core.task_manager.task.Task 
        :rtype: None 
        """
        command_file = os.path.join(task.task_dir, 'command.json')

        if not os.path.isfile(command_file):
            return

        with self._global_context.migrator_server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            command_info = read_json(command_file)
            args = [
                command_info['command'],
                self._global_context.session_files.get_path_to_config_file(),
            ]
            migration_list_file = os.path.join(task.task_dir, 'migration-list.json')
            if os.path.exists(migration_list_file):
                args.extend([
                    '--migration-list-format=json',
                    '--migration-list-file=%s' % migration_list_file
                ])
            args.append('--progress-task-id=%s' % task.task_id)
            args.extend(command_info['args'])

            if is_run_on_windows():
                command = windows_utils.format_command_list(
                    sys.executable,
                    [
                        sys.argv[0],
                        Registry.get_instance().get_var_dir()
                    ] + args
                )
            else:
                command = unix_utils.format_command_list(
                    sys.argv[0], args
                )

            logger.finfo(messages.RUN_COMMAND, command=command)
            runner.execute_command(command, execution_options=ExecutionOptions(ignore_exit_code=True))
            logger.finfo(messages.COMMAND_FINISHED)

            # Mark all subscriptions for which status was not updated as failed
            if os.path.exists(migration_list_file):
                migration_list_reader = MigrationListReaderJSON(
                    self._global_context.migrator.get_migration_list_source_data()
                )
                operation = command_name_to_operation(command_info['command'])
                with open_no_inherit(migration_list_file, 'r') as fp:
                    migration_list_data, _ = migration_list_reader.read(fp, subscriptions_only=True)

                with self._task_manager.task_operations() as task_operations:
                    assert isinstance(task_operations, TaskOperations)
                    task_status = task_operations.get_task_status(task)

                if task_status == Task.STATUS_CANCELLED:
                    unfinished_subscription_status = SubscriptionMigrationStatus.CANCELLED
                else:
                    unfinished_subscription_status = SubscriptionMigrationStatus.FINISHED_ERRORS

                for subscription_name in migration_list_data.subscriptions_mapping.keys():
                    if operation is not None:
                        current_status = self._global_context.subscriptions_status.get_operation_status(
                            subscription_name, operation
                        )
                        if current_status not in SubscriptionMigrationStatus.finished():
                            self._global_context.subscriptions_status.set_operation_status(
                                subscription_name, operation, unfinished_subscription_status
                            )


class ServerMigrationTaskSelector(TaskSelector):
    def select(self, tasks):
        """
        :type tasks: list[parallels.core.task_manager.task.Task]
        :rtype: parallels.core.task_manager.task.Task | None
        """
        if len(tasks) == 0:
            return None

        task_infos = []
        for task in tasks:
            command_file = os.path.join(task.task_dir, 'command.json')
            command_info = read_json(command_file)
            command = command_info['command']
            task_infos.append(TaskInfo(task=task, command=command))

        sorted_tasks = sorted(task_infos, cmp=compare_tasks)

        if len(sorted_tasks) > 0:
            return sorted_tasks[0].task
        else:
            return None


class TaskInfo(Entity):
    def __init__(self, task, command):
        """
        :type task: parallels.core.task_manager.task.Task
        :type command: str | unicode
        """
        self._task = task
        self._command = command

    @property
    def task(self):
        """
        :rtype: parallels.core.task_manager.task.Task 
        """
        return self._task

    @property
    def command(self):
        """
        :rtype: str | unicode 
        """
        return self._command


def compare_tasks(task_info_1, task_info_2):
    """Compare 2 tasks to understand order in which they should be executed

    :type task_info_1: parallels.core.actions.queue.run_tasks.TaskInfo
    :type task_info_2: parallels.core.actions.queue.run_tasks.TaskInfo
    """

    # First, pre-migration check tasks should be executed. Most probably user is waiting for their results
    # to schedule migration. Also these kind of tasks usually work fast and won't make other kinds of tasks
    # to wait a lot.
    if task_info_1.command == 'check' and task_info_2.command != 'check':
        return -1
    if task_info_1.command != 'check' and task_info_2.command == 'check':
        return 1

    # Then compare tasks by date when they were added (task id is a date string) - the earlier the ask was
    if task_info_1.task.task_id > task_info_2.task.task_id:
        return 1
    elif task_info_1.task.task_id < task_info_2.task.task_id:
        return -1
    else:
        return 0
