import json
from collections import namedtuple
from copy import deepcopy

Operation = namedtuple('Operation', ['handler_fn', 'param_keys'])


class Processor:
    recipe_file = None
    mandatory_keys = {
    }

    def __init__(self, data, **kwargs):
        self.data = data

    @classmethod
    def validate_recipe_data(cls, recipe_data):
        violations_list = []

        def validate(json_obj, violations, mandatory_keys_dict, bread_crumbs):
            if isinstance(json_obj, dict):
                for key in mandatory_keys_dict:
                    if key not in json_obj:
                        violations.append('{key} @ {crumbs}'.format(key=key, crumbs='/'.join(map(str, bread_crumbs))))
                    else:
                        validate(json_obj[key], violations, mandatory_keys_dict[key], bread_crumbs + [key])

            elif isinstance(json_obj, list):
                for index, elem in enumerate(json_obj):
                    validate(elem, violations, mandatory_keys_dict, bread_crumbs + [index])

            return violations

        return validate(recipe_data, violations_list, cls.mandatory_keys, [])

    @classmethod
    def load(cls, **kwargs):
        try:
            with open(cls.recipe_file, 'r') as read_f:
                data = json.load(read_f)
            assert isinstance(data, list)
        except FileNotFoundError:
            raise ProcessorException(f'Migration recipe file not found: {cls.recipe_file}') from None
        except json.decoder.JSONDecodeError as ex:
            raise ProcessorException(f'Invalid JSON in recipe file: {cls.recipe_file}: {ex}') from None
        except AssertionError:
            raise ProcessorException(f'Invalid recipe file: {cls.recipe_file}: Top level must be a list') from None

        # Enforce mandatory_keys
        violations = cls.validate_recipe_data(data)
        if violations:
            raise ProcessorException(
                f"Invalid recipe file: {cls.recipe_file}: Missing mandatory keys: {', '.join(violations)}"
            )

        return cls(data, **kwargs)

    def is_in_scope(self, template_obj, **kwargs):
        raise NotImplemented('Method not implemented')

    def eval(self, template_obj, new_name, new_id):
        migrated_payload = deepcopy(template_obj.data)
        trace_log = []

        return migrated_payload, trace_log

    def replace_original(self):
        """ By default, items generated by the processor would replace the original item """
        return True


class StopProcessorException(Exception):
    """ Exception indicating need to stop further processing of this item """
    pass


class ProcessorException(Exception):
    """ Exception for Migration Processor errors """
    pass
