__author__ = 'Bohdan Mushkevych'

from odm.errors import ValidationError

from synergy.conf import settings, context
from synergy.db.dao.unit_of_work_dao import UnitOfWorkDao, QUERY_GET_FLOW_FREERUNS
from synergy.db.dao.log_recording_dao import LogRecordingDao
from synergy.db.model.unit_of_work import TYPE_MANAGED, TYPE_FREERUN
from synergy.db.model.freerun_process_entry import freerun_context_entry, build_schedulable_name, split_schedulable_name
from synergy.mx.base_request_handler import BaseRequestHandler, valid_action_request, safe_json_response
from synergy.scheduler.scheduler_constants import STATE_MACHINE_FREERUN
from synergy.system import time_helper
from werkzeug.utils import cached_property

from flow.conf import flows
from flow.core.execution_context import ExecutionContext
from flow.db.dao.flow_dao import FlowDao
from flow.db.dao.step_dao import StepDao
from flow.flow_constants import *
from flow.mx.rest_model_factory import *

RESPONSE_OK = {'response': 'OK'}
RESPONSE_NOT_OK = {'response': 'Job is not finished'}


def flow_schedulable_name(process_name, flow_name, step_name):
    """ schedulable_name is used as freerun `uow.process_name`
        replicates format of the `FreerunProcessEntry.schedulable_name` with an assumption
        that entry_name is generated by `build_schedulable_name` """
    entry_name = build_schedulable_name(flow_name, step_name)
    return build_schedulable_name(process_name, entry_name)


def find_flow_step_uow(uow_dao, process_name, flow_name, step_name, timeperiod):
    """ Unless created manually, FreerunProcessEntries for workflow steps are run-time only objects
        i.e. they disappear on Synergy Scheduler restart;
        method tries to fetch UOW associated with key <process_name::workflow_name::step_name + timeperiod>"""
    try:
        schedulable_name = flow_schedulable_name(process_name, flow_name, step_name)
        uow = uow_dao.get_by_params(process_name=schedulable_name, timeperiod=timeperiod, start_id=0, end_id=0)
    except:
        uow = None
    return uow


def find_all_flow_uows(uow_dao, process_name, flow_name, timeperiod):
    schedulable_stem = flow_schedulable_name(process_name, flow_name, '')
    query = QUERY_GET_FLOW_FREERUNS(schedulable_stem, timeperiod)
    try:
        records_list = uow_dao.run_query(query)
    except:
        records_list = list()
    return records_list


class FlowRequest(object):
    def __init__(self, process_name, flow_name, step_name, run_mode, timeperiod, start_timeperiod, end_timeperiod):
        self.process_name = process_name
        self.flow_name = flow_name
        self.step_name = step_name
        self.run_mode = run_mode
        self.timeperiod = timeperiod
        self.start_timeperiod = start_timeperiod
        self.end_timeperiod = end_timeperiod

    @property
    def schedulable_name(self):
        return flow_schedulable_name(self.process_name, self.flow_name, self.step_name)

    @property
    def arguments(self):
        return {
            ARGUMENT_FLOW_NAME: self.flow_name,
            ARGUMENT_STEP_NAME: self.step_name,
            ARGUMENT_RUN_MODE: self.run_mode
        }


