"""
LLM Fine-tuning Runner Implementation

This module provides a specialized runner for LLM fine-tuning that executes
LLaMA-Factory configuration files generated by the coder.
"""

from rdagent.app.finetune.llm.conf import FT_RD_SETTING
from rdagent.components.coder.CoSTEER import CoSTEER
from rdagent.components.coder.CoSTEER.evaluators import (
    CoSTEERMultiEvaluator,
    CoSTEERSingleFeedback,
)
from rdagent.components.coder.CoSTEER.evolving_strategy import (
    MultiProcessEvolvingStrategy,
)
from rdagent.components.coder.CoSTEER.knowledge_management import (
    CoSTEERQueriedKnowledge,
)
from rdagent.components.coder.finetune.conf import (
    FT_YAML_FILE_NAME,
    FTCoderCoSTEERSettings,
)
from rdagent.components.coder.finetune.eval import FTDataEvaluator
from rdagent.core.experiment import FBWorkspace, Task
from rdagent.core.scenario import Scenario
from rdagent.log import rdagent_logger as logger
from rdagent.scenarios.finetune.train.eval import FTRunnerEvaluator


class FTRunnerSettings(FTCoderCoSTEERSettings):
    """LLM Fine-tuning specific runner settings."""

    class Config:
        env_prefix = "LLM_FT_Runner_"


class FTRunnerEvolvingStrategy(MultiProcessEvolvingStrategy):
    """Evolving strategy for LLM fine-tuning runner.

    Runner directly executes the yaml from coder without modification.
    The coder generates full training config, and its validator tests with micro-batch.
    """

    def implement_one_task(
        self,
        target_task: Task,
        queried_knowledge: CoSTEERQueriedKnowledge | None = None,
        workspace: FBWorkspace | None = None,
        prev_task_feedback: CoSTEERSingleFeedback | None = None,
    ) -> dict[str, str]:
        """No modification needed - directly use coder's full training config."""
        # TODO: detect error during training automatically, and fix it here
        if not workspace or FT_YAML_FILE_NAME not in workspace.file_dict:
            logger.error(f"No {FT_YAML_FILE_NAME} found in workspace")
            return {}

        # Coder already generated full training config, no modification needed
        # Return empty dict to indicate no changes
        return {}


class LLMFinetuneRunner(CoSTEER):
    """LLM Fine-tuning specific runner that executes LLaMA-Factory configurations."""

    def __init__(
        self,
        scen: Scenario,
        *args,
        **kwargs,
    ) -> None:
        eval_l = [
            FTRunnerEvaluator(scen=scen),  # Training validation
        ]

        eva = CoSTEERMultiEvaluator(single_evaluator=eval_l, scen=scen)
        settings = FTRunnerSettings()

        # Use runner-specific evolving strategy for full dataset training
        es = FTRunnerEvolvingStrategy(scen=scen, settings=settings, improve_mode=True)

        # Initialize with LLM-specific configuration
        super().__init__(
            *args,
            settings=settings,
            eva=eva,
            es=es,
            evolving_version=2,
            scen=scen,
            max_loop=getattr(FT_RD_SETTING, "runner_max_loop", 1),  # Default to 1 loop for running
            stop_eval_chain_on_fail=True,  # finetune involve partial implementation.
            **kwargs,
        )

    def develop(self, exp):
        """Execute LLaMA-Factory fine-tuning on full dataset.

        Runner directly executes the full training config generated by coder.
        The actual training execution and basic validation are handled by LLMFinetuneEvaluator.
        Benchmark evaluation should be done as a separate step after training.
        """
        logger.info("Starting full dataset LLM fine-tuning with LLaMA-Factory")

        # Run the standard CoSTEER develop process:
        # 1. Execute training using coder's full training config (no modification)
        # 2. Validate execution using LLMFinetuneEvaluator
        exp = super().develop(exp)
        return exp

    def get_develop_max_seconds(self) -> int | None:
        """Get maximum seconds for development using FT settings."""
        return int(self.scen.real_full_timeout() * self.settings.max_seconds_multiplier)

    def compare_and_pick_fb(self, base_fb, new_fb) -> bool:
        """Compare feedback for LLM fine-tuning results."""
        if base_fb is None:
            return True

        base_fb = base_fb[0]
        new_fb = new_fb[0]

        def compare_scores(s1, s2) -> bool:
            if s2 is None:
                return False
            if s1 is None:
                return True
            return (s2 > s1) == self.scen.metric_direction

        return compare_scores(getattr(base_fb, "score", None), getattr(new_fb, "score", None))
