from dotenv import load_dotenv
load_dotenv()
import re
import os
import json
from pydantic import BaseModel
from typing import List, Dict, Any
from datetime import datetime
from hello_agents import SimpleAgent, HelloAgentsLLM
from hello_agents.tools import NoteTool
from prompt import CHAPTER_PROMPT, CHAPTER_REVIEW_PROMPT, CHAPTER_START_PROMPT


def extract_note_id(output: str) -> str:
    """从 NoteTool 的输出文本中提取 note_id"""
    match = re.search(r"ID:\s*(note_[0-9_]+)", output)
    if not match:
        raise ValueError(f"无法从输出解析 note_id:\n{output}")
    return match.group(1)


class MemoryItem(BaseModel):
    """记忆项数据结构"""
    node_id: str
    novel_id: str
    title: str
    content: str
    summary: str
    timestamp: datetime
    metadata: Dict[str, Any] = {}
    next_chapter_prediction: str = ""


class ChapterGenerateAgent:
    """具有上下文感知能力的 Agent"""

    def __init__(self, name: str, llm: HelloAgentsLLM = HelloAgentsLLM(), max_steps: int = 5, chapter_length: int = 3000, **kwargs):

        self.chapter_length = chapter_length
        self.max_steps = max_steps

        self.num_chapter_memories = kwargs.get("num_chapter_memories", 5)
        self.workspace = kwargs.get("workspace", "./outputs")
        self.note_tools: Dict[str, NoteTool] = {}
        
        self.generate_agent = SimpleAgent(name="章节生成助手", llm=llm, system_prompt='你是一位擅长长篇小说结构与文本细化的专业作者助理。')
        self.review_agent = SimpleAgent(name="章节审核助手", llm=llm, system_prompt='你是一位专业的小说审核助手，负责检查章节是否符合小说的结构和风格。')

        # 内存存储
        self.memories: Dict[str, List[MemoryItem]] = {}

    @staticmethod
    def extract_json_from_response(response: str) -> dict:
        """从模型输出中提取并解析 JSON"""
        # 尝试清理 Markdown 代码块标记
        clean_response = re.sub(r"```json\s*", "", response)
        clean_response = re.sub(r"```\s*$", "", clean_response)
        clean_response = clean_response.strip()
        
        try:
            return json.loads(clean_response)
        except json.JSONDecodeError as e:
            # 如果直接解析失败，尝试在文本中寻找第一个 { 和最后一个 }
            try:
                start = clean_response.find("{")
                end = clean_response.rfind("}")
                if start != -1 and end != -1:
                    json_str = clean_response[start : end + 1]
                    return json.loads(json_str)
            except Exception:
                pass
            raise ValueError(f"无法解析 JSON 响应: {response}") from e

    def _ensure_tool(self, novel_id: str, novel_title: str = None):
        if not self.note_tools.get(novel_id):
            if not novel_title:
                raise ValueError(f"Tool for novel_id {novel_id} not initialized and novel_title not provided.")
            self.note_tools[novel_id] = NoteTool(workspace=os.path.join(self.workspace, f"{novel_title}-{novel_id}", 'chapters'))

    def get_content_from_note(self, content: str) -> str:
        try:
            # 去除 YAML 前置元数据
            frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
            if frontmatter_match:
                content = content[frontmatter_match.end():].strip()
            
            # 去除标题（第一行如果是标题）
            lines = content.split('\n')
            if lines and lines[0].startswith('# '):
                content = '\n'.join(lines[1:]).strip()
            
            return content
        except:
            return content

    def get_memories(self, novel_id: str):
        """获取最近章节记忆"""
        if not hasattr(self.note_tools[novel_id], "notes_index"):
            self.note_tools[novel_id]._load_index()

        notes = self.note_tools[novel_id].notes_index.get("notes", [])

        # 筛选相关章节笔记
        chapter_notes = [
            n for n in notes
            if n.get("note_type") == "chapter" and str(novel_id) in n.get("title", "")
        ]

        # 获取最后 N 章
        recent_notes = chapter_notes[-self.num_chapter_memories:]

        for note in recent_notes:
            note_id = note.get("id")
            file_path = os.path.join(self.workspace, f"{note_id}.md")

            if os.path.exists(file_path):
                with open(file_path, "r", encoding="utf-8") as f:
                    content = f.read()

                content = self.get_content_from_note(content)
                self.memories[novel_id].append(MemoryItem(
                    node_id=str(note_id),
                    title=note.get("title", "未知章节").strip(),
                    content=content,
                    novel_id=str(novel_id),
                    summary=note['tags'][0]if note.get("tags") and note['tags'] else '',
                    timestamp=datetime.fromisoformat(note.get("created_at", datetime.now().isoformat()))
                ))

    def run(self, user_input: str, **kwargs) -> str:
        """运行 Agent"""
        # 小说id用来区分小说，命名可能会重复
        novel_id = kwargs.pop("novel_id", None)
        assert novel_id, "请提供小说ID"

        novel_title = kwargs.pop("novel_title", None)
        assert novel_title, "请提供小说标题"

        self._ensure_tool(novel_id, novel_title)

        if not self.memories.get(novel_id):
            self.memories[novel_id] = []
            self.get_memories(novel_id)

        # 1. 构建上下文
        outline = self.get_outline(novel_id)
        prev_chapter = self.get_prev_chapter(novel_id)
        prev_summaries = self.get_prev_summaries(novel_id)
        chapter_length = kwargs.get("chapter_length", self.chapter_length)
        context = self.get_prompt(outline, prev_chapter, prev_summaries, user_input, novel_id, chapter_length=chapter_length)
        
        # 2. 使用上下文调用 LLM
        steps = 0
        while steps < self.max_steps:
            steps += 1

            # 生成章节内容
            response = self.generate_agent.run(context)
            try:
                response_data = self.extract_json_from_response(response)
                # 检查是否包含必要字段
                if 'title' not in response_data or 'content' not in response_data or 'next_chapter_prediction' not in response_data or 'summary' not in response_data:
                    raise ValueError("JSON 响应缺少必要字段 'title' 或 'content' 或 'next_chapter_prediction' 或 'summary'")
            except ValueError as e:
                print(f"步骤 {steps} 生成的 JSON 解析错误：{e}")
                continue
            
            # 审核章节内容
            review_context = CHAPTER_REVIEW_PROMPT.format(
                outline=outline,
                prev_chapter=prev_chapter,
                prev_summaries=prev_summaries,
                chapter_content=response_data.get('content', '')
            )
            review_response = self.review_agent.run(review_context)

            # 检查审核结果
            if "【通过】" in review_response:
                break
            
            context = self.get_prompt(outline, prev_chapter, prev_summaries, user_input, novel_id, response_data, review_response, chapter_length=chapter_length)

        # 3. 保存章节到笔记
        create_output = self.note_tools[novel_id].run({
            "action": "create",
            "title": f"{response_data.get('title', '未知章节')}",
            "content": response_data.get('content', ''),
            "note_type": "chapter",
            "tags": [response_data.get('summary', '')]
        })

        # 获取章节笔记ID，保存记忆，并建立与小说ID的关联
        note_id = extract_note_id(create_output)

        self.memories[novel_id].append(MemoryItem(
            node_id=note_id,
            title=response_data.get('title', '未知章节'),
            content=response_data.get('content', ''),
            novel_id=novel_id,
            summary=response_data.get('summary', ''),
            timestamp=datetime.now().isoformat(),
            next_chapter_prediction=response_data.get('next_chapter_prediction', '')
        ))

        return response_data, note_id

    def get_prompt(self, outline: str, prev_chapter: str, prev_summaries: str, user_input: str, novel_id: str, response_data: dict = None, review_response: str = None, chapter_length: int = None) -> str:
        """获取章节生成提示"""
        if chapter_length is None:
            chapter_length = self.chapter_length
        is_first_chapter = (prev_chapter == '无' and prev_summaries == '无')

        if is_first_chapter:
            prompt_template = CHAPTER_START_PROMPT
            context = prompt_template.format(
                outline=outline,
                chapter_history='无' if response_data is None else response_data.get('content', '无'),
                evaluation=review_response or "无",
                user_input=user_input,
                chapter_length=chapter_length
            )
        else:
            prompt_template = CHAPTER_PROMPT
            context = prompt_template.format(
                outline=outline,
                prev_chapter=prev_chapter,
                prev_summaries=prev_summaries,
                chapter_history='无' if response_data is None else response_data.get('content', '无'),
                evaluation=review_response or "无",
                user_input=user_input or [self.memories[novel_id][-1].next_chapter_prediction if self.memories[novel_id] else "无"][0],
                chapter_length=chapter_length
            )
        return context

    def get_outline(self, novel_id: str) -> str:    
        """获取大纲"""
        dir_path = f"{os.path.dirname(self.note_tools[novel_id].workspace)}/outline"
        paths = os.listdir(dir_path)
        assert len(paths) >= 1, f"目录 {dir_path} 下应该有大纲文件"
        # 简单取第一个文件，实际可能需要更精确的逻辑
        path = f"{dir_path}/{paths[0]}"
        with open(path, "r", encoding='utf-8') as f:
            outline = f.read()
        return self.get_content_from_note(outline)

    def get_prev_chapter(self, novel_id: str):
        """获取前一章内容"""
        if self.memories.get(novel_id):
            last_mem = self.memories[novel_id][-1]
            return f"【{last_mem.metadata.get('title', '未知')}】\n...{last_mem.content[-800:]}"
        return "无"

    def get_prev_summaries(self, novel_id: str):
        if self.memories.get(novel_id):
            return "\n".join([f"【{mem.title}】\n{mem.summary}" for mem in self.memories[novel_id][-self.num_chapter_memories:]])
        return "无"
    
    def del_chapter(self, novel_id:str, note_id: str, novel_title: str = None):
        """删除章节"""
        if novel_title:
            self._ensure_tool(novel_id, novel_title)
        self.note_tools[novel_id].run({
            "action": "delete",
            "note_id": note_id
        })
        # 从记忆中删除该章节
        if self.memories.get(novel_id):
            self.memories[novel_id] = [mem for mem in self.memories[novel_id] if mem.node_id != note_id]

    def update_chapter(self, novel_id:str, note_id: str, novel_title: str = None, **kwargs):
        """更新章节"""
        if novel_title:
            self._ensure_tool(novel_id, novel_title)
        self.note_tools[novel_id].run({
            "action": "update",
            "note_id": note_id,
            **kwargs
        })
        # 更新记忆中的章节内容
        if self.memories.get(novel_id):
            for mem in self.memories[novel_id]:
                if mem.node_id == note_id:
                    mem.title = kwargs.get('title', mem.title)
                    mem.content = kwargs.get('content', mem.content)
                    mem.summary = kwargs.get('summary', mem.summary)
                    mem.next_chapter_prediction = kwargs.get('next_chapter_prediction', mem.next_chapter_prediction)
                    mem.timestamp = datetime.now().isoformat()
                    break

