"""
深维面诊智能体 SSE 桥接适配器 (deepview_sse.py)
"""
import sys as _sys, os as _os
_this_dir = _os.path.dirname(_os.path.abspath(__file__))
if _this_dir in _sys.path:
    _sys.path.remove(_this_dir)
_sys.path.append(_this_dir)

_overlay_dir = _os.environ.get("HERMES_OVERLAY_DIR", _os.path.abspath(_os.path.join(_this_dir, "..", "..", "..", "..", "mindOSv2", "hermes-overlay")))
if not _os.path.isdir(_overlay_dir):
    # Production fallback: 实际位置（通过 find /opt/apps -name hermes-overlay 确认）
    _overlay_dir = "/opt/apps/mindos-next/hermes-overlay"

if _os.path.isdir(_overlay_dir) and _overlay_dir not in _sys.path:
    _sys.path.insert(0, _overlay_dir)

import asyncio
import json
import logging
import os
import time
import uuid
from typing import Any, Dict, Optional

try:
    from aiohttp import web, ClientSession, ClientTimeout
    AIOHTTP_AVAILABLE = True
except ImportError:
    AIOHTTP_AVAILABLE = False
    web = None

from platforms.sse_base.sse_server import BaseSSEServer, CORS_HEADERS
from platforms.sse_base import callback_atoms as atoms

logger = logging.getLogger(__name__)

_CORS_HEADERS = CORS_HEADERS

