import { join } from "node:path";

import { config } from "../config.js";
import {
  atomicWriteJson,
  ensureDirectory,
  listJsonFiles,
  readJsonFile,
  toStableId,
} from "../utils/fileStore.js";
import { sanitizePersistentDocument } from "../utils/persistencePolicy.js";

export type SessionTurnRecord = {
  id: string;
  assistantMessage: string;
  timestamp: string;
  toolCallCount: number;
  userMessage: string;
};

type SessionTranscriptRecord = {
  actorKey: string;
  clientSessionId?: string;
  projectKey: string;
  sessionId: string;
  turns: SessionTurnRecord[];
  updatedAt: string;
};

export type SessionSearchHit = {
  matchedField: "assistant" | "user";
  score: number;
  sessionId: string;
  snippet: string;
  timestamp: string;
  turnId: string;
};

type SessionTranscriptContext = {
  actorKey: string;
  clientSessionId?: string;
  projectKey: string;
  sessionId: string;
};

const DEFAULT_SEARCH_MAX_RESULTS = 8;
const DEFAULT_SEARCH_MAX_QUERY_CHARS = 240;

export class SessionTranscriptStore {
  private readonly writeQueues = new Map<string, Promise<void>>();

  constructor(private readonly baseDir = config.SESSION_TRANSCRIPT_STORAGE_DIR) {}

  async initialize() {
    await ensureDirectory(this.baseDir);
  }

  async appendTurn(
    context: SessionTranscriptContext,
    turn: {
      assistantMessage: string;
      toolCallCount: number;
      userMessage: string;
    },
  ) {
    const key = this.filePath(context);
    return this.serializeWrite(key, async () => {
      // 同一会话的多次写入串行化，防止流式结束和 UI 同步同时写同一个 transcript。
      const transcript = (await this.readTranscript(context)) ?? {
        actorKey: context.actorKey,
        clientSessionId: context.clientSessionId,
        projectKey: context.projectKey,
        sessionId: context.sessionId,
        turns: [],
        updatedAt: new Date().toISOString(),
      };
      const userMessage = sanitizePersistentDocument(turn.userMessage, 4000);
      const assistantMessage = sanitizePersistentDocument(turn.assistantMessage, 4000);
      if (!userMessage || !assistantMessage) {
        return transcript;
      }

      const timestamp = new Date().toISOString();
      const lastTurn = transcript.turns.at(-1);
      if (
        lastTurn?.userMessage === userMessage &&
        lastTurn.assistantMessage === assistantMessage
      ) {
        // 相同问答重复写入时只更新工具调用数量，避免刷新/重试造成 transcript 重复膨胀。
        lastTurn.toolCallCount = Math.max(lastTurn.toolCallCount, turn.toolCallCount);
        transcript.clientSessionId = context.clientSessionId ?? transcript.clientSessionId;
        transcript.sessionId = context.sessionId;
        transcript.updatedAt = timestamp;
        await atomicWriteJson(key, transcript);
        return transcript;
      }

      const record: SessionTurnRecord = {
        id: toStableId(context.sessionId, timestamp, userMessage, assistantMessage),
        assistantMessage,
        timestamp,
        toolCallCount: Math.max(0, turn.toolCallCount),
        userMessage,
      };
      transcript.clientSessionId = context.clientSessionId ?? transcript.clientSessionId;
      transcript.sessionId = context.sessionId;
      transcript.turns.push(record);
      if (transcript.turns.length > config.SESSION_TRANSCRIPT_MAX_TURNS_PER_SESSION) {
        transcript.turns = transcript.turns.slice(
          transcript.turns.length - config.SESSION_TRANSCRIPT_MAX_TURNS_PER_SESSION,
        );
      }
      transcript.updatedAt = timestamp;
      await atomicWriteJson(key, transcript);
      return transcript;
    });
  }

  async getRecentTurns(
    context: SessionTranscriptContext,
    limit: number,
  ): Promise<SessionTurnRecord[]> {
    const transcript = await this.readTranscript(context);
    if (!transcript) {
      return [];
    }
    return transcript.turns.slice(-Math.max(1, limit));
  }

