# HLDP原生格式训练执行器
# HLDP://zhuyuan-agent/training-runner
#
# 核心验证：Notion原生页面格式（HLDP标记）直接训练，不转JSONL。

import os
import json
import time
import sys
from typing import Optional, Callable


# 特殊Token定义（HLDP结构标记）
SPECIAL_TOKENS = [
    "[HLDP_PATH]", "[/HLDP_PATH]",
    "[PERSONA]", "[/PERSONA]",
    "[COGNITIVE_JUMP]", "[/COGNITIVE_JUMP]",
    "[CAUSAL_CHAIN]", "[/CAUSAL_CHAIN]",
    "[TITLE]", "[/TITLE]",
    "[CONTENT]", "[/CONTENT]",
    "[QUALITY_HIGH]", "[QUALITY_MEDIUM]",
    "[THINKING]", "[/THINKING]",
    "[HEADING_1]", "[/HEADING_1]",
    "[HEADING_2]", "[/HEADING_2]",
    "[HEADING_3]", "[/HEADING_3]",
    "[CODE_BLOCK]", "[/CODE_BLOCK]",
    "[QUOTE]", "[/QUOTE]",
    "[CALLOUT]", "[/CALLOUT]",
    "[LIST_ITEM]", "[/LIST_ITEM]",
]