def main():
    print("=" * 80)
    print("Novel ChapterGenerateAgent 示例")
    print("=" * 80 + "\n")

    # llm = HelloAgentsLLM(model="qwen3:0.6b", api_key="ollama", base_url="http://127.0.0.1:11434/v1", provider='ollama')
    llm = HelloAgentsLLM(provider='qwen')
    novel_id = "demo_novel_001"
    novel_title = "记忆之城"

    # 1. 模拟大纲文件存在
    # 因为 ChapterGenerateAgent.get_outline 依赖于文件系统查找大纲
    # 我们手动创建一个假的大纲文件用于测试
    workspace_root = "./outputs"
    # 注意：这里模拟 OutlineAgent 的输出路径结构
    outline_dir = os.path.join(workspace_root, f"{novel_title}-{novel_id}", "outline")
    if not os.path.exists(outline_dir):
        os.makedirs(outline_dir)
    
    # 清理旧文件以确保测试环境干净
    for f in os.listdir(outline_dir):
        try:
            os.remove(os.path.join(outline_dir, f))
        except Exception:
            pass
        
    dummy_outline_content = """---
tags: [outline]
created_at: 2025-01-27T10:00:00
---
# 记忆之城-大纲

## 核心梗概
一位能与城市记忆对话的年轻人，在拆迁浪潮中发现一段被刻意抹去的历史。

## 主要人物
- 李寻：主角，拥有"读取"物体记忆的能力。
- 陈叔：古董店老板，似乎知道李寻身世的秘密。

## 故事走向
1. 觉醒能力，卷入拆迁冲突。
2. 发现神秘物品，引出旧事。
3. ...
"""
    dummy_outline_path = os.path.join(outline_dir, f"{novel_id}-outline.md")
    with open(dummy_outline_path, "w", encoding="utf-8") as f:
        f.write(dummy_outline_content)

    print(f"已创建模拟大纲文件: {dummy_outline_path}")
    
    # 2. 初始化章节生成 Agent
    chapter_agent = ChapterGenerateAgent(
        name="小说章节助手",
        llm=llm,
        workspace=workspace_root,  # 使用与 OutlineAgent 一致的根目录
        chapter_length=1000 # 演示用，设短一点
    )

    # 3. 生成第一章
    print(f"\n正在生成第一章...")
    try:
        # run 方法需要 novel_title 来定位目录
        chapter_data_1, note_id_1 = chapter_agent.run(
            user_input="第一章需要通过一个具体的拆迁冲突场景，引出主角的能力。主角李寻在试图保护一家老店不被强拆时，无意中听到了推土机的'心声'。",
            novel_id=novel_id,
            novel_title=novel_title 
        )
        print(f"第一章生成完成，Note ID: {note_id_1}")
        print(f"标题: {chapter_data_1.get('title')}")
        print(f"摘要: {chapter_data_1.get('summary')}")
        print(f"下一章预测: {chapter_data_1.get('next_chapter_prediction')}")

        # 4. 生成第二章（会自动读取第一章作为上下文）
        print(f"\n正在生成第二章...")
        chapter_data_2, note_id_2 = chapter_agent.run(
            user_input="主角在废墟中发现了一个奇怪的物品，触发了回忆。那个物品似乎在呼唤他。",
            novel_id=novel_id,
            novel_title=novel_title
        )
        print(f"第二章生成完成，Note ID: {note_id_2}")
        print(f"标题: {chapter_data_2.get('title')}")
        print(f"摘要: {chapter_data_2.get('summary')}")
        
    except Exception as e:
        print(f"生成过程中出错: {e}")
        import traceback
        traceback.print_exc()



if __name__ == "__main__":
    main()