  async cloneThread(
    sourceContext: SessionTranscriptContext,
    targetContext: SessionTranscriptContext,
    keepMessageCount: number,
  ) {
    const sourceTranscript = await this.readTranscript(sourceContext);
    const timestamp = new Date().toISOString();
    // 分叉会话只复制用户选择保留的上下文，后续新分支拥有独立 transcript 文件。
    const nextTranscript: SessionTranscriptRecord = {
      actorKey: targetContext.actorKey,
      clientSessionId: targetContext.clientSessionId,
      projectKey: targetContext.projectKey,
      sessionId: targetContext.sessionId,
      turns: projectTurnsForFork(sourceTranscript?.turns ?? [], keepMessageCount),
      updatedAt: timestamp,
    };
    await atomicWriteJson(this.filePath(targetContext), nextTranscript);
    return nextTranscript;
  }

  async search(
    context: Pick<SessionTranscriptContext, "actorKey" | "projectKey">,
    query: string,
    maxResults = DEFAULT_SEARCH_MAX_RESULTS,
  ): Promise<SessionSearchHit[]> {
    // 当前搜索是轻量本地文本匹配，按 actor/project 过滤后再计算简单相关性分数。
    const normalizedQuery = query.trim().toLowerCase().slice(0, DEFAULT_SEARCH_MAX_QUERY_CHARS);
    if (!normalizedQuery) {
      return [];
    }
    const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
    const hits: SessionSearchHit[] = [];
    const files = await listJsonFiles(this.baseDir);
    for (const file of files) {
      const transcript = await readJsonFile<SessionTranscriptRecord>(file);
      if (!transcript) {
        continue;
      }
      if (
        transcript.actorKey !== context.actorKey ||
        transcript.projectKey !== context.projectKey
      ) {
        continue;
      }
      for (const turn of transcript.turns) {
        const candidates: Array<["user" | "assistant", string]> = [
          ["user", turn.userMessage],
          ["assistant", turn.assistantMessage],
        ];
        for (const [matchedField, text] of candidates) {
          const score = scoreText(text, normalizedQuery, queryTokens);
          if (score <= 0) {
            continue;
          }
          hits.push({
            matchedField,
            score,
            sessionId: transcript.sessionId,
            snippet: buildSnippet(text, normalizedQuery),
            timestamp: turn.timestamp,
            turnId: turn.id,
          });
        }
      }
    }
    return hits.sort((a, b) => b.score - a.score).slice(0, Math.max(1, maxResults));
  }

  private async readTranscript(context: SessionTranscriptContext) {
    return await readJsonFile<SessionTranscriptRecord>(this.filePath(context));
  }

  private filePath(context: SessionTranscriptContext) {
    return join(
      this.baseDir,
      `${context.actorKey}__${context.projectKey}__${context.sessionId}.json`,
    );
  }

  private async serializeWrite<T>(key: string, task: () => Promise<T>) {
    const previous = this.writeQueues.get(key) ?? Promise.resolve();
    const run = previous.catch(() => undefined).then(task);
    const next = run.then(
      () => undefined,
      () => undefined,
    );
    this.writeQueues.set(key, next);
    try {
      return await run;
    } finally {
      if (this.writeQueues.get(key) === next) {
        this.writeQueues.delete(key);
      }
    }
  }
}

const scoreText = (text: string, query: string, queryTokens: string[]) => {
  const normalized = text.toLowerCase();
  let score = 0;
  if (normalized.includes(query)) {
    score += Math.max(10, query.length);
  }
  for (const token of queryTokens) {
    if (token.length >= 2 && normalized.includes(token)) {
      score += 1;
    }
  }
  return score;
};

const buildSnippet = (text: string, query: string) => {
  const compact = text.replace(/\s+/g, " ").trim();
  const idx = compact.toLowerCase().indexOf(query);
  if (idx === -1) {
    return compact.length > 180 ? `${compact.slice(0, 177)}...` : compact;
  }
  const start = Math.max(0, idx - 60);
  const end = Math.min(compact.length, idx + query.length + 100);
  const snippet = compact.slice(start, end).trim();
  const prefix = start > 0 ? "..." : "";
  const suffix = end < compact.length ? "..." : "";
  return `${prefix}${snippet}${suffix}`;
};

const projectTurnsForFork = (
  turns: SessionTurnRecord[],
  keepMessageCount: number,
): SessionTurnRecord[] => {
  if (keepMessageCount <= 0) {
    return [];
  }

  const keepTurnCount = Math.floor(keepMessageCount / 2);
  if (keepTurnCount <= 0) {
    return [];
  }

  return turns.slice(0, keepTurnCount);
};
