#
# Copyright BrainPad Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
from cliboa.core.context import _CliboaContext
from cliboa.core.executor import _StepExecutor
from cliboa.core.factory import _CliboaFactory, _get_scenario_loader_class
from cliboa.core.interface import _IExecute
from cliboa.core.loader import _ScenarioLoader
from cliboa.core.model import CommandArgument, ParallelStepModel, ScenarioModel, StepModel
from cliboa.core.processor import _ParallelProcessor
from cliboa.listener.base import BaseStepListener
from cliboa.listener.step import StepStatusListener
from cliboa.util.base import _BaseObject
from cliboa.util.exception import CliboaException


class _ScenarioBuilder(_BaseObject):
    """
    Build executable instances from scenario file and command argument.
    """

    def __init__(
        self,
        scenario_file: str,
        common_file: str | list[str] | None = None,
        file_format: str = "yaml",
        cmd_arg: CommandArgument | None = None,
        project_name: str | None = None,
        *args,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self._scenario_file = scenario_file
        if isinstance(common_file, str):
            self._common_files = [common_file]
        elif isinstance(common_file, list):
            self._common_files = common_file
        else:
            self._common_files = []
        self._loader_cls: _ScenarioLoader = self._resolve_cls(
            "loader", _get_scenario_loader_class(file_format)
        )
        self._scenario_model_cls = self._resolve_cls("scenario_model", ScenarioModel)
        self._factory = self._resolve("factory", _CliboaFactory, project_name)
        self._cmd_arg = cmd_arg
        self._step_model_map = {}
        self._context = self._resolve("context", _CliboaContext)

    def execute(self) -> list[_IExecute]:
        """
        Main logic of builder.
        """
        self._logger.info("Start to build scenario.")
        # 1. Get a scenario model including step models.
        scenario = self._parse_scenario()
        # 2. Set up the scenario model - propagate settings and calc with_vars.
        scenario.setup()
        # 3. Create step instances by step models which are in the scenario model.
        steps = self._create_steps(scenario)
        self._logger.info("Complete to build scenario.")
        return steps

    def _parse_scenario(self) -> ScenarioModel:
        """
        Load main scenario file and generate a model instance.

        if common file exists, merge it to main.
        """
        self._logger.info("Start to parse scenario files.")

        self._logger.info(f"Load main scenario file {self._scenario_file}")
        top_dict = self._loader_cls(self._scenario_file, True)()
        main_scenario = self._scenario_model_cls.model_validate(top_dict)

        for cmn_scenario_file in self._common_files:
            self._logger.info(f"Load common scenario file {cmn_scenario_file}")
            top_dict = self._loader_cls(cmn_scenario_file, False)()
            if top_dict:
                cmn_scenario = self._scenario_model_cls.model_validate(top_dict)
                main_scenario.merge(cmn_scenario)
            else:
                self._logger.warning(
                    f"Failed to load {cmn_scenario_file}, continue to parse scenario."
                )

        self._logger.info("Complete to parse scenario files.")
        return main_scenario

    def _setup_step_model_map(self, scenario: ScenarioModel) -> None:
        for step in scenario.scenario:
            if isinstance(step, StepModel):
                key = step.step
                if key in self._step_model_map:
                    self._logger.warning(f"Duplicate step: {key}")
                # Last-one-wins if duplicated
                self._step_model_map[key] = step
            elif isinstance(step, ParallelStepModel):
                for p_step in step.parallel:
                    key = p_step.step
                    if key in self._step_model_map:
                        self._logger.warning(f"Duplicate step: {key}")
                    # Last-one-wins if duplicated
                    self._step_model_map[key] = p_step
            else:
                raise CliboaException(f"Unexpected step instance: {step.__class__.__name__}")

    def _create_steps(self, scenario: ScenarioModel) -> list[_IExecute]:
        """
        Add executable instance to the queue
        """
        self._setup_step_model_map(scenario)
        steps = []
        for step in scenario.scenario:
            instance = self._create_step_instance(step)
            steps.append(instance)
        return steps

    def _create_step_instance(
        self, step: StepModel | ParallelStepModel
    ) -> _StepExecutor | _ParallelProcessor:
        """
        Create executable instances
        """
        if isinstance(step, StepModel):
            instance = self._create_executor(step)
            return instance
        elif isinstance(step, ParallelStepModel):
            instances = []
            for p_step in step.parallel:
                instance = self._create_executor(p_step)
                instances.append(instance)
            return self._resolve(
                "parallel_processor", _ParallelProcessor, instances, step.parallel_config
            )
        else:
            raise CliboaException(f"Unexpected step instance: {step.__class__.__name__}")

    def _create_executor(self, step: StepModel) -> _StepExecutor:
        """
        Create _StepExecutor instance - wraps BaseStep.
        """
        cls_name = step.class_name
        self._logger.debug("Create %s instance" % cls_name)

        instance = self._factory.create(cls_name, **self._di_kwargs)
        symbol_model = self._step_model_map.get(step.symbol) if step.symbol else None

        executor = self._resolve(
            "step_executor",
            _StepExecutor,
            instance,
            step,
            self._cmd_arg,
            self._context,
            symbol_model,
        )

        for lis in self._create_listeners(step):
            executor.register_listener(lis)
        return executor

    def _create_listeners(self, step: StepModel) -> list[BaseStepListener]:
        listeners = [self._resolve("step_status_listener", StepStatusListener())]
        for lis_cls in step.get_listeners():
            clz = self._factory.create(lis_cls, **self._di_kwargs)
            listeners.append(clz)
        return listeners
