import * as fs from 'node:fs';
import * as path from 'node:path';
import { Provide, Scope, ScopeEnum, Logger, Inject } from '@midwayjs/core';
import type { ILogger } from '@midwayjs/logger';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { resolveDataDir } from '../../../comm/data-dir.js';
import { NetaClawAgentChannelEntity } from '../entity/agent_channel.js';
import { NetaClawAgentChannelGroupEntity } from '../entity/agent_channel_group.js';
import { WeixinDbService } from './weixin_db.js';
import { ArchiveDb } from '../runtime/weixin_db/archive_db.js';
import { MessageRepo } from '../runtime/weixin_db/message_repo.js';
import { projectToArchive } from '../runtime/weixin_db/project_archive.js';
import { decryptDbToWorkDir } from '../runtime/weixin_db/archive_decryptor.js';

export interface SyncResult {
  newCount: number;
  totalCount: number;
  lastSyncTs: number;
  durationMs: number;
}

const BATCH_LIMIT = 500;

/**
 * 微信群聊历史归档同步主服务。
 *
 * 流程:
 *  1. 校验 group / channel / runtime,通过 RoomResolver 反查 RoomInfo
 *  2. 在 cid-<id>/archive 隔离 workDir 解密 message_0.db 副本
 *  3. 以 sync_state.lastSyncTs 为水位,分批 listSince → projectToArchive → insertMessages
 *  4. 写 sync_state(成功更新水位 / 失败回写 lastError)
 *
 * 单写者 + 串行化:每个 channelId 维护一个串行 Promise 链,避免对同一 archive db 并发写。
 */
@Provide()
@Scope(ScopeEnum.Singleton)
export class WeixinArchiveSyncService {
  @Logger() logger: ILogger;

  @InjectEntityModel(NetaClawAgentChannelEntity)
  channelRepo: Repository<NetaClawAgentChannelEntity>;

  @InjectEntityModel(NetaClawAgentChannelGroupEntity)
  groupRepo: Repository<NetaClawAgentChannelGroupEntity>;

  @Inject() weixinDbService: WeixinDbService;

  /** 测试钩子,默认实现委托给真实模块 */
  platform: NodeJS.Platform = process.platform;
  resolveDataDir = (): string => resolveDataDir();
  decryptDb = (srcDb: string, rawKey: Buffer, workDir: string): string =>
    decryptDbToWorkDir({ srcDb, rawKey, workDir });
  openMessageRepo = (plainDb: string): {
    listSince(tableName: string, lastTs: number, limit?: number): Promise<any[]>;
    close(): void;
  } => new MessageRepo(plainDb);

  private readonly channelLocks = new Map<number, Promise<void>>();

  async syncGroup(groupId: number): Promise<SyncResult> {
    const group = await this.groupRepo.findOne({ where: { id: groupId } });
    if (!group) throw new Error('group-not-found');

    const cid = group.channelId;
    const prev = this.channelLocks.get(cid) ?? Promise.resolve();
    let release: () => void;
    const next = new Promise<void>(r => (release = r));
    this.channelLocks.set(cid, prev.then(() => next));

    await prev;
    try {
      return await this.doSyncGroup(group);
    } finally {
      release!();
      if (this.channelLocks.get(cid) === next) this.channelLocks.delete(cid);
    }
  }

  async deleteChannelArchive(channelId: number): Promise<void> {
    const file = ArchiveDb.archivePath(channelId, this.resolveDataDir());
    for (const suffix of ['', '-wal', '-shm']) {
      const p = file + suffix;
      if (fs.existsSync(p)) {
        try { fs.unlinkSync(p); }
        catch (err: any) {
          this.logger.warn('[archive] rm %s failed: %s', p, err?.message || err);
        }
      }
    }
  }

  private async doSyncGroup(group: NetaClawAgentChannelGroupEntity): Promise<SyncResult> {
    if (this.platform !== 'win32') throw new Error('unsupported-platform');

    const channel = await this.channelRepo.findOne({ where: { id: group.channelId } });
    if (!channel) throw new Error('channel-not-found');
    if (channel.type !== 'weixin-db') throw new Error('unsupported-channel-type');
    if (channel.loginStatus !== 'connected') throw new Error('channel-not-connected');

    const runtime = this.weixinDbService.getRuntime(channel.id);
    if (!runtime) throw new Error('channel-not-bound');

    const resolver = runtime.reader.getRoomResolver();
    const matches = resolver.findRoomsByName(group.roomName ?? '');
    if (matches.length === 0) throw new Error('room-not-found');
    if (matches.length > 1) {
      this.logger.warn('[archive] %d rooms with same name=%s, using first', matches.length, group.roomName);
    }
    const room = matches[0];

    const dataDir = this.resolveDataDir();
    const workDir = path.join(dataDir, 'weixin-db-work', `cid-${channel.id}`, 'archive');

    const t0 = Date.now();
    const archive = ArchiveDb.openOrCreate(channel.id, dataDir);
    let cursorTs = archive.getSyncState(room.username)?.lastSyncTs ?? 0;
    let newCount = 0;

    let plainDb: string;
    try {
      plainDb = this.decryptDb(runtime.paths.messageDb, runtime.messageKey, workDir);
    } catch (err: any) {
      this.logger.error('[archive] decrypt failed cid=%s: %s', channel.id, err?.message || err);
      this.tryWriteLastError(archive, room.username, room.roomName, `decrypt-failed: ${err?.message || err}`);
      archive.close();
      throw new Error('decrypt-failed');
    }

    let repo: ReturnType<WeixinArchiveSyncService['openMessageRepo']> | null = null;
    try {
      repo = this.openMessageRepo(plainDb);
      while (true) {
        const rows = await repo.listSince(room.tableName, cursorTs, BATCH_LIMIT);
        if (!rows || rows.length === 0) break;
        const archiveRows = rows.map(r => projectToArchive(
          r,
          channel.id,
          room,
          username => resolver.resolveContact?.(username),
          (roomUsername, realSenderId) => resolver.resolveRoomSenderIdentity?.(roomUsername, realSenderId),
        ));
        newCount += archive.insertMessages(archiveRows);
        cursorTs = rows[rows.length - 1].createTime;
        if (rows.length < BATCH_LIMIT) break;
      }

      const totalCount = archive.countMessages(room.username);
      archive.updateSyncState(room.username, {
        roomName: room.roomName,
        lastSyncTs: cursorTs,
        lastSyncCount: newCount,
        totalCount,
        lastSyncAt: Math.floor(Date.now() / 1000),
        lastError: null,
      });
      const durationMs = Date.now() - t0;
      this.logger.info('[archive] sync cid=%s room=%s new=%d total=%d ts=%d in %dms',
        channel.id, group.roomName, newCount, totalCount, cursorTs, durationMs);
      return { newCount, totalCount, lastSyncTs: cursorTs, durationMs };
    } catch (err: any) {
      this.tryWriteLastError(archive, room.username, room.roomName, err?.message || String(err));
      throw err;
    } finally {
      try { repo?.close(); } catch { /* ignore */ }
      try { archive.close(); } catch { /* ignore */ }
    }
  }

  private tryWriteLastError(
    archive: ArchiveDb,
    roomId: string,
    roomName: string | null,
    message: string,
  ): void {
    try {
      archive.updateSyncState(roomId, {
        roomName,
        lastSyncAt: Math.floor(Date.now() / 1000),
        lastError: message,
      });
    } catch (e: any) {
      this.logger.warn('[archive] write lastError failed: %s', e?.message || e);
    }
  }
}