class TrainingRunner:
    """HLDP原生格式训练管道"""
    
    def __init__(self, config: dict, progress_callback: Optional[Callable] = None):
        """
        Args:
            config: 训练配置（来自config.json的training部分）
            progress_callback: 每步回调，接收 (step, loss, total_steps) 
        """
        self.config = config
        self.progress_callback = progress_callback
        self.model = None
        self.tokenizer = None
        self.start_time = None
        self.loss_history = []
    
    def prepare(self):
        """准备训练环境：注册特殊token，加载模型"""
        print("[铸渊Agent] 准备HLDP训练环境...")
        
        try:
            from transformers import AutoModelForCausalLM, AutoTokenizer
            from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
            import torch
            
            model_name = self.config.get("model_name", "Qwen/Qwen2.5-3B")
            use_4bit = self.config.get("use_4bit", True)
            
            print(f"[铸渊Agent] 加载模型: {model_name}")
            
            # 加载tokenizer并添加HLDP特殊token
            self.tokenizer = AutoTokenizer.from_pretrained(
                model_name,
                trust_remote_code=True,
                padding_side="right"
            )
            
            # 设置pad_token
            if self.tokenizer.pad_token is None:
                self.tokenizer.pad_token = self.tokenizer.eos_token
            
            # 添加HLDP特殊token到tokenizer
            num_added = self.tokenizer.add_tokens(SPECIAL_TOKENS)
            print(f"[铸渊Agent] 添加了 {num_added} 个HLDP特殊token到tokenizer")
            
            # 加载模型（4bit量化以适配3090 24GB）
            load_kwargs = {
                "trust_remote_code": True,
                "torch_dtype": torch.float16,
                "device_map": "auto",
            }
            
            if use_4bit:
                try:
                    from transformers import BitsAndBytesConfig
                    load_kwargs["quantization_config"] = BitsAndBytesConfig(
                        load_in_4bit=True,
                        bnb_4bit_compute_dtype=torch.float16,
                        bnb_4bit_use_double_quant=True,
                    )
                    print("[铸渊Agent] 使用4bit量化加载")
                except ImportError:
                    print("[铸渊Agent] bitsandbytes不可用，使用float16")
                    load_kwargs.pop("quantization_config", None)
            
            self.model = AutoModelForCausalLM.from_pretrained(model_name, **load_kwargs)
            
            # 扩展embedding层以支持新token
            if num_added > 0:
                self.model.resize_token_embeddings(len(self.tokenizer))
            
            # 准备LoRA
            self.model = prepare_model_for_kbit_training(self.model)
            lora_config = LoraConfig(
                r=self.config.get("lora_r", 16),
                lora_alpha=self.config.get("lora_alpha", 32),
                target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
                lora_dropout=0.05,
                bias="none",
                task_type="CAUSAL_LM",
            )
            self.model = get_peft_model(self.model, lora_config)
            self.model.print_trainable_parameters()
            
            print("[铸渊Agent] HLDP训练环境准备完成")
            return True
            
        except ImportError as e:
            print(f"[铸渊Agent] 缺少依赖: {e}")
            print("请安装: pip install transformers peft accelerate bitsandbytes torch")
            return False
        except Exception as e:
            print(f"[铸渊Agent] 准备失败: {e}")
            return False
    
    def load_hldp_corpus(self, corpus_dir: str) -> list:
        """加载HLDP格式语料（不转JSONL，保留原生HLDP标记）
        
        Returns:
            list of str: 每条是一个HLDP标记的完整训练文本
        """
        texts = []
        
        if not os.path.exists(corpus_dir):
            print(f"[铸渊Agent] 语料目录不存在: {corpus_dir}")
            return texts
        
        for root, dirs, files in os.walk(corpus_dir):
            for filename in files:
                filepath = os.path.join(root, filename)
                
                if filename.endswith(".hldp"):
                    # HLDP原生格式文件
                    with open(filepath, "r", encoding="utf-8") as f:
                        texts.append(f.read())
                
                elif filename.endswith(".md"):
                    # Markdown文件 → 按HLDP结构包装
                    with open(filepath, "r", encoding="utf-8") as f:
                        content = f.read()
                    # 包裹HLDP标记
                    wrapped = f"[HLDP_PATH]{filepath}[/HLDP_PATH]\n[CONTENT]\n{content}\n[/CONTENT]"
                    texts.append(wrapped)
        
        print(f"[铸渊Agent] 加载了 {len(texts)} 条HLDP语料")
        return texts
    
    def train(self, corpus_dir: str, progress_callback: Optional[Callable] = None):
        """执行HLDP原生格式训练"""
        if self.model is None:
            if not self.prepare():
                return {"status": "error", "message": "模型准备失败"}
        
        texts = self.load_hldp_corpus(corpus_dir)
        if not texts:
            return {"status": "error", "message": "无语料数据"}
        
        # 如果没有transformers Trainer，用simulated training输出结构
        # 实际训练在3090上运行时才加载完整transformers
        try:
            from transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling
            import torch
            from torch.utils.data import Dataset
            
            max_steps = self.config.get("max_steps", 500)
            batch_size = self.config.get("batch_size", 2)
            grad_accum = self.config.get("gradient_accumulation", 4)
            lr = self.config.get("learning_rate", 2e-4)
            output_dir = self.config.get("output_dir", "/data/models/shuangyan-3b-hldp")
            max_seq_length = self.config.get("max_seq_length", 2048)
            
            # 准备数据集
            class HLDPDataset(Dataset):
                def __init__(self, texts, tokenizer, max_length):
                    self.encodings = tokenizer(
                        texts, truncation=True, padding="max_length",
                        max_length=max_length, return_tensors="pt"
                    )
                
                def __len__(self): return len(self.encodings["input_ids"])
                
                def __getitem__(self, idx):
                    return {k: v[idx] for k, v in self.encodings.items()}
            
            dataset = HLDPDataset(texts, self.tokenizer, max_seq_length)
            data_collator = DataCollatorForLanguageModeling(
                tokenizer=self.tokenizer, mlm=False
            )
            
            # 训练参数
            training_args = TrainingArguments(
                output_dir=output_dir,
                per_device_train_batch_size=batch_size,
                gradient_accumulation_steps=grad_accum,
                learning_rate=lr,
                warmup_steps=self.config.get("warmup_steps", 100),
                max_steps=max_steps,
                logging_steps=5,
                save_steps=self.config.get("save_steps", 50),
                save_total_limit=3,
                fp16=True,
                report_to=[],
                dataloader_pin_memory=False,
            )
            
            # 自定义callback用于进度上报
            class ProgressCallback:
                def __init__(self, runner, total_steps):
                    self.runner = runner
                    self.total_steps = total_steps
                    self.current_step = 0
                    self.start = time.time()
                
                def on_log(self, args, state, control, logs=None, **kwargs):
                    if logs and "loss" in logs:
                        self.current_step = state.global_step
                        loss = logs["loss"]
                        self.runner.loss_history.append(loss)
                        elapsed = time.time() - self.start
                        
                        # 估算ETA
                        if self.current_step > 0:
                            eta = (elapsed / self.current_step) * (self.total_steps - self.current_step)
                        else:
                            eta = 0
                        
                        # 回调上报进度
                        cb = progress_callback or self.runner.progress_callback
                        if cb:
                            cb(self.current_step, loss, self.total_steps, {
                                "eta_seconds": eta,
                                "elapsed_seconds": elapsed,
                                "learning_rate": state.optimizer.param_groups[0]["lr"] if state.optimizer else None,
                                "loss_history": self.runner.loss_history[-50:],
                            })
            
            progress_cb = ProgressCallback(self, max_steps)
            
            trainer = Trainer(
                model=self.model,
                args=training_args,
                train_dataset=dataset,
                data_collator=data_collator,
                callbacks=[progress_cb],
            )
            
            print(f"[铸渊Agent] 开始HLDP训练: {max_steps}步 × {batch_size}batch × {grad_accum}累积")
            self.start_time = time.time()
            
            trainer.train()
            
            # 保存模型
            trainer.save_model(output_dir)
            self.tokenizer.save_pretrained(output_dir)
            
            elapsed = time.time() - self.start_time
            print(f"[铸渊Agent] HLDP训练完成！耗时: {elapsed:.0f}s, 最终loss: {self.loss_history[-1] if self.loss_history else 'N/A'}")
            
            return {
                "status": "done",
                "final_loss": self.loss_history[-1] if self.loss_history else None,
                "steps_completed": max_steps,
                "elapsed_seconds": elapsed,
                "output_dir": output_dir,
                "message": f"HLDP原生格式训练完成，{len(texts)}条语料，{max_steps}步",
            }
            
        except ImportError as e:
            # 如果transformers不可用，返回simulated结果用于测试仪表盘
            print(f"[铸渊Agent] 训练依赖不可用: {e}")
            return {
                "status": "error",
                "message": f"缺少训练依赖: {e}。请安装: pip install transformers peft accelerate bitsandbytes torch",
            }
