// ============================================================================
// atom-extraction.js - L0 场景锚点提取（v2 - 场景摘要 + 图结构）
//
// 设计依据：
// - BGE-M3 (BAAI, 2024): 自然语言段落检索精度最高 → semantic = 纯自然语言
// - Interpersonal Circumplex (Kiesler, 1983): 权力轴+情感轴 → dynamics 枚举
// - Labov Narrative Structure (1972): 叙事功能轴 → dynamics 枚举补充
// - TransE (Bordes, 2013): s/t/r 三元组方向性 → edges 格式
//
// 每楼层 1-2 个场景锚点（非碎片原子），60-100 字场景摘要
// ============================================================================

import { callLLM, parseJson } from './llm-service.js';
import { xbLog } from '../../../../core/debug-core.js';
import { filterText } from '../utils/text-filter.js';

const MODULE_ID = 'atom-extraction';

const CONCURRENCY = 10;
const RETRY_COUNT = 2;
const RETRY_DELAY = 500;
const DEFAULT_TIMEOUT = 20000;
const STAGGER_DELAY = 80;

let batchCancelled = false;

export function cancelBatchExtraction() {
    batchCancelled = true;
}

export function isBatchCancelled() {
    return batchCancelled;
}

// ============================================================================
// dynamics 封闭枚举（8 个标签，两轴四象限 + 叙事轴）
// ============================================================================

const VALID_DYNAMICS = new Set([
    // 权力轴 (Interpersonal Circumplex: Dominance-Submission)
    '支配',   // 控制、命令、审视、威慑、主导
    '让渡',   // 顺从、服从、屈服、被动、配合

    // 情感轴 (Interpersonal Circumplex: Hostility-Friendliness)
    '亲密',   // 温柔、关怀、依赖、信任、连接
    '敌意',   // 对抗、拒绝、攻击、嘲讽、排斥

    // 叙事轴 (Labov Narrative Structure)
    '揭示',   // 真相、发现、告白、暴露、秘密
    '决意',   // 选择、承诺、放弃、宣言、转折
    '张力',   // 悬念、对峙、暗涌、不安、等待
    '丧失',   // 分离、死亡、破碎、遗憾、崩塌
]);

// ============================================================================
// L0 提取 Prompt
// ============================================================================

const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场景锚点，用于语义检索和关系追踪。

输入格式：
<round>
  <user name="用户名">...</user>
  <assistant name="角色名">...</assistant>
</round>

只输出严格JSON：
{"anchors":[
  {
    "scene": "60-100字完整场景描述",
    "who": ["角色名1","角色名2"],
    "edges": [{"s":"施事方","t":"受事方","r":"互动行为"}],
    "dynamics": ["标签"],
    "where": "地点"
  }
]}

## scene 写法
- 纯自然语言，像旁白或日记，不要任何标签/标记/枚举值
- 必须包含：角色名、动作、情感氛围、关键细节
- 读者只看 scene 就能复原这一幕
- 60-100字，信息密集但流畅

## who
- 参与互动的角色正式名称，不用代词或别称

## edges（关系三元组）
- s=施事方 t=受事方 r=互动行为（10-15字）
- 每个锚点 1-3 条

## dynamics（封闭枚举，选0-2个）
权力轴：支配（控制/命令/审视） | 让渡（顺从/服从/屈服）
情感轴：亲密（温柔/信任/连接） | 敌意（对抗/拒绝/攻击）
叙事轴：揭示（真相/秘密） | 决意（选择/承诺） | 张力（对峙/不安） | 丧失（分离/破碎）
纯日常无明显模式时 dynamics 为 []

## where
- 场景地点，无明确地点时空字符串

## 数量规则
- 最多2个。1个够时不凑2个
- 明显场景切换（地点/时间/对象变化）时才2个
- 同一场景不拆分
- 无角色互动时返回 {"anchors":[]}

