import { Config, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { readdir } from 'node:fs/promises';
import * as path from 'node:path';
import { In, IsNull, Repository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { NetaClawSessionEntity } from '../entity/session.js';
import { NetaClawMessageEntity } from '../entity/message.js';
import { NetaClawSubagentSessionEntity } from '../entity/subagent_session.js';
import { NetaClawAgentEntity } from '../entity/agent.js';
import { NetaClawAgentSessionEntity } from '../entity/agent_session.js';
import { NetaClawAgentSessionEntryEntity } from '../entity/agent_session_entry.js';
import { AuditOrderEntity } from '../../audit/entity/order.js';
import { LLMMessage, LLMMessageWithId } from '../plugins/plugin_entry.js';
import type { NetaClawConfig } from '../config.js';
import type { SessionTreeProvider } from '../session-tree/provider.js';
import type { SessionTreeEntry, SessionTreeMessage, SessionTreeSnapshot, SessionTreeSession } from '../session-tree/types.js';
import {
  createSessionTreeProvider,
  type ResolvedAgentSessionTreeConfig,
  resolveAgentSessionTreeConfig,
} from '../session-tree/factory.js';
import { createSessionTreeSessionId } from '../session-tree/id.js';
import { projectSubagentEntry, projectSubagentSnapshot } from '../session-tree/subagent_projection.js';

export interface AuditOrderSessionMetadata extends Record<string, unknown> {
  source: 'audit_order';
  type: 'audit_order';
  isAuditOrder: true;
  auditOrder: {
    id?: number;
    orderNo: string;
    orderType: string;
    requestId: string;
  };
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

export function isAuditOrderSessionMetadata(
  metadata: unknown,
): metadata is AuditOrderSessionMetadata {
  if (!isPlainObject(metadata)) return false;
  if (metadata.source !== 'audit_order') return false;
  if (metadata.type !== 'audit_order') return false;
  if (metadata.isAuditOrder !== true) return false;
  if (!isPlainObject(metadata.auditOrder)) return false;

  const { auditOrder } = metadata;
  return (
    typeof auditOrder.orderNo === 'string'
    && auditOrder.orderNo.trim().length > 0
    && typeof auditOrder.orderType === 'string'
    && auditOrder.orderType.trim().length > 0
    && typeof auditOrder.requestId === 'string'
    && auditOrder.requestId.trim().length > 0
    && (auditOrder.id === undefined || typeof auditOrder.id === 'number')
  );
}

export function buildAuditOrderSessionMetadata(order: Pick<AuditOrderEntity, 'id' | 'orderNo' | 'orderType' | 'requestId'>): AuditOrderSessionMetadata {
  return {
    source: 'audit_order',
    type: 'audit_order',
    isAuditOrder: true,
    auditOrder: {
      id: order.id,
      orderNo: order.orderNo,
      orderType: order.orderType,
      requestId: order.requestId,
    },
  };
}

function getAuditOrderSessionLabel(
  sessionId: string,
  metadata?: Record<string, unknown> | null,
  auditOrder?: Pick<AuditOrderEntity, 'orderNo'> | null,
): string | null {
  if (auditOrder?.orderNo && auditOrder.orderNo.trim()) {
    return auditOrder.orderNo.trim();
  }
  if (isAuditOrderSessionMetadata(metadata)) {
    return metadata.auditOrder.orderNo.trim() || sessionId;
  }
  return null;
}

function summarizeSessionTitle(message: string, maxLength = 48): string {
  const normalized = message.replace(/\s+/g, ' ').trim();
  if (!normalized) return '';
  if (normalized.length <= maxLength) return normalized;
  return `${normalized.slice(0, maxLength - 1)}…`;
}

function shouldReplaceSessionTitle(session: SessionTreeSession | null, nextTitle: string): boolean {
  if (!session || !nextTitle) return false;
  const currentTitle = session.title?.trim();
  if (!currentTitle) return true;
  if (currentTitle === 'default') return true;
  if (typeof session.metadata?.agentName === 'string' && currentTitle === session.metadata.agentName) return true;
  return false;
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class NetaClawSessionService {
  @Config('netaclaw')
  netaclawConfig: NetaClawConfig & {
    session?: {
      backend?: 'file' | 'mysql';
    };
  };

  @InjectEntityModel(NetaClawAgentEntity)
  agentRepo: Repository<NetaClawAgentEntity>;

  @InjectEntityModel(NetaClawSessionEntity)
  sessionRepo: Repository<NetaClawSessionEntity>;

  @InjectEntityModel(NetaClawMessageEntity)
  messageRepo: Repository<NetaClawMessageEntity>;

  @InjectEntityModel(NetaClawSubagentSessionEntity)
  subagentSessionRepo: Repository<NetaClawSubagentSessionEntity>;

  @InjectEntityModel(NetaClawAgentSessionEntity)
  agentSessionRepo: Repository<NetaClawAgentSessionEntity>;

  @InjectEntityModel(NetaClawAgentSessionEntryEntity)
  agentSessionEntryRepo: Repository<NetaClawAgentSessionEntryEntity>;

  @InjectEntityModel(AuditOrderEntity)
  auditOrderRepo: Repository<AuditOrderEntity>;

  private resolveSessionTreeConfig(agentConfig?: NetaClawAgentEntity['config']): ResolvedAgentSessionTreeConfig {
    return resolveAgentSessionTreeConfig(
      agentConfig?.session,
      {
        backend: this.netaclawConfig?.session?.backend,
        dataDir: this.netaclawConfig?.dataDir,
      },
    );
  }

  async getSessionTreeProviderForAgent(agentId?: number): Promise<SessionTreeProvider> {
    const agent = agentId ? await this.agentRepo.findOneBy({ id: agentId }) : null;
    const sessionConfig = this.resolveSessionTreeConfig(agent?.config);

    return createSessionTreeProvider({
      session: sessionConfig,
      sessionRepo: this.agentSessionRepo,
      entryRepo: this.agentSessionEntryRepo,
    });
  }

  async listSessionTreeSessions(agentId?: number): Promise<SessionTreeSession[]> {
    const annotateAuditSessions = async (sessions: SessionTreeSession[]) => {
      if (sessions.length === 0) return sessions;
      const sessionIds = sessions.map(session => session.sessionId);
      const auditOrders = await this.auditOrderRepo.find({
        where: { sessionId: In(sessionIds) },
        select: ['id', 'sessionId', 'orderNo', 'orderType', 'requestId'],
      });
      if (auditOrders.length === 0) return sessions;
      const auditOrderMap = new Map(
        auditOrders.map(order => [order.sessionId, buildAuditOrderSessionMetadata(order)]),
      );
      return sessions.map(session => {
        const auditMetadata = auditOrderMap.get(session.sessionId);
        if (!auditMetadata) return session;
        return {
          ...session,
          metadata: {
            ...(session.metadata || {}),
            ...auditMetadata,
          },
        };
      });
    };

    if (agentId) {
      const agent = await this.agentRepo.findOneBy({ id: agentId });
      const sessionConfig = this.resolveSessionTreeConfig(agent?.config);

      if (sessionConfig.backend === 'mysql') {
        return annotateAuditSessions(this.mapMysqlTreeSessions(await this.agentSessionRepo.find({
          where: { agentId: String(agentId) },
          order: { updateTime: 'DESC' },
        })));
      }

      return annotateAuditSessions(await this.listFileTreeSessions(sessionConfig.file?.rootDir, { agentId }));
    }

    const mysqlSessions = this.mapMysqlTreeSessions(await this.agentSessionRepo.find({
      order: { updateTime: 'DESC' },
    }));
    const fileSessions = await this.listFileTreeSessions(
      path.join(this.netaclawConfig?.dataDir ?? '~/.neta', 'sessions'),
    );
    const merged = new Map<string, SessionTreeSession>();

    for (const session of mysqlSessions) {
      merged.set(session.sessionId, session);
    }
    for (const session of fileSessions) {
      if (!merged.has(session.sessionId)) {
        merged.set(session.sessionId, session);
      }
    }

    return annotateAuditSessions([...merged.values()].sort((a, b) => {
      const aTime = Date.parse(a.updatedAt || a.createdAt || '') || 0;
      const bTime = Date.parse(b.updatedAt || b.createdAt || '') || 0;
      return bTime - aTime;
    }));
  }

  async getRuntimeSnapshot(sessionId: string, agentId?: number): Promise<{
    session: SessionTreeSnapshot['session'];
    runtimeContext: SessionTreeSnapshot['runtimeContext'];
  }> {
    const provider = await this.getSessionTreeProviderForAgent(agentId);
    const snapshot = await provider.getSnapshot(sessionId);
    return {
      session: snapshot.session,
      runtimeContext: snapshot.runtimeContext,
    };
  }

  async getSessionTreeSnapshot(sessionId: string, agentId?: number): Promise<SessionTreeSnapshot> {
    const provider = await this.getSessionTreeProviderForAgent(agentId);
    return projectSubagentSnapshot(await provider.getSnapshot(sessionId));
  }

  async getSessionTreeSession(sessionId: string, agentId?: number): Promise<SessionTreeSession | null> {
    const provider = await this.getSessionTreeProviderForAgent(agentId);
    return provider.getSession(sessionId);
  }

  async appendUserMessageEntry(sessionId: string, params: {
    parentEntryId: string | null;
    message: string;
    agentId?: number;
    metadata?: Record<string, unknown>;
  }): Promise<{ id: string; parentId: string | null; entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const entry = await provider.appendEntry(sessionId, {
      parentId: params.parentEntryId,
      type: 'message',
      message: {
        role: 'user',
        content: params.message,
        metadata: params.metadata,
      },
    });
    let session = await provider.getSession(sessionId);
    const title = summarizeSessionTitle(params.message);
    if (shouldReplaceSessionTitle(session, title)) {
      session = await provider.updateSession(sessionId, { title });
    }

    return {
      id: entry.id,
      parentId: entry.parentId,
      entry,
      session: session!,
    };
  }

  async appendAssistantPlaceholderEntry(sessionId: string, params: {
    parentEntryId: string;
    agentId?: number;
  }): Promise<{ id: string; parentId: string | null; entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const entry = await provider.appendEntry(sessionId, {
      parentId: params.parentEntryId,
      type: 'message',
      message: {
        role: 'assistant',
        content: '',
        metadata: {
          inFlight: true,
        },
      },
    });

    return {
      id: entry.id,
      parentId: entry.parentId,
      entry,
      session: (await provider.getSession(sessionId))!,
    };
  }

  async finalizeAssistantEntry(sessionId: string, entryId: string, params: {
    content: string;
    thinking?: string;
    metadata?: Record<string, unknown>;
    agentId?: number;
  }): Promise<{ entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const snapshot = await provider.getSnapshot(sessionId);
    const existing = snapshot.entries.find(entry => entry.id === entryId);

    if (!existing || existing.type !== 'message' || existing.message.role !== 'assistant') {
      throw new Error(`Assistant entry ${entryId} not found in session ${sessionId}`);
    }

    const nextMessage: SessionTreeMessage = {
      ...existing.message,
      content: params.content,
      thinking: params.thinking,
      metadata: params.metadata,
    };

    const updated = await provider.updateEntry(sessionId, entryId, {
      message: nextMessage,
    });
    return {
      entry: projectSubagentEntry(updated),
      session: (await provider.getSession(sessionId))!,
    };
  }

  async appendSubagentBatchEntry(sessionId: string, params: {
    parentEntryId: string;
    batchId: string;
    mode: 'single' | 'parallel';
    tasks: Record<string, unknown>[];
    status?: 'running' | 'completed';
    agentId?: number;
  }): Promise<{ id: string; parentId: string | null; entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const entry = await provider.appendEntry(sessionId, {
      parentId: params.parentEntryId,
      type: 'subagent_batch',
      content: {
        batchId: params.batchId,
        mode: params.mode,
        tasks: params.tasks,
        status: params.status ?? 'running',
        parentEntryId: params.parentEntryId,
      },
    });

    return {
      id: entry.id,
      parentId: entry.parentId,
      entry,
      session: (await provider.getSession(sessionId))!,
    };
  }

  async updateSubagentBatchEntry(sessionId: string, entryId: string, params: {
    batchId: string;
    mode?: 'single' | 'parallel';
    tasks?: Record<string, unknown>[];
    status?: 'running' | 'completed';
    metadata?: Record<string, unknown>;
    agentId?: number;
  }): Promise<{ entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const snapshot = await provider.getSnapshot(sessionId);
    const existing = snapshot.entries.find(entry => entry.id === entryId);

    if (!existing || existing.type !== 'subagent_batch') {
      throw new Error(`Subagent batch entry ${entryId} not found in session ${sessionId}`);
    }

    const previousContent = (existing.content || {}) as Record<string, unknown>;
    const updated = await provider.updateEntry(sessionId, entryId, {
      content: {
        ...previousContent,
        batchId: params.batchId,
        mode: params.mode ?? previousContent.mode,
        tasks: params.tasks ?? previousContent.tasks,
        status: params.status ?? previousContent.status ?? 'running',
        parentEntryId: previousContent.parentEntryId ?? existing.parentId,
      },
      metadata: params.metadata ?? existing.metadata,
    });

    return {
      entry: projectSubagentEntry(updated),
      session: (await provider.getSession(sessionId))!,
    };
  }

  async appendSubagentResultEntry(sessionId: string, params: {
    parentEntryId: string;
    batchId: string;
    results: Record<string, unknown>[];
    status?: 'running' | 'completed';
    metadata?: Record<string, unknown>;
    agentId?: number;
  }): Promise<{ id: string; parentId: string | null; entry: SessionTreeEntry; session: SessionTreeSession }> {
    const provider = await this.getSessionTreeProviderForAgent(params.agentId);
    const entry = await provider.appendEntry(sessionId, {
      parentId: params.parentEntryId,
      type: 'subagent_result',
      content: {
        batchId: params.batchId,
        results: params.results,
        status: params.status ?? 'completed',
        parentEntryId: params.parentEntryId,
      },
      metadata: params.metadata,
    });

    return {
      id: entry.id,
      parentId: entry.parentId,
      entry: projectSubagentEntry(entry),
      session: (await provider.getSession(sessionId))!,
    };
  }

  async createSession(agentName: string, userId?: string, agentId?: number, sessionId?: string): Promise<string> {
    const resolvedSessionId = sessionId || uuid();
    await this.sessionRepo.save({
      sessionId: resolvedSessionId,
      agentName,
      userId,
      agentId: agentId ?? null,
      status: 'active',
    });
    return resolvedSessionId;
  }

  async getOrCreateSession(
    sessionId: string | undefined,
    agentName: string,
    agentId?: number,
    userId?: string,
    cwd?: string,
    metadata?: Record<string, unknown>,
  ): Promise<string> {
    if (agentId) {
      const agent = await this.agentRepo.findOneBy({ id: agentId });
      const sessionConfig = this.resolveSessionTreeConfig(agent?.config);
      const resolvedCwd = cwd || sessionConfig.cwd;
      const provider = createSessionTreeProvider({
        session: sessionConfig,
        cwd: resolvedCwd,
        sessionRepo: this.agentSessionRepo,
        entryRepo: this.agentSessionEntryRepo,
      });
      if (sessionId) {
        const existing = await provider.getSession(sessionId);
        if (existing) {
          const nextPatch: Partial<SessionTreeSession> = {};
          if (userId && existing.userId !== userId) {
            nextPatch.userId = userId;
          }
          if (resolvedCwd && existing.cwd !== resolvedCwd) {
            nextPatch.cwd = resolvedCwd;
          }
          if (metadata && Object.keys(metadata).length > 0) {
            nextPatch.metadata = {
              ...(existing.metadata || {}),
              ...metadata,
            };
          }
          if (Object.keys(nextPatch).length > 0) {
            await provider.updateSession(sessionId, nextPatch);
          }
          return sessionId;
        }
      }

      const created = await provider.createSession({
        sessionId: sessionId ?? createSessionTreeSessionId(),
        provider: 'file',
        cwd: resolvedCwd,
        agentId,
        userId,
        title: agentName,
        metadata: {
          agentName,
          ...(metadata || {}),
        },
      });
      return created.sessionId;
    }

    if (sessionId) {
      const existing = await this.sessionRepo.findOneBy({ sessionId });
      if (existing) {
        const nextPatch: Partial<NetaClawSessionEntity> = {};
        if (userId && existing.userId !== userId) {
          nextPatch.userId = userId;
        }
        if (metadata && Object.keys(metadata).length > 0) {
          nextPatch.metadata = {
            ...(existing.metadata || {}),
            ...metadata,
          };
        }
        if (Object.keys(nextPatch).length > 0) {
          await this.sessionRepo.update({ sessionId }, nextPatch);
        }
        return sessionId;
      }
    }
    return this.createSession(agentName, userId, agentId, sessionId);
  }

  async loadHistory(
    sessionId: string,
    options: { view?: 'compacted' | 'full' } = {},
  ): Promise<LLMMessage[]> {
    const { view = 'compacted' } = options;
    const where: any = { sessionId };
    if (view === 'compacted') {
      where.compactedAt = IsNull();
    }
    const messages = await this.messageRepo.find({
      where,
      order: { createTime: 'ASC', id: 'ASC' },
    });
    return messages.map(m => ({
      role: m.role as LLMMessage['role'],
      content: m.content,
      toolCalls: m.toolCalls as any,
      toolCallId: m.toolCallId,
    }));
  }

  /** 加载历史消息（带数据库ID），仅返回未被压缩的消息 */
  async loadHistoryWithId(
    sessionId: string,
    options: { view?: 'compacted' | 'full' } = {},
  ): Promise<LLMMessageWithId[]> {
    const { view = 'compacted' } = options;
    const where: any = { sessionId };
    if (view === 'compacted') {
      where.compactedAt = IsNull();
    }
    const messages = await this.messageRepo.find({
      where,
      order: { createTime: 'ASC', id: 'ASC' },
    });
    return messages.map(m => ({
      id: m.id,
      role: m.role as LLMMessage['role'],
      content: m.content,
      toolCalls: m.toolCalls as any,
      toolCallId: m.toolCallId,
    }));
  }

  /** 获取会话最新的压缩摘要内容 */
  async getLatestSummary(sessionId: string): Promise<string | null> {
    const msg = await this.messageRepo.findOne({
      where: { sessionId, isCompactionSummary: true, compactedAt: IsNull() },
      order: { createTime: 'DESC' },
    });
    return msg?.content ?? null;
  }

  async saveMessage(
    sessionId: string,
    msg: LLMMessage & { thinking?: string; skillName?: string; metadata?: Record<string, unknown> }
  ): Promise<NetaClawMessageEntity> {
    return this.messageRepo.save({
      sessionId,
      role: msg.role,
      content: msg.content,
      thinking: msg.thinking,
      toolCalls: msg.toolCalls,
      toolCallId: msg.toolCallId,
      skillName: msg.skillName,
      metadata: msg.metadata,
    });
  }

  async updateMessage(
    id: number,
    patch: Partial<Pick<NetaClawMessageEntity, 'content' | 'thinking' | 'toolCalls' | 'toolCallId' | 'skillName' | 'metadata'>>
  ): Promise<void> {
    await this.messageRepo.update({ id }, patch);
  }

  async updateTitle(sessionId: string, title: string): Promise<void> {
    await this.sessionRepo.update({ sessionId }, { title });
  }

  async updateSessionMetadata(sessionId: string, metadata: Record<string, unknown>): Promise<void> {
    const existing = await this.sessionRepo.findOneBy({ sessionId });
    if (!existing) return;

    await this.sessionRepo.update(
      { sessionId },
      {
        metadata: {
          ...(existing.metadata || {}),
          ...metadata,
        },
      }
    );
  }

  /** 会话列表 */
  async listSessions(userId?: string, agentId?: number): Promise<NetaClawSessionEntity[]> {
    const where: any = { status: 'active' };
    if (userId) where.userId = userId;
    if (agentId) where.agentId = agentId;
    return this.sessionRepo.find({ where, order: { updateTime: 'DESC' } });
  }

  /**
   * 获取会话消息历史
   * @param view 'compacted'（默认，只返回未压缩消息 + 压缩摘要）| 'full'（返回所有消息）
   */
  async getMessages(sessionId: string, view: 'compacted' | 'full' = 'compacted') {
    if (view === 'full') {
      return this.messageRepo.find({
        where: { sessionId },
        order: { createTime: 'ASC', id: 'ASC' },
      });
    }
    return this.messageRepo.find({
      where: { sessionId, compactedAt: IsNull() },
      order: { createTime: 'ASC', id: 'ASC' },
    });
  }

  /** 删除会话及其消息 */
  async deleteSession(sessionId: string, agentId?: number): Promise<void> {
    const treeSession = await this.agentSessionRepo.findOneBy({ sessionId });
    const auditOrder = await this.auditOrderRepo.findOne({
      where: { sessionId },
      select: ['orderNo'],
    });
    const auditLabel = getAuditOrderSessionLabel(sessionId, treeSession?.metadata, auditOrder);
    if (auditLabel) {
      throw new Error(`审核订单会话不可删除：${auditLabel}`);
    }
    const treeAgentId = this.parseSessionTreeAgentId(treeSession?.agentId);
    const provider = await this.getSessionTreeProviderForAgent(treeAgentId ?? agentId);
    await provider.deleteSession(sessionId);
    await this.agentSessionEntryRepo.delete({ sessionId });
    await this.agentSessionRepo.delete({ sessionId });
    await this.subagentSessionRepo.delete({ sessionId });
    await this.messageRepo.delete({ sessionId });
    await this.sessionRepo.delete({ sessionId });
  }

  /** 删除用户所有会话 */
  async deleteAllSessions(userId?: string): Promise<void> {
    const where: any = {};
    if (userId) where.userId = userId;
    const sessions = await this.sessionRepo.find({ where });
    const treeSessions = userId
      ? await this.agentSessionRepo.find({ where: { userId } })
      : await this.agentSessionRepo.find();
    const deletableTreeSessions: NetaClawAgentSessionEntity[] = [];
    for (const session of treeSessions) {
      const auditOrder = await this.auditOrderRepo.findOne({
        where: { sessionId: session.sessionId },
        select: ['orderNo'],
      });
      const auditLabel = getAuditOrderSessionLabel(session.sessionId, session.metadata, auditOrder);
      if (!auditLabel) {
        deletableTreeSessions.push(session);
      }
    }

    const deletableSessionIds = new Set(deletableTreeSessions.map(session => session.sessionId));
    const treeSessionById = new Map(treeSessions.map(session => [session.sessionId, session]));
    const deletableLegacySessions: NetaClawSessionEntity[] = [];
    for (const session of sessions) {
      const treeSession = treeSessionById.get(session.sessionId);
      const auditOrder = await this.auditOrderRepo.findOne({
        where: { sessionId: session.sessionId },
        select: ['orderNo'],
      });
      const auditLabel = getAuditOrderSessionLabel(
        session.sessionId,
        (treeSession?.metadata || session.metadata) as Record<string, unknown> | null,
        auditOrder
      );
      if (!auditLabel) {
        deletableLegacySessions.push(session);
        deletableSessionIds.add(session.sessionId);
      }
    }

    for (const s of deletableLegacySessions) {
      await this.subagentSessionRepo.delete({ sessionId: s.sessionId });
      await this.messageRepo.delete({ sessionId: s.sessionId });
      await this.agentSessionEntryRepo.delete({ sessionId: s.sessionId });
      await this.agentSessionRepo.delete({ sessionId: s.sessionId });
    }

    for (const s of deletableTreeSessions) {
      if (!deletableSessionIds.has(s.sessionId)) continue;
      await this.agentSessionEntryRepo.delete({ sessionId: s.sessionId });
      await this.agentSessionRepo.delete({ sessionId: s.sessionId });
    }
    if (userId) {
      if (deletableLegacySessions.length > 0) {
        await this.sessionRepo.delete({
          sessionId: In(deletableLegacySessions.map(session => session.sessionId)),
        });
      }
      return;
    }

    if (deletableLegacySessions.length > 0) {
      await this.sessionRepo.delete({
        sessionId: In(deletableLegacySessions.map(session => session.sessionId)),
      });
    }
  }

  private mapMysqlTreeSessions(rows: NetaClawAgentSessionEntity[]): SessionTreeSession[] {
    return rows.map(session => ({
      sessionId: session.sessionId,
      provider: 'mysql',
      rootEntryId: session.rootEntryId ?? null,
      leafEntryId: session.leafEntryId ?? null,
      parentSessionId: session.parentSessionId ?? undefined,
      agentId: session.agentId ?? undefined,
      userId: session.userId ?? undefined,
      title: session.title ?? undefined,
      status: session.status ?? undefined,
      metadata: session.metadata ?? undefined,
      createdAt: session.createTime?.toISOString?.() ?? undefined,
      updatedAt: session.updateTime?.toISOString?.() ?? undefined,
    }));
  }

  private async listFileTreeSessions(rootDir: string | undefined, options?: { agentId?: number }): Promise<SessionTreeSession[]> {
    if (!rootDir) return [];

    let fileNames: string[];
    try {
      fileNames = await readdir(rootDir);
    } catch {
      return [];
    }

    const provider = createSessionTreeProvider({
      session: {
        backend: 'file',
        file: {
          rootDir,
        },
      },
    });

    const sessions: SessionTreeSession[] = [];
    for (const fileName of fileNames) {
      if (!fileName.endsWith('.jsonl')) continue;
      const sessionId = this.decodeSessionIdFromFileName(fileName);
      if (!sessionId) continue;

      try {
        const session = await provider.getSession(sessionId);
        if (!session) continue;
        if (options?.agentId && Number(session.agentId) !== options.agentId) continue;
        sessions.push(session);
      } catch {
        // Ignore malformed files so one bad session file does not block the whole list.
      }
    }

    return sessions.sort((a, b) => {
      const aTime = Date.parse(a.updatedAt || a.createdAt || '') || 0;
      const bTime = Date.parse(b.updatedAt || b.createdAt || '') || 0;
      return bTime - aTime;
    });
  }

  private parseSessionTreeAgentId(agentId: string | number | null | undefined): number | undefined {
    if (agentId === null || agentId === undefined || agentId === '') {
      return undefined;
    }
    const parsed = Number(agentId);
    return Number.isFinite(parsed) ? parsed : undefined;
  }

  private async assertSessionDeletable(sessionId: string, metadata?: Record<string, unknown> | null): Promise<void> {
    const auditOrder = await this.auditOrderRepo.findOne({
      where: { sessionId },
      select: ['id', 'orderNo', 'sessionId'],
    });
    if (auditOrder) {
      throw new Error(`审核订单会话不可删除：${auditOrder.orderNo || sessionId}`);
    }

    if (isAuditOrderSessionMetadata(metadata)) {
      throw new Error(`审核订单会话不可删除：${metadata.auditOrder.orderNo || sessionId}`);
    }
  }

  private decodeSessionIdFromFileName(fileName: string): string | null {
    const encoded = fileName.replace(/\.jsonl$/i, '');
    if (!encoded) return null;

    try {
      return Buffer.from(encoded, 'base64url').toString('utf8');
    } catch {
      return null;
    }
  }
}
