# Copyright (c) ModelScope Contributors. All rights reserved.
import os
from dataclasses import dataclass, field
from typing import Literal, Optional

from swift.template import TEMPLATE_MAPPING, get_template_meta
from swift.utils import get_logger

logger = get_logger()


@dataclass
class TemplateArguments:
    """TemplateArguments class holds various arguments for template configuration.

    This dataclass manages settings related to how data is formatted and processed using templates, including
    tokenization, truncation, loss calculation, and special handling for multimodal and agent-based models.

    Args:
        template (Optional[str]): The dialogue template type. Defaults to None, which automatically selects the
            template corresponding to the model type. Refer to the list of supported models for mappings.
        system (Optional[str]): Custom system prompt. Can be a string or a path to a .txt file. Defaults to None,
            which uses the default system from the registered template.
            Note: The priority for the system prompt is as follows:
            1. System prompt from the dataset.
            2. The `--system` command-line argument.
            3. The `default_system` set when the template was registered.
        max_length (Optional[int]): The maximum number of tokens for a single sample after tokenization. Samples
            exceeding this length are handled according to `truncation_strategy` to prevent OOM errors. Defaults to
            None, which uses the model's maximum supported length (`max_model_len`). In PPO, GRPO, and inference
            scenarios, this argument specifies the `max_prompt_length`.
        truncation_strategy (Literal['delete', 'left', 'right', 'split']): Strategy for handling samples exceeding
            `max_length`. Options are 'delete', 'left' (truncate from the left), 'right' (truncate from the right),
            and 'split' (split into multiple samples). Defaults to 'delete'.
            Note: The 'split' strategy is only supported during pre-training (e.g., `swift/megatron pt`),
            and is incompatible with `cached_dataset`. It splits long samples to avoid wasting tokens.
            Note: For multimodal models, setting this to 'left' or 'right' preserves all image tokens, which may lead
            to OOM errors.
        max_pixels (Optional[int]): The maximum number of pixels (H*W) for an input image in a multimodal model.
            Images exceeding this limit will be scaled down to prevent OOM errors. Defaults to None, meaning no limit.
            Note: This parameter applies to all multimodal models. The model-specific `MAX_PIXELS` parameter for
            Qwen2.5-VL is separate and only applies to that model.
        agent_template (Optional[str]): The Agent template to use. This determines how the 'tools' list is converted
            into a 'system' prompt, how tool calls are extracted from the model's response during inference, and the
            format for tool call messages. Options include "react_en", "hermes", "glm4", "qwen_en", "toolbench", etc.
            Defaults to None, which auto-selects based on the model type. Refer to the Agent documentation for more
            details.
        norm_bbox (Optional[Literal['norm1000', 'none']]): Controls how bounding box coordinates (from the "bbox"
            field in the dataset) are scaled. 'norm1000' scales coordinates to a 1000x1000 grid, while 'none' performs
            no scaling. Defaults to None, which auto-selects based on the model. This handles cases where images are
            resized during training (e.g., due to `max_pixels`).
        use_chat_template (bool): Whether to use the chat template or the generation template. The generation template
            is typically used for pre-training. Defaults to True.
            Note: Defaults to False for `swift pt`, which uses the generation template. This parameter is compatible
            with multimodal models.
        padding_side (Literal['left', 'right']): The side to pad on when `batch_size >= 2` during training.
            Options are 'left' or 'right'. Defaults to 'right'. For inference with `batch_size >= 2`, padding is always
            on the left.
            Note: Defaults to 'left' for PPO and GKD.
        padding_free (bool): If True, flattens the data within a batch to avoid padding, reducing memory usage and
            speeding up training. Sequences within the batch remain causally isolated. Defaults to False. Supported for
            CPT/SFT/DPO/GRPO/KTO/GKD.
            Note: This requires `--attn_impl flash_attn` and `transformers>=4.44`. Compared to packing, padding_free
            has no preprocessing overhead, but packing offers faster training speeds and more stable memory usage.
        loss_scale (str): Loss weight configuration for training tokens. Default is `'default'`.
            loss_scale includes 3 basic strategies: 'default', 'last_round', 'all', and other strategies:
            'ignore_empty_think' and agent-specific ones: 'react', 'hermes', 'qwen', 'agentflan', 'alpha_umi', etc.
            For available options, refer to
            [loss_scale module](https://github.com/modelscope/ms-swift/blob/main/swift/loss_scale/mapping.py).
            ms-swift supports mixing basic strategies with other strategies,
            for example: `'default+ignore_empty_think'`, `'last_round+ignore_empty_think'`.
            If no basic strategy is specified, it defaults to 'default',
            for example: 'hermes' is equivalent to 'default+hermes'.
            Multiple non-base strategies can be chained together
            (each strategy processes the output segments of the previous one, with weights
            multiplied accordingly). For example: `'last_round+hermes+ignore_empty_think'`, where
            `'last_round'` is the base strategy, and `'hermes+ignore_empty_think'` represents a
            chain of multiple non-base strategies that share the same base strategy.
            - 'default': All responses (including history) are calculated with weight 1 for cross-entropy loss
            (**system/user/multimodal tokens in messages and `tool_response` parts in Agent training are
            not included in loss calculation**). (**Default value for SFT**)
            - 'last_round': Only calculate loss for the last round response. The last round
            means all content after the last "user". (**Default value for RLHF**)
            - 'all': Calculate loss for all tokens. (**Default value for `swift pt`**)
            - 'ignore_empty_think': Ignore loss computation for empty `'<think>\n\n</think>\n\n'`
            (as long as it matches the regex `'<think>\\s*</think>\\s*'`).
            - 'react', 'hermes', 'qwen': Adjust the loss weight of the `tool_call` part to 2.
        sequence_parallel_size (int): The size of sequence parallelism. Defaults to 1. Currently supported for CPT,
            SFT, DPO, and GRPO.
        template_backend (Literal['swift', 'jinja']): The backend to use for templating. Options are 'swift' or
            'jinja'. Defaults to 'swift'. If 'jinja' is used, it will leverage `transformers.apply_chat_template`.
            Note: The 'jinja' backend is only supported for inference, not for training, as it cannot determine the
            token range for loss calculation.
        response_prefix (Optional[str]): A prefix string for the response, e.g., '<think>\\n' for Qwen-32B. This
            parameter only affects inference. Defaults to None, which is auto-set based on the model.
        enable_thinking (Optional[bool]): This parameter takes effect during inference,
            indicating whether to enable thinking mode. Default is None, the default value is determined by the
            template (model) type (True for thinking/hybrid thinking templates, False for non-thinking templates).
            If enable_thinking is False, a non-thinking prefix is added, for example the Qwen3-8B hybrid thinking
            model adds the prefix `'<think>\n\n</think>\n\n'`, while Qwen3-8B-Thinking does not add a prefix.
            If enable_thinking is True, a thinking prefix is added, for example `'<think>\n'`.
            Note: The priority of this parameter is lower than the response_prefix parameter.
        preserve_thinking (Optional[bool]): Whether to preserve historical thinking content during inference and
            training. When set to `True`, thinking content from all rounds is retained. When set to `False`,
            only the thinking content from the last round is retained (i.e., the content following the last
            user message). Defaults to `None`.
            Default behavior: For thinking models (thinking/hybrid-thinking) or when `enable_thinking` is
            explicitly enabled, this is set to `False` by default during inference and training, retaining
            only the last round of thinking content. If the `loss_scale` base strategy during training is
            not `'last_round'` (e.g., `'default'`), it defaults to `True`, and historical thinking content will
            not be removed.
        add_non_thinking_prefix (bool): This parameter only takes effect during training, indicating whether to
            add a non-thinking prefix to data samples whose assistant part does not start with the thinking
            marker `'<think>'` (typically hybrid thinking models contain a non-thinking prefix).
            This feature allows swift's built-in datasets to train hybrid thinking models. Default value is True.
            For example: the non-thinking prefix for the Qwen3-8B hybrid thinking model is
            `'<think>\n\n</think>\n\n'`, while the non-thinking prefix for Qwen3-8B-Thinking/Instruct is `''`.
            Note: During training, if the basic strategy of loss_scale is last_round, this modification is only
            applied to the last round; otherwise, for example 'default' or 'all', this modification is applied to
            every round of data. If set to False, no non-thinking prefix is added to data samples.


    """
    template: Optional[str] = field(
        default=None, metadata={'help': f'template choices: {list(TEMPLATE_MAPPING.keys())}'})
    system: Optional[str] = None  # Override the default_system in the template.
    max_length: Optional[int] = None

    truncation_strategy: Literal['delete', 'left', 'right', 'split', None] = None
    max_pixels: Optional[int] = None
    agent_template: Optional[str] = None
    norm_bbox: Literal['norm1000', 'none', None] = None
    use_chat_template: Optional[bool] = None
    padding_side: Literal['left', 'right'] = 'right'
    # train
    padding_free: bool = False
    loss_scale: str = 'default'
    sequence_parallel_size: int = 1
    is_binary_loss_scale: Optional[bool] = None
    # infer/deploy
    template_backend: Literal['swift', 'jinja'] = 'swift'
    # thinking
    response_prefix: Optional[str] = None
    enable_thinking: Optional[bool] = None
    preserve_thinking: Optional[bool] = None
    add_non_thinking_prefix: bool = True
    disable_ignore_empty_think: bool = False

    def __post_init__(self):
        if getattr(self, 'model_meta', None) is not None:
            self.template_meta = get_template_meta(self.model_info, self.model_meta, template_type=self.template)
            self.template = self.template_meta.template_type
        if self.use_chat_template is None:
            self.use_chat_template = True
        if self.system is not None:
            if self.system.endswith('.txt'):
                assert os.path.isfile(self.system), f'self.system: {self.system}'
                with open(self.system, 'r', encoding='utf-8') as f:
                    self.system = f.read()
            else:
                self.system = self.system.replace('\\n', '\n')
        if self.response_prefix is not None:
            self.response_prefix = self.response_prefix.replace('\\n', '\n')
        if self.truncation_strategy is None:
            self.truncation_strategy = 'delete'
        self._set_loss_scale()

    def _set_loss_scale(self):
        """For hybrid thinking models, automatically append '+ignore_empty_think' to loss_scale."""
        if not self.disable_ignore_empty_think and getattr(self, 'template_meta', None) is not None:
            template_meta = self.template_meta
            if template_meta.is_thinking and template_meta.non_thinking_prefix:
                # hybrid thinking model detected
                if self.loss_scale and 'ignore_empty_think' not in self.loss_scale:
                    self.loss_scale = self.loss_scale + '+ignore_empty_think'

    def get_template_kwargs(self):
        truncation_strategy = self.truncation_strategy
        if truncation_strategy == 'delete':
            truncation_strategy = 'raise'
        return {
            'template_type': self.template,
            'default_system': self.system,
            'max_length': self.max_length,
            'truncation_strategy': truncation_strategy,
            'max_pixels': self.max_pixels,
            'agent_template': self.agent_template,
            'norm_bbox': self.norm_bbox,
            'use_chat_template': self.use_chat_template,
            'remove_unused_columns': self.remove_unused_columns,  # from DataArguments
            'padding_side': self.padding_side,
            # train
            'padding_free': self.padding_free,
            'loss_scale': self.loss_scale,
            'is_binary_loss_scale': self.is_binary_loss_scale,
            'sequence_parallel_size': self.sequence_parallel_size,
            # infer/deploy
            'template_backend': self.template_backend,
            # thinking
            'response_prefix': self.response_prefix,
            'enable_thinking': self.enable_thinking,
            'preserve_thinking': self.preserve_thinking,
            'add_non_thinking_prefix': self.add_non_thinking_prefix,
        }