## 示例
输入：艾拉在火山口举起圣剑刺穿古龙心脏，龙血溅满她的铠甲，她跪倒在地痛哭
输出：
{"anchors":[{"scene":"火山口上艾拉举起圣剑刺穿古龙的心脏，龙血溅满铠甲，古龙轰然倒地，艾拉跪倒在滚烫的岩石上痛哭，完成了她不得不做的弑杀","who":["艾拉","古龙"],"edges":[{"s":"艾拉","t":"古龙","r":"以圣剑刺穿心脏"}],"dynamics":["决意","丧失"],"where":"火山口"}]}`;

const JSON_PREFILL = '{"anchors":[';

// ============================================================================
// 睡眠工具
// ============================================================================

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// ============================================================================
// 清洗与构建
// ============================================================================

/**
 * 清洗 dynamics 标签，只保留合法枚举值
 * @param {string[]} raw
 * @returns {string[]}
 */
function sanitizeDynamics(raw) {
    if (!Array.isArray(raw)) return [];
    return raw
        .map(d => String(d || '').trim())
        .filter(d => VALID_DYNAMICS.has(d))
        .slice(0, 2);
}

/**
 * 清洗 edges 三元组
 * @param {object[]} raw
 * @returns {object[]}
 */
function sanitizeEdges(raw) {
    if (!Array.isArray(raw)) return [];
    return raw
        .filter(e => e && typeof e === 'object')
        .map(e => ({
            s: String(e.s || '').trim(),
            t: String(e.t || '').trim(),
            r: String(e.r || '').trim().slice(0, 30),
        }))
    .filter(e => e.s && e.t && e.r)
    .slice(0, 3);
}

/**
 * 清洗 who 列表
 * @param {string[]} raw
 * @returns {string[]}
 */
function sanitizeWho(raw) {
    if (!Array.isArray(raw)) return [];
    const seen = new Set();
    return raw
        .map(w => String(w || '').trim())
        .filter(w => {
            if (!w || w.length < 1 || seen.has(w)) return false;
            seen.add(w);
            return true;
        })
        .slice(0, 6);
}

/**
 * 将解析后的 anchor 转换为 atom 存储对象
 *
 * semantic = scene（纯自然语言，直接用于 embedding）
 *
 * @param {object} anchor - LLM 输出的 anchor 对象
 * @param {number} aiFloor - AI 消息楼层号
 * @param {number} idx - 同楼层序号（0 或 1）
 * @returns {object|null} atom 对象
 */
function anchorToAtom(anchor, aiFloor, idx) {
    const scene = String(anchor.scene || '').trim();
    if (!scene) return null;

    // scene 过短（< 15 字）可能是噪音
    if (scene.length < 15) return null;

    const who = sanitizeWho(anchor.who);
    const edges = sanitizeEdges(anchor.edges);
    const dynamics = sanitizeDynamics(anchor.dynamics);
    const where = String(anchor.where || '').trim();

    return {
        atomId: `atom-${aiFloor}-${idx}`,
        floor: aiFloor,
        source: 'ai',

        // ═══ 检索层（embedding 的唯一入口） ═══
        semantic: scene,

        // ═══ 场景数据 ═══
        scene,

        // ═══ 图结构层（扩散的 key） ═══
        who,
        edges,
        dynamics,
        where,
    };
}

// ============================================================================
// 单轮提取（带重试）
// ============================================================================

async function extractAtomsForRoundWithRetry(userMessage, aiMessage, aiFloor, options = {}) {
    const { timeout = DEFAULT_TIMEOUT } = options;

    if (!aiMessage?.mes?.trim()) return [];

    const parts = [];
    const userName = userMessage?.name || '用户';
    const aiName = aiMessage.name || '角色';

    if (userMessage?.mes?.trim()) {
        const userText = filterText(userMessage.mes);
        parts.push(`<user name="${userName}">\n${userText}\n</user>`);
    }

    const aiText = filterText(aiMessage.mes);
    parts.push(`<assistant name="${aiName}">\n${aiText}\n</assistant>`);

    const input = `<round>\n${parts.join('\n')}\n</round>`;

    for (let attempt = 0; attempt <= RETRY_COUNT; attempt++) {
        if (batchCancelled) return [];

        try {
            const response = await callLLM([
                { role: 'system', content: SYSTEM_PROMPT },
                { role: 'user', content: input },
                { role: 'assistant', content: JSON_PREFILL },
            ], {
                temperature: 0.3,
                max_tokens: 600,
                timeout,
            });

            const rawText = String(response || '');
            if (!rawText.trim()) {
                if (attempt < RETRY_COUNT) {
                    await sleep(RETRY_DELAY);
                    continue;
                }
                return null;
            }

            const fullJson = JSON_PREFILL + rawText;

            let parsed;
            try {
                parsed = parseJson(fullJson);
            } catch (e) {
                xbLog.warn(MODULE_ID, `floor ${aiFloor} JSON解析失败 (attempt ${attempt})`);
                if (attempt < RETRY_COUNT) {
                    await sleep(RETRY_DELAY);
                    continue;
                }
                return null;
            }

            // 兼容：优先 anchors，回退 atoms
            const rawAnchors = parsed?.anchors || parsed?.atoms;
            if (!rawAnchors || !Array.isArray(rawAnchors)) {
                if (attempt < RETRY_COUNT) {
                    await sleep(RETRY_DELAY);
                    continue;
                }
                return null;
            }

            // 转换为 atom 存储格式（最多 2 个）
            const atoms = rawAnchors
                .slice(0, 2)
                .map((a, idx) => anchorToAtom(a, aiFloor, idx))
                .filter(Boolean);

            return atoms;

        } catch (e) {
            if (batchCancelled) return null;

            if (attempt < RETRY_COUNT) {
                await sleep(RETRY_DELAY * (attempt + 1));
                continue;
            }
            xbLog.error(MODULE_ID, `floor ${aiFloor} 失败`, e);
            return null;
        }
    }

    return null;
}

export async function extractAtomsForRound(userMessage, aiMessage, aiFloor, options = {}) {
    return extractAtomsForRoundWithRetry(userMessage, aiMessage, aiFloor, options);
}

// ============================================================================
// 批量提取
// ============================================================================

export async function batchExtractAtoms(chat, onProgress) {
    if (!chat?.length) return [];

    batchCancelled = false;

    const pairs = [];
    for (let i = 0; i < chat.length; i++) {
        if (!chat[i].is_user) {
            const userMsg = (i > 0 && chat[i - 1]?.is_user) ? chat[i - 1] : null;
            pairs.push({ userMsg, aiMsg: chat[i], aiFloor: i });
        }
    }

    if (!pairs.length) return [];

    const allAtoms = [];
    let completed = 0;
    let failed = 0;

    for (let i = 0; i < pairs.length; i += CONCURRENCY) {
        if (batchCancelled) break;

        const batch = pairs.slice(i, i + CONCURRENCY);

        if (i === 0) {
            const promises = batch.map((pair, idx) => (async () => {
                await sleep(idx * STAGGER_DELAY);

                if (batchCancelled) return;

                try {
                    const atoms = await extractAtomsForRoundWithRetry(
                        pair.userMsg,
                        pair.aiMsg,
                        pair.aiFloor,
                        { timeout: DEFAULT_TIMEOUT }
                    );
                    if (atoms?.length) {
                        allAtoms.push(...atoms);
                    } else if (atoms === null) {
                        failed++;
                    }
                } catch {
                    failed++;
                }
                completed++;
                onProgress?.(completed, pairs.length, failed);
            })());
            await Promise.all(promises);
        } else {
            const promises = batch.map(pair =>
                extractAtomsForRoundWithRetry(
                    pair.userMsg,
                    pair.aiMsg,
                    pair.aiFloor,
                    { timeout: DEFAULT_TIMEOUT }
                )
                    .then(atoms => {
                        if (batchCancelled) return;
                        if (atoms?.length) {
                            allAtoms.push(...atoms);
                        } else if (atoms === null) {
                            failed++;
                        }
                        completed++;
                        onProgress?.(completed, pairs.length, failed);
                    })
                    .catch(() => {
                        if (batchCancelled) return;
                        failed++;
                        completed++;
                        onProgress?.(completed, pairs.length, failed);
                    })
            );

            await Promise.all(promises);
        }

        if (i + CONCURRENCY < pairs.length && !batchCancelled) {
            await sleep(30);
        }
    }

    xbLog.info(MODULE_ID, `批量提取完成: ${allAtoms.length} atoms, ${failed} 失败`);

    return allAtoms;
}