class FlowActionHandler(BaseRequestHandler):
    def __init__(self, request, **values):
        super(FlowActionHandler, self).__init__(request, **values)
        self.flow_dao = FlowDao(self.logger)
        self.step_dao = StepDao(self.logger)
        self.uow_dao = UnitOfWorkDao(self.logger)
        self.log_recording_dao = LogRecordingDao(self.logger)

        self.process_name = self.request_arguments.get(ARGUMENT_PROCESS_NAME)
        self.unit_of_work_type = self.request_arguments.get(ARGUMENT_UNIT_OF_WORK_TYPE, TYPE_MANAGED)
        self.flow_name = self.request_arguments.get(ARGUMENT_FLOW_NAME)
        if not self.flow_name and self.process_name:
            process_entry = context.process_context[self.process_name]
            self.flow_name = process_entry.arguments.get(ARGUMENT_FLOW_NAME)

        self.step_name = self.request_arguments.get(ARGUMENT_STEP_NAME)
        self.timeperiod = self.request_arguments.get(ARGUMENT_TIMEPERIOD)
        self.is_request_valid = True if self.flow_name \
                                        and self.flow_name in flows.flows \
                                        and self.timeperiod \
                                else False

        if self.is_request_valid:
            self.flow_name = self.flow_name.strip()
            self.timeperiod = self.timeperiod.strip()

        self.run_mode = self.request_arguments.get(ARGUMENT_RUN_MODE, '')
        self.run_mode = self.run_mode.strip()

    def _get_tree_node(self):
        tree = self.scheduler.timetable.get_tree(self.process_name)
        if tree is None:
            raise UserWarning('No Timetable tree is registered for process {0}'.format(self.process_name))

        time_qualifier = context.process_context[self.process_name].time_qualifier
        self.timeperiod = time_helper.cast_to_time_qualifier(time_qualifier, self.timeperiod)
        node = tree.get_node(self.process_name, self.timeperiod)
        return node

    @property
    def job_record(self):
        node = self._get_tree_node()
        return node.job_record

    @property
    def managed_uow_record(self):
        node = self._get_tree_node()
        uow_id = node.job_record.related_unit_of_work
        if not uow_id:
            return None
        return self.uow_dao.get_one(uow_id)

    @property
    def freerun_uow_records(self):
        valid_freerun_uow = list()
        records_list = find_all_flow_uows(self.uow_dao, self.process_name, self.flow_name, self.timeperiod)
        if len(records_list) == 0:
            self.logger.warning('MX: no Freerun UOW records found for {0}@{1} -> {2}.'
                                .format(self.process_name, self.timeperiod, self.flow_name))
            return valid_freerun_uow

        for uow_record in records_list:
            # freerun uow.process_name is a composite in format <process_name::entry_name>
            handler_key = split_schedulable_name(uow_record.process_name)
            if handler_key not in self.scheduler.freerun_handlers:
                # skip UOW records that have no active freerun handler
                continue

            valid_freerun_uow.append(uow_record)
        return valid_freerun_uow

    @property
    def freerun_process_entry(self):
        """ :returns run-time only instance of the FreerunProcessEntry """
        entry_name = build_schedulable_name(self.flow_name, self.step_name)
        handler_key = (self.process_name, entry_name)
        if handler_key not in self.scheduler.freerun_handlers:
            classname = context.process_context[self.process_name].classname
            entry = freerun_context_entry(
                process_name=self.process_name,
                entry_name=entry_name,
                classname=classname,
                token=entry_name,
                trigger_frequency='every {0}'.format(SECONDS_IN_CENTURY),
                is_on=False,
                description='Runtime freerun object to facilitate CUSTOM RUN MODES for workflow'
            )

            # find uow for workflow step if any
            uow = find_flow_step_uow(self.uow_dao, self.process_name, self.flow_name, self.step_name, self.timeperiod)
            if uow:
                entry.related_unit_of_work = uow.db_id

            self.scheduler.freerun_handlers[handler_key] = entry
        return self.scheduler.freerun_handlers[handler_key]

    @property
    def flow_record(self):
        return self.flow_dao.get_one([self.flow_name, self.timeperiod])

    @property
    def step_record(self):
        return self.step_dao.get_one([self.flow_name, self.step_name, self.timeperiod])

    @property
    def flow_graph_obj(self):
        _flow_graph_obj = copy.deepcopy(flows.flows[self.flow_name])
        _flow_graph_obj.context = ExecutionContext(self.flow_name, self.timeperiod, None, None, settings.settings)

        try:
            flow_entry = self.flow_dao.get_one([self.flow_name, self.timeperiod])
            _flow_graph_obj.context.flow_entry = flow_entry
            _flow_graph_obj.context.start_timeperiod = flow_entry.start_timeperiod
            _flow_graph_obj.context.end_timeperiod = flow_entry.end_timeperiod

            steps = self.step_dao.get_all_by_flow_id(flow_entry.db_id)
            for s in steps:
                assert isinstance(s, Step)
                _flow_graph_obj[s.step_name].step_entry = s
                _flow_graph_obj.yielded.append(s)
        except LookupError:
            pass
        return _flow_graph_obj

    @cached_property
    @valid_action_request
    def flow_details(self):
        rest_model = create_rest_flow(self.flow_graph_obj)
        return rest_model.document

    @cached_property
    def process_name(self):
        return self.process_name

    @cached_property
    def unit_of_work_type(self):
        return self.unit_of_work_type

    @cached_property
    def active_run_mode(self):
        return self.flow_dao.managed_run_mode(self.process_name, self.flow_name, self.timeperiod)

    @cached_property
    @valid_action_request
    def step_details(self):
        graph_node_obj = self.flow_graph_obj._dict[self.step_name]
        rest_model = create_rest_step(graph_node_obj)
        return rest_model.document

    @valid_action_request
    def set_run_mode(self):
        """
        - set a flag for ProcessEntry.arguments[ARGUMENT_RUN_MODE] = RUN_MODE_RECOVERY
        - trigger standard reprocessing
        """
        if not self.job_record or not self.run_mode:
            return RESPONSE_NOT_OK

        try:
            msg = 'MX: setting RUN MODE for {0}@{1} -> {2} to {3}' \
                .format(self.process_name, self.timeperiod, self.flow_name, self.run_mode)
            self.scheduler.timetable.add_log_entry(self.process_name, self.timeperiod, msg)
            self.logger.info(msg + ' {')

            local_record = self.flow_record
            local_record.run_mode = self.run_mode
            self.flow_dao.update(local_record)

            return RESPONSE_OK
        except (ValidationError, LookupError):
            return RESPONSE_NOT_OK
        finally:
            self.logger.info('}')

    def perform_freerun_action(self, run_mode):
        """
        - make sure that the job is finished
          i.e. the job is in [STATE_NOOP, STATE_PROCESSED, STATE_SKIPPED]
        - submit a FREERUN UOW for given (process_name::flow_name::step_name, timeperiod)
        :return RESPONSE_OK if the UOW was submitted and RESPONSE_NOT_OK otherwise
        """
        if not self.job_record or not self.job_record.is_finished:
            return RESPONSE_NOT_OK

        uow = self.managed_uow_record
        if not uow:
            # for skipped job that has no UOW associated with it
            return RESPONSE_NOT_OK

        flow_request = FlowRequest(self.process_name, self.flow_name, self.step_name,
                                   run_mode,
                                   self.timeperiod, uow.start_timeperiod, uow.end_timeperiod)

        state_machine = self.scheduler.timetable.state_machines[STATE_MACHINE_FREERUN]
        state_machine.manage_schedulable(self.freerun_process_entry, flow_request)
        return RESPONSE_OK

    @valid_action_request
    def run_one_step(self):
        return self.perform_freerun_action(RUN_MODE_RUN_ONE)

    @valid_action_request
    def run_from_step(self):
        """
        - make sure that the job is finished
          i.e. the job is in [STATE_NOOP, STATE_PROCESSED, STATE_SKIPPED]
        - submit a FREERUN UOW for given (process_name::flow_name::step_name, timeperiod)
        :return RESPONSE_OK if the UOW was submitted and RESPONSE_NOT_OK otherwise
        """
        return self.perform_freerun_action(RUN_MODE_RUN_FROM)

    @valid_action_request
    @safe_json_response
    def get_step_log(self):
        try:
            resp = self.log_recording_dao.get_one(self.step_record.db_id).document
        except (TypeError, LookupError):
            resp = {'response': 'no related step log'}
        return resp

    @valid_action_request
    @safe_json_response
    def get_flow_log(self):
        try:
            resp = self.log_recording_dao.get_one(self.flow_record.db_id).document
        except (TypeError, LookupError):
            resp = {'response': 'no related workflow log'}
        return resp