class DeepviewSSEServer(BaseSSEServer):
    def __init__(self, host: str = "127.0.0.1", port: int = 8653):
        super().__init__(prefix="/deepview", host=host, port=port)
        self._initMaterialsDb()
        self._initReportsDb()
        self._initProfilesDb()

    # ── V3 宪法管理员名单（与 MindOS NEXT 保持一致） ──
    _ADMIN_USER_IDS = {"11c1cece-2422-41e7-86f0-1f54b6162b95"}

    def register_routes(self, app):
        # 业务 API
        app.router.add_get(f"{self._prefix}/api/profile", self._handleProfileGet)
        app.router.add_post(f"{self._prefix}/api/profile", self._handleProfilePost)
        app.router.add_post(f"{self._prefix}/api/report/generate", self._handleReportGenerate)
        app.router.add_get(f"{self._prefix}/api/report/get", self._handleReportGet)
        app.router.add_get(f"{self._prefix}/api/reports/list", self._handleReportsList)
        app.router.add_post(f"{self._prefix}/api/materials/upload-token", self._handleMaterialsUploadToken)
        app.router.add_post(f"{self._prefix}/api/materials/confirm", self._handleMaterialsConfirm)
        app.router.add_get(f"{self._prefix}/api/materials/list", self._handleMaterialsList)
        app.router.add_get(f"{self._prefix}/api/clients/list", self._handleClientsList)
        app.router.add_post(f"{self._prefix}/api/client/create", self._handleClientCreate)
        app.router.add_get(f"{self._prefix}/api/client/profile", self._handleClientProfile)
        app.router.add_post(f"{self._prefix}/api/report/archive", self._handleReportArchive)
        app.router.add_get(f"{self._prefix}/api/clients/{{id}}/profile-report", self._handleClientProfileReport)
        # 积分与管理 API（复用 MindOS NEXT 标准实现）
        app.router.add_get(f"{self._prefix}/api/credits", self._handleCredits)
        app.router.add_get(f"{self._prefix}/api/admin/dashboard", self._handleAdminDashboard)
        app.router.add_post(f"{self._prefix}/api/admin/credits/topup", self._handleAdminTopup)

    def get_callback_atoms(self) -> list:
        return [atoms.make_stream_delta, atoms.make_tool_thinking]

    def get_hydrate_post_processors(self) -> list:
        """DeepView 后处理：过滤掉 tool/system 消息。

        DeepView 的产出物（报告/素材）走独立 DB 表，不需要文件水合。
        但需要过滤非可见角色的消息，避免泄漏 tool_calls 细节到前端。
        """
        from platforms.sse_base.hydrate_atoms import _filterVisibleMessages
        return [_filterVisibleMessages]

    def _get_platform_source(self) -> str:
        return "deepview"


    def _get_storage_root(self) -> str:
        """覆写基座方法：指向 Deepview 的 storage 目录。"""
        return os.getenv(
            "DEEPVIEW_STORAGE_DIR",
            os.path.join(
                os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
                "storage",
            ),
        )

    def _resolve_org_id(self, user: dict) -> str:
        """覆写基座方法：从 JWT 中提取 orgId。"""
        return user.get("org", user.get("orgId", "org_001"))

    def _extract_chat_params(self, body: dict, user: dict) -> dict:
        return {
            "contextId": body.get("contextId", "deepview"),
            "doctorId": body.get("doctorId", "doc_001")
        }

    def _getUserStorageDir(self, userId: str) -> str:
        storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
        userDir = os.path.join(storageDir, "users", userId)
        os.makedirs(userDir, exist_ok=True)
        return userDir

    def _getOrgStorageDir(self, orgId: str) -> str:
        storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
        orgDir = os.path.join(storageDir, "orgs", orgId)
        os.makedirs(orgDir, exist_ok=True)
        return orgDir

    async def _runChat(self, userId: str, chatId: str, text: str, contextId: str = "deepview", doctorId: str = "doc_001", **kwargs) -> None:
        try:
            from run_agent import AIAgent
            from hermes_state import SessionDB

            db = SessionDB()
            loop = asyncio.get_event_loop()

            credit_check = db.check_credits(userId)
            if not credit_check["allowed"]:
                self._pushEvent(userId, "agent:error", {
                    "chatId": chatId,
                    "message": "今日免费额度已用完，请联系管理员充值。",
                    "errorType": "quota_exceeded",
                    "credits": credit_check,
                })
                return

            _snap = db.get_session(chatId) or {}
            _snap_input = _snap.get("input_tokens") or 0
            _snap_output = _snap.get("output_tokens") or 0

            storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
            userDir = self._getUserStorageDir(userId)
            orgId = "org_001"
            orgDir = self._getOrgStorageDir(orgId)
            platformDir = os.path.join(storageDir, "platform")

            # SKILL.md 已通过三元域装配自动注入到 AIAgent 的 skills system prompt，
            # 此处只构建运行时上下文（SKILL.md 无法预知的动态信息）
            systemPrompt = ""

            if contextId.startswith("recording:"):
                recordingId = contextId.split(":", 1)[1]
                parts = recordingId.split("/")
                clientId = parts[0] if len(parts) > 1 else "unknown_client"
                asrId = parts[-1] 
                asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md"
                
                sp_chunks = [
                    "",
                    "## 🎙️ 当前上下文模式：单次面诊录音复盘",
                    f"- 录音 ASR 文件：{asrPath}",
                    f"- 医生风格档案：{userDir}/doctor_profile.md",
                    f"- 企业知识库目录：{orgDir}/wiki/",
                    f"- 平台规则参考：{platformDir}/wiki/",
                    "",
                    "### 你的工作重心",
                    "基于这一次面诊录音，分析信任断点、沟通体征、改进建议。",
                    "不要读取该客户的完整 profile.md（那是全景档案模式的职责）。",
                    ""
                ]
                systemPrompt += "\n".join(sp_chunks)
            elif contextId.startswith("client:"):
                clientId = contextId.split(":", 1)[1]
                sp_chunks = [
                    "",
                    "## 👤 当前上下文模式：客户全景档案",
                    f"- 客户档案目录：{userDir}/clients/{clientId}/",
                    f"- 核心档案：{userDir}/clients/{clientId}/profile.md",
                    f"- 历史录音目录：{userDir}/clients/{clientId}/history/",
                    f"- 医生风格档案：{userDir}/doctor_profile.md",
                    f"- 企业知识库目录：{orgDir}/wiki/",
                    f"- 平台规则参考：{platformDir}/wiki/",
                    "",
                    "### 你的工作重心",
                    "基于该客户的全生命周期数据，提供诊前策略、社交杠杆分析、跨品类破冰方案。",
                    "优先读取 profile.md 获取全景概览，必要时深入 history/ 追溯原始录音细节。",
                    ""
                ]
                systemPrompt += "\n".join(sp_chunks)
            else:
                systemPrompt += f"\n\n## 通用模式\n企业知识库目录：{orgDir}/wiki/\n平台规则参考：{platformDir}/wiki/\n"

            systemPrompt += "\n\n【严格输出规范】\n严禁在最终回答中输出任何 JSON 结构、工具调用过程记录、文件读取错误等底层调试信息。你必须将系统结果和发现转化为专业、流畅的文档级 Markdown 给医生阅读！\n"

            history = db.get_messages_as_conversation(chatId)
            stream_cb, tool_cb, reasoning_cb = self._makeCallbacks(userId, chatId, loop)

            agent = AIAgent(
                model=os.getenv("DEEPVIEW_MODEL", "gemini-pro-vertex"),
                base_url=os.getenv("OPENAI_API_BASE", ""),
                api_key=os.getenv("OPENAI_API_KEY", ""),
                enabled_toolsets=["file"],
                stream_delta_callback=stream_cb,
                tool_progress_callback=tool_cb,
                reasoning_callback=reasoning_cb,
                quiet_mode=True,
                platform="deepview",
                session_id=chatId,
                session_db=db,
                user_id=userId,
            )

            result = await self._run_agent_task(
                lambda: agent.run_conversation(
                    user_message=text,
                    system_message=systemPrompt,
                    conversation_history=history,
                ),
                userId, chatId
            )

            finalResponse = result.get("final_response", "")
            if not finalResponse:
                finalResponse = result.get("error", "(未生成回答)")

            _MODEL_RATES = {
                "deepseek":          (1, 2),
                "qwen-plus":         (4, 12),
                "gemini-pro-vertex": (9, 36),
            }
            try:
                session_info = db.get_session(chatId)
                if session_info:
                    delta_input = max(0, (session_info.get("input_tokens") or 0) - _snap_input)
                    delta_output = max(0, (session_info.get("output_tokens") or 0) - _snap_output)
                    effective_model = session_info.get("model") or "qwen-plus"
                    in_rate, out_rate = _MODEL_RATES.get(effective_model, (4, 12))
                    credits_used = max(1, (delta_input * in_rate + delta_output * out_rate) // 1000)
                    db.deduct_credits(
                        user_id=userId, credits=credits_used, tx_type="llm_chat",
                        session_id=chatId, model=effective_model,
                        raw_metric=json.dumps({
                            "delta_input": delta_input, "delta_output": delta_output,
                            "in_rate": in_rate, "out_rate": out_rate,
                        }),
                    )
            except Exception as _ce:
                logger.warning("[DeepviewSSE] credit deduction failed (non-fatal): %s", _ce)

            self._pushEvent(userId, "agent:done", {
                "chatId": chatId,
                "fullAnswer": finalResponse,
            })

        except Exception as e:
            logger.error("[DeepviewSSE] Chat error for %s/%s: %s", userId, chatId, e, exc_info=True)
            self._pushEvent(userId, "agent:error", {
                "chatId": chatId,
                "message": f"处理失败：{str(e)}",
            })

    def _initProfilesDb(self) -> None:
        """确保 deepview_user_profiles 表存在，用于存储额外的用户档案"""
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                db._conn.execute("""
                    CREATE TABLE IF NOT EXISTS deepview_user_profiles (
                        user_id TEXT PRIMARY KEY,
                        real_name TEXT,
                        company TEXT,
                        role TEXT,
                        voice_url TEXT,
                        updated_at REAL NOT NULL
                    )
                """)
                # Try to add real_name column if it's missing (for upgrade)
                try:
                    db._conn.execute("ALTER TABLE deepview_user_profiles ADD COLUMN real_name TEXT")
                except:
                    pass
                db._conn.commit()
        except Exception as e:
            logger.warning("[DeepviewSSE] Failed to init profiles table: %s", e)

    async def _handleProfileGet(self, request: "web.Request") -> "web.Response":
        """GET /deepview/profile"""
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
        
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                row = db._conn.execute(
                    "SELECT real_name, company, role, voice_url FROM deepview_user_profiles WHERE user_id = ?",
                    (user["userId"],)
                ).fetchone()
            
            if row:
                return web.json_response({
                    "realName": row[0],
                    "company": row[1], 
                    "role": row[2], 
                    "voiceUrl": row[3]
                }, headers=_CORS_HEADERS)
            else:
                return web.json_response({
                    "realName": "",
                    "company": "", 
                    "role": "", 
                    "voiceUrl": ""
                }, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Failed to get profile: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleProfilePost(self, request: "web.Request") -> "web.Response":
        """POST /deepview/profile"""
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
        
        try:
            data = await request.json()
            real_name = data.get("realName", "")
            company = data.get("company", "")
            role = data.get("role", "")
            voice_url = data.get("voiceUrl", "")
            
            from hermes_state import SessionDB
            import time
            db = SessionDB()
            with db._lock:
                db._conn.execute("""
                    INSERT INTO deepview_user_profiles (user_id, real_name, company, role, voice_url, updated_at)
                    VALUES (?, ?, ?, ?, ?, ?)
                    ON CONFLICT(user_id) DO UPDATE SET
                        real_name=excluded.real_name,
                        company=excluded.company,
                        role=excluded.role,
                        voice_url=excluded.voice_url,
                        updated_at=excluded.updated_at
                """, (user["userId"], real_name, company, role, voice_url, time.time()))
                db._conn.commit()
            return web.json_response({"success": True}, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Failed to set profile: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleReportGenerate(self, request: "web.Request") -> "web.Response":
        """
        POST /deepview/report/generate
        Body: { contextId, doctorId? }
        同步生成 JSON 报告，包含最多 3 次重试逻辑。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            body = await request.json()
        except Exception:
            return web.json_response({"error": "Invalid JSON"}, status=400, headers=_CORS_HEADERS)

        import uuid
        userId = user["userId"]
        contextId = body.get("contextId", "")
        doctorId = body.get("doctorId", "doc_001")
        presetReportId = body.get("presetReportId")

        if not contextId:
            return web.json_response({"error": "Missing contextId"}, status=400, headers=_CORS_HEADERS)
            
        userObj = await self._extractUser(request)
        userId = userObj["userId"] if userObj else "unknown"
        userDir = self._getUserStorageDir(userId)
        
        # 解析 orgId
        orgId = userObj.get("org", "org_001") if userObj else "org_001"
        orgDir = self._getOrgStorageDir(orgId)
        
        storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
        platformDir = os.path.join(storageDir, "platform")
        
        # SKILL.md 已通过三元域装配自动注入，此处只构建运行时上下文
        systemPrompt = f"\n\n## 运行时上下文\n企业知识库：{orgDir}/wiki/\n平台规则：{platformDir}/wiki/\n"

        if contextId.startswith("recording:"):
            recordingId = contextId.split(":", 1)[1]
            # Inbox 模式：ASR 文件在 inbox/{reportId}/ 下
            # 也兼容旧格式 recording:clientId/asrId
            parts = recordingId.split("/")
            if len(parts) > 1:
                # 旧格式（已归档）：recording:clientId/asrId
                clientId = parts[0]
                asrId = parts[-1]
                asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md"
                clientProfilePath = f"{userDir}/clients/{clientId}/profile.md"
                reportDraftPath = f"{userDir}/clients/{clientId}/history/{asrId}/report_draft.md"
            else:
                # 新格式（Inbox）：recording:asrId
                clientId = None
                asrId = parts[0]
                asrPath = f"{userDir}/inbox/{asrId}/asr.md"
                clientProfilePath = None
                reportDraftPath = f"{userDir}/inbox/{asrId}/report_draft.md"
            systemPrompt += f"\n## 🎙️ 第1模式：单条面诊录音复盘\n你的唯一任务是提取基于这通录音的战术复盘。\n录音路径：{asrPath}\n医生档案参考：{userDir}/doctor_profile.md"
            systemPrompt += f"\n\n📄 【输出要求】将完整的分析报告写入以下路径：{reportDraftPath}"
            systemPrompt += f"\n文件第一行必须为来源标注：<!-- provenance: ai-single | level: L1 | sources: [{os.path.basename(os.path.dirname(asrPath))}/asr.md] -->"
            if clientProfilePath and os.path.exists(clientProfilePath):
                systemPrompt += f"\n客户档案参考：{clientProfilePath}"
            systemPrompt += f"\n\n🚨 【系统硬约束：Speaker 天然推断指令】"
            systemPrompt += f"\n原生录音 ASR 中仅含有 Speaker_1、Speaker_2 等无感情编号。请根据对话上下文（谁在做专业科普与处方，谁在表述容貌焦虑与顾虑）自然反演出真实的医生和客户对应关系。"
            systemPrompt += f"\n在输出所有报告正文时，请直接使用真实姓名，彻底抹除 Speaker_X 痕迹。"
        else:
            clientId = contextId.split(":", 1)[1] if ":" in contextId else contextId
            reportDraftPath = None  # 全景档案模式由 _generateClientProfile 处理
            systemPrompt += f"\n## 👤 第2模式：客户全景档案战略\n你的唯一任务是生成该客户的全景战略洞察报告。\n档案路径：{userDir}/clients/{clientId}/profile.md\n历史录音目录：{userDir}/clients/{clientId}/history/\n医生风格：{userDir}/doctor_profile.md"
        # 从 pipeline/deepview_xray 加载 Stage1 Prompt
        try:
            skill_dir = os.path.join(os.path.dirname(__file__), "..", "..", "pipeline", "deepview_xray")
            with open(os.path.join(skill_dir, "PROMPT_stage1.md"), "r", encoding="utf-8") as f:
                stage1PromptTemplate = f.read()
        except Exception as e:
            logger.error(f"[DeepviewSSE] Cannot read deepview_xray/PROMPT_stage1.md: {e}")
            stage1PromptTemplate = "请按照 SKILL.md 的组件库要求生成报告。"
            
        stage1UserPrompt = stage1PromptTemplate.replace("{historyDir}", f"{userDir}/clients/{clientId}/history" if clientId else f"{userDir}/inbox/{asrId}")
        stage1UserPrompt = stage1UserPrompt.replace("{reportMdPath}", reportDraftPath if reportDraftPath else "输出目录")
        stage1UserPrompt = stage1UserPrompt.replace("{sourceRecordings}", contextId)


        from run_agent import AIAgent
        from hermes_state import SessionDB
        db = SessionDB()
        loop = asyncio.get_event_loop()

        try:
            agent = AIAgent(
                model=os.getenv("DEEPVIEW_MODEL", "gemini-pro-vertex"),
                enabled_toolsets=["file"],
                quiet_mode=True,
                platform="deepview_xray",
                session_id=str(uuid.uuid4()),
                session_db=db,
                user_id=userId,
            )
            
            userMsg = stage1UserPrompt

            result = await loop.run_in_executor(
                None,
                lambda: agent.run_conversation(
                    user_message=userMsg,
                    system_message=systemPrompt,
                ),
            )

            # md-first: 优先从 Agent 写入的物理文件读取，兜底 final_response
            if reportDraftPath and os.path.exists(reportDraftPath):
                with open(reportDraftPath, "r", encoding="utf-8") as f:
                    mdReport = f.read().strip()
                logger.info(f"[DeepviewSSE] Stage 1 done (from report_draft.md), length={len(mdReport)} chars")
            else:
                mdReport = result.get("final_response", "").strip()
                logger.info(f"[DeepviewSSE] Stage 1 done (from final_response), length={len(mdReport)} chars")
                # 兜底：如果 Agent 返回了内容但未写文件，帮它落盘（审计留痕）
                if reportDraftPath and mdReport and len(mdReport) > 100:
                    os.makedirs(os.path.dirname(reportDraftPath), exist_ok=True)
                    with open(reportDraftPath, "w", encoding="utf-8") as f:
                        f.write(f"<!-- provenance: ai-single | level: L1 | sources: [{contextId}] -->\n")
                        f.write(mdReport)
                    logger.info(f"[DeepviewSSE] Fallback: wrote report_draft.md from final_response")

            if not mdReport or len(mdReport) < 100:
                return web.json_response({"error": "Stage 1 报告内容为空"}, status=500, headers=_CORS_HEADERS)

        except Exception as e:
            logger.error(f"[DeepviewSSE] Stage 1 error: {e}")
            return web.json_response({"error": f"Stage 1 failed: {e}"}, status=500, headers=_CORS_HEADERS)

        # ── Stage 2 格式引擎: qwen3-plus via LiteLLM (JSON Schema 硬约束) ──
        # 读取外置 Stage2 Prompt 和 Schema
        try:
            skill_dir = os.path.join(os.path.dirname(__file__), "..", "..", "pipeline", "deepview_xray")
            with open(os.path.join(skill_dir, "PROMPT_stage2.md"), "r", encoding="utf-8") as f:
                stage2SystemPrompt = f.read()
            import json
            with open(os.path.join(skill_dir, "SCHEMA_xray.json"), "r", encoding="utf-8") as f:
                jsonSchema = json.load(f)
        except Exception as e:
            logger.error(f"[DeepviewSSE] Cannot read deepview_xray Stage2 config: {e}")
            stage2SystemPrompt = "你是一个 JSON 转换器。"
            jsonSchema = {}

        try:
            from openai import OpenAI
            litellmClient = OpenAI(
                base_url=os.getenv("GEMINI_BASE_URL", "http://127.0.0.1:4000/v1"),
                api_key=os.getenv("GEMINI_API_KEY", "sk-placeholder"),
            )
            
            stage2Response = await loop.run_in_executor(
                None,
                lambda: litellmClient.chat.completions.create(
                    model=os.getenv("DEEPVIEW_STAGE2_MODEL", "qwen-plus"),
                    messages=[
                        {"role": "system", "content": stage2SystemPrompt},
                        {"role": "user", "content": mdReport}
                    ],
                    response_format={"type": "json_object"},
                    max_tokens=8192,
                    user=userId,
                    extra_body={
                        "metadata": {
                            "deepview_task": contextId,
                            "deepview_stage": "stage2_json",
                            "deepview_user": userId,
                        }
                    },
                )
            )
            
            jsonOutput = stage2Response.choices[0].message.content.strip()
            parsedData = json.loads(jsonOutput)

            logger.info(f"[DeepviewSSE] Stage 2 (json_object) done, keys={list(parsedData.keys())}")
            xray = parsedData.get('xray', {})
            for mk in ['module1', 'module2', 'module3', 'module4', 'module5']:
                logger.info(f"[DeepviewSSE] xray.{mk}: {'✅' if xray.get(mk) else '❌ MISSING'}")

            # ── 持久化：写入 DB（hermes state.db）──
            import uuid
            reportId = presetReportId if presetReportId else "rep_" + uuid.uuid4().hex[:8]
            
            # Programmatically inject the real report identifier instead of hallucinated one
            parsedData['reportCode'] = f"DW-AMXG-{reportId[4:].upper()}"
            parsedData['id'] = reportId
            parsedData['context_id'] = contextId

            
            try:
                from hermes_state import SessionDB
                reportDb = SessionDB()
                reportJson = json.dumps(parsedData, ensure_ascii=False)
                with reportDb._lock:
                    if presetReportId:
                        reportDb._conn.execute(
                            "UPDATE deepview_reports_v2 SET status='completed', report_json=?, created_at=? WHERE report_id=?",
                            (reportJson, time.time(), presetReportId)
                        )
                    else:
                        reportDb._conn.execute(
                            "INSERT OR REPLACE INTO deepview_reports_v2 (report_id, context_id, user_id, report_json, created_at, status) VALUES (?, ?, ?, ?, ?, 'completed')",
                            (reportId, contextId, userId, reportJson, time.time())
                        )
                    reportDb._conn.commit()
                logger.info(f"[DeepviewSSE] Report persisted to DB: {contextId} -> {reportId}")
            except Exception as dbErr:
                logger.error(f"[DeepviewSSE] DB persist failed: {dbErr}")

            safeBody = json.dumps({"success": True, "reportId": reportId, "data": parsedData}, ensure_ascii=False)
            return web.Response(text=safeBody, content_type="application/json", headers=_CORS_HEADERS)

        except Exception as e:
            logger.error(f"[DeepviewSSE] Stage 2 error: {e}")
            return web.json_response({"error": f"Stage 2 (json conversion) failed: {e}"}, status=500, headers=_CORS_HEADERS)

    async def _handleReportGet(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/report/get?reportId=rep_xxx
        从 DB 读取已持久化的报告 JSON。
        """
        if request.method == "OPTIONS":
            return web.Response(status=200, headers=_CORS_HEADERS)
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        reportId = request.query.get("reportId", "")
        if not reportId:
            return web.json_response({"error": "Missing reportId"}, status=400, headers=_CORS_HEADERS)

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                cursor = db._conn.execute(
                    "SELECT report_json, created_at, client_id, context_id FROM deepview_reports_v2 WHERE report_id = ?",
                    (reportId,)
                )
                row = cursor.fetchone()

            if not row:
                return web.json_response({"error": "Report not found", "reportId": reportId}, status=404, headers=_CORS_HEADERS)

            reportData = json.loads(row[0])
            clientId = row[2]
            contextId = row[3]
            
            # Hot patch reportCode for older generated reports that might have hallucinated ones
            reportData["reportCode"] = f"DW-AMXG-{reportId[4:].upper()}"
            reportData["id"] = reportId
            reportData["context_id"] = contextId
            
            # Attach clientId into the data payload so the frontend knows if it's archived
            if clientId:
                reportData["clientId"] = clientId

            safeBody = json.dumps({"success": True, "data": reportData}, ensure_ascii=False)
            return web.Response(text=safeBody, content_type="application/json", headers=_CORS_HEADERS)
        except Exception as e:
            logger.error(f"[DeepviewSSE] Report get error: {e}")
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleReportsList(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/reports/list
        返回当前用户所有的面诊报告，按时间倒序排列。包含生成中的占位记录。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                cursor = db._conn.execute(
                    "SELECT report_id, context_id, report_json, created_at, status, client_id FROM deepview_reports_v2 WHERE user_id = ? ORDER BY created_at DESC",
                    (user["userId"],)
                )
                rows = cursor.fetchall()
                
            reports = []
            for row in rows:
                rep_id, ctx_id, r_json, c_time, status, client_id = row
                try:
                    data = json.loads(r_json)
                except:
                    data = {}
                reports.append({
                    "id": rep_id,
                    "contextId": ctx_id,
                    "status": status,
                    "createdAt": c_time,
                    "clientId": client_id,
                    "data": data
                })

            return web.json_response({"success": True, "reports": reports}, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error(f"[DeepviewSSE] Reports list error: {e}")
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleMaterialsUploadToken(self, request: "web.Request") -> "web.Response":
        """
        POST /deepview/materials/upload_token
        Get a pre-signed URL for direct OSS upload.
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            body = await request.json()
        except:
            body = {}
            
        filename = body.get("filename", "unknown.pdf")
        import os
        ext = os.path.splitext(filename)[1].lower()
        import uuid
        file_uid = uuid.uuid4().hex
        object_key = f"deepview-raw/{user['userId']}/{file_uid}{ext}"

        oss_ak = os.getenv("ALIYUN_ACCESS_KEY_ID", "")
        oss_sk = os.getenv("ALIYUN_ACCESS_KEY_SECRET", "")
        oss_bucket = os.getenv("OSS_BUCKET", "meetings-dev")
        oss_endpoint = os.getenv("OSS_ENDPOINT", "oss-cn-beijing.aliyuncs.com")

        if not all([oss_ak, oss_sk]):
            # 降级：如果服务器没配 OSS 参数，或者本地测试，可能直接失败
            return web.json_response({"error": "Server missing OSS credentials (ALIYUN_ACCESS_KEY_ID)"}, status=500, headers=_CORS_HEADERS)

        import oss2
        auth = oss2.Auth(oss_ak, oss_sk)
        bucket = oss2.Bucket(auth, oss_endpoint, oss_bucket)
        
        # 必须显式指定 Content-Type 来生成签名，否则前端带默认 MIME type 会导致 OSS 拒签 403
        headers = {'Content-Type': 'application/octet-stream'}
        put_url = bucket.sign_url("PUT", object_key, 3600, headers=headers)
        if put_url.startswith("http://"):
            put_url = put_url.replace("http://", "https://", 1)
        
        return web.json_response({
            "putUrl": put_url,
            "ossKey": object_key,
            "filename": filename
        }, headers=_CORS_HEADERS)

    async def _handleMaterialsConfirm(self, request: "web.Request") -> "web.Response":
        """
        POST /deepview/materials/confirm
        Frontend calls this after OSS PUT succeeds.
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
            
        try:
            body = await request.json()
        except:
            return web.json_response({"error": "Invalid JSON"}, status=400, headers=_CORS_HEADERS)
            
        oss_key = body.get("ossKey")
        context_id = body.get("contextId", "deepview")
        filename = body.get("filename", "unknown.pdf")
        
        if not oss_key:
            return web.json_response({"error": "Missing ossKey"}, status=400, headers=_CORS_HEADERS)

        # Offload to background task
        try:
            from .deepview_materials import process_uploaded_material_oss
        except ImportError:
            from gateway.platforms.deepview_materials import process_uploaded_material_oss
            
        import uuid
        import time
        report_id = "rep_" + uuid.uuid4().hex[:8]
        
        # INSERT placeholder INTO deepview_reports_v2
        if context_id.startswith("recording:"):
            try:
                from hermes_state import SessionDB
                db = SessionDB()
                with db._lock:
                    db._conn.execute(
                        "INSERT INTO deepview_reports_v2 (report_id, context_id, user_id, report_json, created_at, status) VALUES (?, ?, ?, '{}', ?, 'processing')",
                        (report_id, context_id, user["userId"], time.time())
                    )
                    db._conn.commit()
            except Exception as e:
                logger.error(f"Failed to create processing report placeholder: {e}")

        async def _wrapper():
            # 1. ASR stage
            await process_uploaded_material_oss(
                oss_key=oss_key,
                original_filename=filename,
                context_id=context_id,
                push_event_fn=self._pushEvent,
                user_id=user["userId"],
                org_id=user.get("org", "org_001")
            )
            # 2. X-ray stage (Auto orchestrate)
            if context_id.startswith("recording:"):
                import sys, os
                try:
                    port = int(os.environ.get('DEEPVIEW_SSE_PORT', 8653))
                    import aiohttp
                    async with aiohttp.ClientSession() as session:
                        url = f"http://127.0.0.1:{port}/deepview/api/report/generate"
                        headers = {"Authorization": request.headers.get("Authorization", "")}
                        logger.info(f"[DeepviewSSE] Auto-triggering report generation internally for {context_id} -> {report_id}")
                        async with session.post(url, json={"contextId": context_id, "doctorId": "doc_001", "presetReportId": report_id}, headers=headers) as resp:
                            res_json = await resp.json()
                            if res_json.get("success"):
                                self._pushEvent(user["userId"], "report:ready", {"reportId": report_id})
                            else:
                                logger.error(f"[DeepviewSSE] Internal generate failed: {res_json}")
                                # Mark as failed in DB
                                from hermes_state import SessionDB
                                db = SessionDB()
                                with db._lock:
                                    db._conn.execute("UPDATE deepview_reports_v2 SET status='failed' WHERE report_id=?", (report_id,))
                                    db._conn.commit()
                except Exception as e:
                    logger.error(f"[DeepviewSSE] Wrapper pipeline failed: {e}")
        
        asyncio.create_task(_wrapper())

        return web.json_response({"received": True, "reportId": report_id if context_id.startswith("recording:") else None}, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 会话列表 API
    # ──────────────────────────────────────────────

    def _initReportsDb(self) -> None:
        """确保 deepview_reports 表存在（复用 Hermes state.db）。"""
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                db._conn.execute("""
                    CREATE TABLE IF NOT EXISTS deepview_reports_v2 (
                        report_id TEXT PRIMARY KEY,
                        context_id TEXT NOT NULL,
                        user_id TEXT NOT NULL,
                        report_json TEXT NOT NULL,
                        created_at REAL NOT NULL,
                        status TEXT DEFAULT 'completed',
                        client_id TEXT DEFAULT NULL
                    )
                """)
                cursor = db._conn.execute("PRAGMA table_info(deepview_reports_v2)")
                columns = [row[1] for row in cursor.fetchall()]
                if 'status' not in columns:
                    db._conn.execute("ALTER TABLE deepview_reports_v2 ADD COLUMN status TEXT DEFAULT 'completed'")
                if 'client_id' not in columns:
                    db._conn.execute("ALTER TABLE deepview_reports_v2 ADD COLUMN client_id TEXT DEFAULT NULL")
                    
                db._conn.execute("""
                    CREATE INDEX IF NOT EXISTS idx_deepview_reports_v2_user
                    ON deepview_reports_v2(user_id)
                """)
                db._conn.commit()
            logger.info("[DeepviewSSE] Reports table ready")
        except Exception as e:
            logger.warning("[DeepviewSSE] Failed to init reports table: %s", e)

    # ──────────────────────────────────────────────
    # 积分与管理 API（复用 MindOS NEXT 标准实现）
    # ──────────────────────────────────────────────

    async def _requireAdmin(self, request):
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
        if user["userId"] not in self._ADMIN_USER_IDS:
            return web.json_response({"error": "Forbidden"}, status=403, headers=_CORS_HEADERS)
        return user

    async def _handleCredits(self, request):
        """GET /deepview/api/credits"""
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            credits = db.check_credits(user["userId"])
            return web.json_response(credits, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Failed to check credits: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleAdminDashboard(self, request):
        """GET /deepview/api/admin/dashboard?days=7"""
        result = await self._requireAdmin(request)
        if isinstance(result, web.Response):
            return result
        try:
            days = int(request.query.get("days", "7"))
            from hermes_state import SessionDB
            db = SessionDB()
            dashboard = db.admin_dashboard(days=days)
            return web.json_response(dashboard, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Admin dashboard error: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    async def _handleAdminTopup(self, request):
        """POST /deepview/api/admin/credits/topup"""
        result = await self._requireAdmin(request)
        if isinstance(result, web.Response):
            return result
        try:
            body = await request.json()
            target_user = body.get("userId")
            amount = int(body.get("amount", 0))
            reason = body.get("reason", "admin_topup")
            if not target_user or amount <= 0:
                return web.json_response(
                    {"error": "需要 userId 和正整数 amount"}, status=400, headers=_CORS_HEADERS
                )
            from hermes_state import SessionDB
            db = SessionDB()
            updated = db.admin_topup(target_user, amount, reason)
            logger.info("[Admin] topup userId=%s amount=%d reason=%s", target_user, amount, reason)
            return web.json_response({"ok": True, "credits": updated}, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Admin topup error: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 素材文件持久化
    # ──────────────────────────────────────────────

    # ★ 驨总基础知识库文件（预填充到 DB，统一管理）
    _DEEPVIEW_BASE_FILES = [
        ("xz_0", "00_前言.md", "deepview"),
        ("xz_1", "01_第一章 品牌机会论证.md", "deepview"),
        ("xz_2", "02_第二章 原点市场与产品概念.md", "deepview"),
        ("xz_3", "03_第三章 品牌九要素之语言表达.md", "deepview"),
        ("xz_4", "04_第四章  品牌九要素之视觉表达.md", "deepview"),
        ("xz_5", "05_第五章  产品设计、广告与公关建品牌.md", "deepview"),
        ("xz_6", "06_第六章  新产品上市之品牌试错期.md", "deepview"),
        ("xz_7", "07_第七章 品牌的增长.md", "deepview"),
        ("xz_8", "08_第八章 连锁合作谈判.md", "deepview"),
        ("xz_9", "09_第九章  打赢商战.md", "deepview"),
    ]

    def _initMaterialsDb(self) -> None:
        """确保 deepview_materials 表存在并预填基础文件。"""
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                db._conn.execute("""
                    CREATE TABLE IF NOT EXISTS deepview_materials (
                        id TEXT PRIMARY KEY,
                        filename TEXT NOT NULL,
                        context_id TEXT DEFAULT 'deepview',
                        user_id TEXT,
                        source TEXT DEFAULT 'base',
                        created_at REAL NOT NULL
                    )
                """)
                db._conn.execute("""
                    CREATE INDEX IF NOT EXISTS idx_deepview_materials_ctx
                    ON deepview_materials(context_id)
                """)
                # 预填基础知识库文件
                for fid, fname, ctx in self._DEEPVIEW_BASE_FILES:
                    db._conn.execute(
                        "INSERT OR IGNORE INTO deepview_materials (id, filename, context_id, source, created_at) "
                        "VALUES (?, ?, ?, 'base', ?)",
                        (fid, fname, ctx, 0)  # created_at=0 表示基础文件
                    )
                db._conn.commit()
            logger.info("[DeepviewSSE] Materials table ready")
        except Exception as e:
            logger.warning("[DeepviewSSE] Failed to init materials table: %s", e)

    async def _handleMaterialsList(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/materials/list?contextId=deepview

        返回指定 context 下的所有素材文件（基础 + 用户上传）。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        contextId = request.query.get("contextId", "deepview")

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                cursor = db._conn.execute(
                    "SELECT id, filename, context_id, source "
                    "FROM deepview_materials WHERE context_id = ? ORDER BY created_at ASC",
                    (contextId,)
                )
                rows = cursor.fetchall()

            files = []
            for r in rows:
                row = dict(r)
                files.append({
                    "id": row["id"],
                    "name": row["filename"],
                    "projectId": row["context_id"],
                    "source": row["source"],
                })

            return web.json_response({"files": files}, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error("Failed to list materials: %s", e)
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 客户信息 API (Phase 4)
    # ──────────────────────────────────────────────

    async def _handleClientsList(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/clients/list
        返回所有客户记录列表。扫描 storage/clients 目录。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                cursor = db._conn.execute(
                    "SELECT c.client_id, c.name, c.phone, c.created_at, c.updated_at, p.profile_json "
                    "FROM deepview_clients c "
                    "LEFT JOIN deepview_client_profiles p ON c.client_id = p.client_id "
                    "WHERE c.user_id=? ORDER BY c.updated_at DESC", 
                    (user["userId"],)
                )
                rows = cursor.fetchall()
            
            clients = []
            for row in rows:
                c_id, c_name, c_phone, c_created, c_updated, p_json = row
                
                ltcStatus = None
                if p_json:
                    try:
                        profile = json.loads(p_json)
                        topics = profile.get("nextVisitBrief", {}).get("topicsToPrepare", [])
                        risks = profile.get("nextVisitBrief", {}).get("keyRisks", [])
                        ltcStatus = {
                            "topics": topics,
                            "risks": risks
                        }
                    except:
                        pass

                clients.append({
                    "id": c_id,
                    "name": c_name,
                    "phone": c_phone,
                    "createdAt": c_created,
                    "updatedAt": c_updated,
                    "ltcStatus": ltcStatus
                })

            return web.json_response({"clients": clients}, headers=_CORS_HEADERS)
        except Exception as e:
            logger.error(f"[DeepviewSSE] DB Clients list failed: {e}")
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)
    async def _handleClientCreate(self, request: "web.Request") -> "web.Response":
        """
        POST /deepview/clients/create
        动态建档
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
            
        try:
            body = await request.json()
        except:
            return web.json_response({"error": "Invalid JSON"}, status=400, headers=_CORS_HEADERS)
            
        name = body.get("name", "").strip()
        phone = body.get("phone", "").strip()
        
        if not name:
            return web.json_response({"error": "Name is required"}, status=400, headers=_CORS_HEADERS)
            
        import uuid
        import time
        clientId = f"p_{str(uuid.uuid4())[:8]}"
        
        userDir = self._getUserStorageDir(user["userId"])
        clientDir = os.path.join(userDir, "clients", clientId)
        os.makedirs(clientDir, exist_ok=True)
        
        profileData = f"# 客户基本档案\n姓名：{name}\n手机号/尾号：{phone}\n建档时间：{time.strftime('%Y-%m-%d')}\n\n## 标签\n- 新客\n\n## AI 沉淀洞察\n- 暂无\n"
        with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f:
            f.write(profileData)
            
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                db._conn.execute(
                    "INSERT INTO deepview_clients (client_id, user_id, name, phone) VALUES (?, ?, ?, ?)",
                    (clientId, user["userId"], name, phone)
                )
                db._conn.commit()
            logger.info(f"[DeepviewSSE] Created client {clientId} in DB")
        except Exception as e:
            logger.error(f"[DeepviewSSE] Failed to create client {clientId} in DB: {e}")
            
        return web.json_response({
            "id": clientId,
            "name": name,
            "phone": phone
        }, headers=_CORS_HEADERS)

    async def _handleClientProfile(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/clients/{id}/profile
        读取并返回某个客户的 profile.md。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
            
        clientId = request.match_info.get("id", "")
        if not clientId:
            return web.json_response({"error": "Missing client ID"}, status=400, headers=_CORS_HEADERS)

        userDir = self._getUserStorageDir(user["userId"])
        profilePath = os.path.join(userDir, "clients", clientId, "profile.md")
        
        content = ""
        if os.path.exists(profilePath):
            with open(profilePath, "r", encoding="utf-8") as f:
                content = f.read()
                
        return web.json_response({
            "id": clientId,
            "profileContent": content
        }, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 报告归档 (Inbox → Client)
    # ──────────────────────────────────────────────

    async def _handleReportArchive(self, request: "web.Request") -> "web.Response":
        """
        POST /deepview/reports/archive
        Body: { reportId, clientId }
        将 inbox 中的报告物理迁移到 clients/{clientId}/history/ 并更新 DB。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        try:
            body = await request.json()
        except:
            return web.json_response({"error": "Invalid JSON"}, status=400, headers=_CORS_HEADERS)

        reportId = body.get("reportId", "").strip()
        clientId = body.get("clientId", "").strip()

        if not reportId or not clientId:
            return web.json_response({"error": "Missing reportId or clientId"}, status=400, headers=_CORS_HEADERS)

        userId = user["userId"]
        userDir = self._getUserStorageDir(user["userId"])

        # 1. 从 DB 中查询真实的 context_id 从而获取 ASR ID，执行物理文件迁移
        import shutil
        from hermes_state import SessionDB
        db = SessionDB()
        
        with db._lock:
            ctx_row = db._conn.execute(
                "SELECT context_id FROM deepview_reports_v2 WHERE report_id=? AND user_id=?",
                (reportId, userId)
            ).fetchone()

        originalCtx = ctx_row[0] if ctx_row else ""
        inboxDir = None
        if originalCtx.startswith("recording:"):
            ctxPayload = originalCtx.split(":", 1)[1]
            if "/" not in ctxPayload:
                asrId = ctxPayload
                inboxDir = os.path.join(userDir, "inbox", asrId)
        
        targetDir = os.path.join(userDir, "clients", clientId, "history", reportId)

        if inboxDir and os.path.exists(inboxDir):
            os.makedirs(os.path.dirname(targetDir), exist_ok=True)
            shutil.move(inboxDir, targetDir)
            logger.info(f"[DeepviewSSE] Archived {inboxDir} → {targetDir}")
        else:
            logger.warning(f"[DeepviewSSE] Inbox dir not found or already migrated for {reportId}, skip file migration")

        # 2. DB 更新：标记 client_id + 更新 context_id
        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                # Update report records to associate with client
                cursor = db._conn.execute("SELECT report_json FROM deepview_reports_v2 WHERE report_id=? AND user_id=?", (reportId, userId))
                row = cursor.fetchone()
                
                # Update clientName directly into the json to avoid joins in reports/list and report/get
                if row:
                    try:
                        rData = json.loads(row[0])
                        
                        # Find client name
                        c_cursor = db._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,))
                        c_row = c_cursor.fetchone()
                        c_name = c_row[0] if c_row else "未知访客"
                        
                        rData["clientName"] = c_name
                        db._conn.execute("UPDATE deepview_reports_v2 SET report_json=? WHERE report_id=?", (json.dumps(rData, ensure_ascii=False), reportId))
                    except:
                        pass
                
                db._conn.execute(
                    "UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?",
                    (clientId, f"recording:{clientId}/{reportId}", reportId, userId)
                )
                
                # Update client's updated_at timestamp to float to top in the list
                db._conn.execute("UPDATE deepview_clients SET updated_at=strftime('%s','now') WHERE client_id=?", (clientId,))
                
                db._conn.commit()
            logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}")
        except Exception as e:
            logger.error(f"[DeepviewSSE] Archive DB update failed: {e}")
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

        # 异步触发客户全景报告生成/刷新
        try:
            userObj = await self._extractUser(request)
            userSub = userObj["userId"] if userObj else "unknown"
            userStorageDir = self._getUserStorageDir(userSub)
            asyncio.create_task(self._generateClientProfile(userId, clientId, userStorageDir, userSub))
            logger.info(f"[DeepviewSSE] Async client profile generation triggered for {clientId}")
        except Exception as triggerErr:
            logger.error(f"[DeepviewSSE] Profile gen trigger failed: {triggerErr}")

        return web.json_response({"success": True, "reportId": reportId, "clientId": clientId}, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 客户全景报告生成管道 (Profile Pipeline)
    # ──────────────────────────────────────────────

    async def _generateClientProfile(self, userId: str, clientId: str, userDir: str, userSub: str) -> None:
        """
        两段式异步管道：
        Stage 1 (Hermes Agent): 读取 profile.md + history/*.md → 生成 Markdown 分析
        Stage 2 (Qwen JSON): Markdown → 结构化 JSON (Schema 硬约束)
        """
        import time as _time
        loop = asyncio.get_event_loop()
        clientDir = os.path.join(userDir, "clients", clientId)
        profileMdPath = os.path.join(clientDir, "profile.md")
        historyDir = os.path.join(clientDir, "history")

        # 收集所有源录音 ID
        sourceRecordings = []
        if os.path.exists(historyDir):
            for item in os.listdir(historyDir):
                itemPath = os.path.join(historyDir, item)
                if os.path.isdir(itemPath):
                    sourceRecordings.append(item)
                elif item.endswith(".md"):
                    sourceRecordings.append(item.replace(".md", ""))

        if not sourceRecordings:
            logger.info(f"[DeepviewSSE] No recordings for client {clientId}, skipping profile gen")
            return

        # ── Stage 1: Hermes Agent (md2md) ──
        storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
        orgId = "org_001"
        orgDir = self._getOrgStorageDir(orgId)
        platformDir = os.path.join(storageDir, "platform")

        # SKILL.md 已通过三元域装配自动注入，此处只构建运行时上下文
        systemPrompt = f"\n\n## 运行时上下文\n企业知识库：{orgDir}/wiki/\n平台规则：{platformDir}/wiki/\n\n"

        # Read phase 1 prompt from external file
        stage1PromptPath = os.path.join(
            os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
            "pipeline", "deepview_profile", "PROMPT_stage1.md",
        )
        if os.path.exists(stage1PromptPath):
            with open(stage1PromptPath, "r", encoding="utf-8") as f:
                stage1Prompt = f.read()
        else:
            stage1Prompt = "## 👤 客户全景档案生成模式\n请生成档案。" # Fallback
            
        stage1Prompt = stage1Prompt.replace("{profileMdPath}", profileMdPath)
        stage1Prompt = stage1Prompt.replace("{historyDir}", historyDir)
        stage1Prompt = stage1Prompt.replace("{userDir}", userDir)
        
        systemPrompt += stage1Prompt

        mdReport = ""
        try:
            from run_agent import AIAgent
            from hermes_state import SessionDB
            db = SessionDB()

            import uuid
            agent = AIAgent(
                model=os.getenv("DEEPVIEW_MODEL", "gemini-pro-vertex"),
                enabled_toolsets=["file"],
                quiet_mode=True,
                platform="deepview",
                session_id=str(uuid.uuid4()),
                session_db=db,
                user_id=userId,
            )

            result = await loop.run_in_executor(
                None,
                lambda: agent.run_conversation(
                    user_message=(
                        f"请读取 {historyDir}/ 下所有历史录音文件（优先读取 report_draft.md 摘要，必要时回溯 asr.md 原文验证），"
                        f"生成该客户的全景档案报告并写入 {profileMdPath}。"
                        f"每条洞察必须标注来源录音文件名。"
                        f"文件第一行必须为：<!-- provenance: ai-aggregate | level: L2 | sources: [{', '.join(sourceRecordings)}] -->"
                    ),
                    system_message=systemPrompt,
                ),
            )
            # Agent 通常通过 write_file 将分析结果写入 profile.md，
            # final_response 仅是操作完成的告知性消息。
            # 因此优先从物理文件获取分析内容，final_response 作为兜底。
            if os.path.exists(profileMdPath):
                with open(profileMdPath, "r", encoding="utf-8") as f:
                    mdReport = f.read().strip()
                logger.info(f"[DeepviewSSE] Profile Stage 1 done (from file), length={len(mdReport)} chars")
            else:
                mdReport = result.get("final_response", "").strip()
                logger.info(f"[DeepviewSSE] Profile Stage 1 done (from response), length={len(mdReport)} chars")
        except Exception as e:
            logger.error(f"[DeepviewSSE] Profile Stage 1 failed for {clientId}: {e}")
            return

        if not mdReport or len(mdReport) < 50:
            logger.warning(f"[DeepviewSSE] Profile Stage 1 output too short for {clientId}")
            return

        # ── Stage 2: JSON 格式引擎 ──
        schemaPath = os.path.join(
            os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
            "pipeline", "deepview_profile", "SCHEMA_profile.json",
        )
        try:
            with open(schemaPath, "r", encoding="utf-8") as f:
                profileSchema = json.load(f)
        except Exception as e:
            logger.error(f"[DeepviewSSE] Failed to load SCHEMA_profile.json: {e}")
            profileSchema = {}

        stage2PromptPath = os.path.join(
            os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
            "pipeline", "deepview_profile", "PROMPT_stage2.md",
        )
        if os.path.exists(stage2PromptPath):
            with open(stage2PromptPath, "r", encoding="utf-8") as f:
                stage2Prompt = f.read()
        else:
            stage2Prompt = "你是一个 JSON 格式转换器。" # Fallback

        # 将外置 Schema 注入 Stage 2 的 system prompt 中，作为硬约束参考
        if profileSchema:
            stage2Prompt += f"\n\n## 目标 JSON Schema（严格遵循）\n```json\n{json.dumps(profileSchema, indent=2, ensure_ascii=False)}\n```"

        try:
            from openai import OpenAI
            litellmClient = OpenAI(
                base_url=os.getenv("GEMINI_BASE_URL", "http://127.0.0.1:4000/v1"),
                api_key=os.getenv("GEMINI_API_KEY", "sk-placeholder"),
            )

            stage2Response = await loop.run_in_executor(
                None,
                lambda: litellmClient.chat.completions.create(
                    model=os.getenv("DEEPVIEW_STAGE2_MODEL", "qwen-plus"),
                    messages=[
                        {"role": "system", "content": stage2Prompt},
                        {"role": "user", "content": mdReport}
                    ],
                    response_format={"type": "json_object"},
                    max_tokens=8192,
                )
            )

            jsonOutput = stage2Response.choices[0].message.content.strip()
            parsedProfile = json.loads(jsonOutput)
            logger.info(f"[DeepviewSSE] Profile Stage 2 done for {clientId}, keys={list(parsedProfile.keys())}")

            # Inject metadata
            parsedProfile["_meta"] = {
                "generatedAt": _time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
                "generatedBy": "deepview_profile_pipeline_v1",
                "sourceRecordings": sourceRecordings,
                "modelUsed": f"{os.getenv('DEEPVIEW_MODEL', 'gemini-pro-vertex')} + {os.getenv('DEEPVIEW_STAGE2_MODEL', 'qwen-plus')}"
            }
            parsedProfile["clientId"] = clientId

            # Persist to DB
            from hermes_state import SessionDB
            profileDb = SessionDB()
            
            # 绝对真理：客户姓名是确定性数据，无论 AI 填了什么都需要以人工录入的数据库记录为准
            try:
                with profileDb._lock:
                    name_cursor = profileDb._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,))
                    name_row = name_cursor.fetchone()
                    if name_row and name_row[0]:
                        parsedProfile["clientName"] = name_row[0]
            except Exception as e:
                logger.warning(f"[DeepviewSSE] Failed to lookup client name for forceful overwrite: {e}")

            profileJson = json.dumps(parsedProfile, ensure_ascii=False)
            with profileDb._lock:
                profileDb._conn.execute(
                    "INSERT OR REPLACE INTO deepview_client_profiles (client_id, user_id, profile_json, generated_at, source_recordings) VALUES (?, ?, ?, ?, ?)",
                    (clientId, userId, profileJson, _time.time(), json.dumps(sourceRecordings))
                )
                profileDb._conn.commit()
            logger.info(f"[DeepviewSSE] Profile persisted to DB for {clientId}")

            # Notify via SSE
            self._pushEvent(userId, "profile:done", {
                "clientId": clientId,
                "clientName": parsedProfile.get("clientName", ""),
            })

        except Exception as e:
            logger.error(f"[DeepviewSSE] Profile Stage 2 failed for {clientId}: {e}")
            # Notify error
            self._pushEvent(userId, "profile:error", {
                "clientId": clientId,
                "error": str(e),
            })

    async def _handleClientProfileReport(self, request: "web.Request") -> "web.Response":
        """
        GET /deepview/clients/{id}/profile-report
        返回客户全景档案的 JSON 报告。
        """
        user = await self._extractUser(request)
        if not user:
            return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)

        clientId = request.match_info.get("id", "")
        if not clientId:
            return web.json_response({"error": "Missing client ID"}, status=400, headers=_CORS_HEADERS)

        try:
            from hermes_state import SessionDB
            db = SessionDB()
            with db._lock:
                cursor = db._conn.execute(
                    "SELECT profile_json, generated_at FROM deepview_client_profiles WHERE client_id=?",
                    (clientId,)
                )
                row = cursor.fetchone()

            if not row:
                return web.json_response({"error": "No profile report yet", "clientId": clientId}, status=404, headers=_CORS_HEADERS)

            profileData = json.loads(row[0])
            
            # 绝对真理：每次读取时，强制用数据库当前的真实姓名覆盖 JSON（防止改名不同步或 AI 幻觉）
            with db._lock:
                name_cursor = db._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,))
                name_row = name_cursor.fetchone()
                if name_row and name_row[0]:
                    profileData["clientName"] = name_row[0]

            safeBody = json.dumps({"success": True, "data": profileData, "generatedAt": row[1]}, ensure_ascii=False)
            return web.Response(text=safeBody, content_type="application/json", headers=_CORS_HEADERS)
        except Exception as e:
            logger.error(f"[DeepviewSSE] Profile report get error: {e}")
            return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)

    # ──────────────────────────────────────────────
    # 服务器生命周期
    # ──────────────────────────────────────────────

async def main():
    """直接运行此文件来启动深维 SSE 服务器。"""
    import sys
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))

    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")

    server = DeepviewSSEServer(
        host=os.getenv("DEEPVIEW_SSE_HOST", "127.0.0.1"),
        port=int(os.getenv("DEEPVIEW_SSE_PORT", "8653")),
    )
    await server.start()

    # 保持运行
    try:
        while True:
            await asyncio.sleep(3600)
    except KeyboardInterrupt:
        pass
    finally:
        await server.stop()


if __name__ == "__main__":
    asyncio.run(main())
