import * as path from 'node:path';
import * as fs from 'node:fs';
import { Provide, Scope, ScopeEnum, Init, Logger } 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 { DbPaths } from '../runtime/weixin_db/db_paths.js';
import { extractWeixinKeys } from '../runtime/weixin_db/key_extractor.js';
import { IncrementalReader } from '../runtime/weixin_db/incremental_reader.js';
import { WalWatcher } from '../runtime/weixin_db/wal_watcher.js';
import { buildPseudoMessageFromDb } from '../runtime/weixin_db/build_pseudo.js';
import { NetaClawAgentChannelEntity } from '../entity/agent_channel.js';
import { NetaClawAgentChannelGroupEntity } from '../entity/agent_channel_group.js';

export interface ChannelRuntime {
  channelId: number;
  reader: IncrementalReader;
  watcher: WalWatcher;
  weixinPid: number;
  paths: DbPaths;
  messageKey: Buffer;
  onInbound: (channelId: number, pseudo: unknown) => Promise<void>;
}

const HEALTH_INTERVAL_MS = 60_000;

/**
 * weixin-db 主服务 (架构 C)。
 * - bindChannel: 抽 key → 启 WalWatcher (agent_channel.runLoop 调用)
 * - unbindChannel: 停 watcher + 清缓存
 * - v4: replyToGroup 已删除(reply agent 通过 delegate_task 委托 desktop agent 发送)
 * - 健康探针: 每 60s 检测 Weixin 进程仍在 + DB 可读;失败自动 unbind+rebind
 * - 跨平台: 非 Windows 直接 skip,标 loginStatus='unsupported_platform',不 crash
 */
@Provide()
@Scope(ScopeEnum.Singleton)
export class WeixinDbService {
  @Logger() logger: ILogger;

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

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

  private readonly runtimes = new Map<number, ChannelRuntime>();
  private healthTimer: NodeJS.Timeout | null = null;
  private whitelistCache = new Map<number, Set<string>>();

  @Init()
  async onInit(): Promise<void> {
    if (process.platform !== 'win32') {
      this.logger.info('[weixin-db] non-Windows platform (%s), service idle', process.platform);
      return;
    }
    this.healthTimer = setInterval(
      () => this.healthCheck().catch(err => this.logger.error('[weixin-db] healthCheck err: %s', err.message)),
      HEALTH_INTERVAL_MS
    );
  }

  async bindChannel(
    channel: NetaClawAgentChannelEntity,
    onInbound: (channelId: number, pseudo: unknown) => Promise<void>,
  ): Promise<void> {
    if (process.platform !== 'win32') {
      this.logger.warn('[weixin-db] non-Windows platform, channel %s skipped', channel.id);
      await this.channelRepo.update({ id: channel.id }, { loginStatus: 'unsupported_platform' });
      return;
    }
    if (this.runtimes.has(channel.id)) return;

    const seedDir = DbPaths.autoFindSeedDir();
    if (!seedDir) {
      this.logger.warn('[weixin-db] xwechat_files 未找到, channel %s loginStatus=disconnected', channel.id);
      await this.channelRepo.update({ id: channel.id }, { loginStatus: 'disconnected' });
      return;
    }
    const paths = new DbPaths(seedDir);

    let keys;
    try {
      keys = await extractWeixinKeys(seedDir);
    } catch (err: any) {
      this.logger.warn('[weixin-db] key 抽取失败 cid=%s: %s', channel.id, err.message);
      await this.channelRepo.update({ id: channel.id }, { loginStatus: 'disconnected', lastError: err.message });
      return;
    }
    const msgKeyHex = keys.dbKeys['message_0.db'];
    const sessionKeyHex = keys.dbKeys['session.db'];
    const contactKeyHex = keys.dbKeys['contact.db'];
    if (!msgKeyHex || !sessionKeyHex || !contactKeyHex) {
      this.logger.warn('[weixin-db] 抽不到 message/session/contact key, channel %s', channel.id);
      await this.channelRepo.update({ id: channel.id }, { loginStatus: 'disconnected' });
      return;
    }

    const dataDir = resolveDataDir();
    const workDir = path.join(dataDir, 'weixin-db-work', `cid-${channel.id}`);
    fs.mkdirSync(workDir, { recursive: true });

    await this.ensureWhitelistLoaded(channel.id);

    const reader = new IncrementalReader({
      dbPaths: paths,
      messageDbKey: Buffer.from(msgKeyHex, 'hex'),
      sessionDbKey: Buffer.from(sessionKeyHex, 'hex'),
      contactDbKey: Buffer.from(contactKeyHex, 'hex'),
      workDir,
      whitelistRoomNames: () => this.currentWhitelistSync(channel.id),
    });
    await reader.initialize();

    const watcher = new WalWatcher(paths.messageDb, async () => {
      try {
        const rows = await reader.readIncrement();
        const wl = this.currentWhitelistSync(channel.id);
        this.logger.debug(
          '[weixin-db] cid=%s WAL tick: rows=%s whitelist=%j',
          channel.id, rows.length, [...wl],
        );
        for (const row of rows) {
          const room = reader.resolveRoom(row.tableName);
          if (!room) continue;
          const pseudo = buildPseudoMessageFromDb({
            channelId: channel.id,
            row,
            room,
            resolveContact: username => reader.getRoomResolver().resolveContact(username),
            resolveRoomSenderIdentity: (roomUsername, realSenderId) =>
              reader.getRoomResolver().resolveRoomSenderIdentity(roomUsername, realSenderId),
          });
          await onInbound(channel.id, pseudo);
        }
      } catch (err: any) {
        this.logger.error('[weixin-db] incremental read failed cid=%s: %s', channel.id, err.message);
      }
    });
    watcher.start();

    this.runtimes.set(channel.id, {
      channelId: channel.id, reader, watcher,
      weixinPid: keys.pid, paths,
      messageKey: Buffer.from(msgKeyHex, 'hex'),
      onInbound,
    });
    await this.channelRepo.update({ id: channel.id }, {
      loginStatus: 'connected',
      // 用户手填的 wxid 是权威源, 脚本回填仅在用户没填时兜底
      credential: {
        ...(channel.credential || {}),
        wxid: (channel.credential as any)?.wxid || keys.wxid,
        wechatVersion: keys.wechatVersion,
      },
      lastConnectedAt: new Date().toISOString(),
    });
    this.logger.info('[weixin-db] channel %s bound, wxid=%s ver=%s pid=%s',
      channel.id, keys.wxid, keys.wechatVersion, keys.pid);
  }

  /**
   * 暴露已 bind 的 runtime 给同进程内其他服务(如 weixin_archive_sync)。
   * 未 bind 返回 undefined。
   */
  getRuntime(channelId: number): ChannelRuntime | undefined {
    return this.runtimes.get(channelId);
  }

  unbindChannel(channelId: number): void {
    const r = this.runtimes.get(channelId);
    if (!r) return;
    r.watcher.stop();
    this.runtimes.delete(channelId);
    this.logger.info('[weixin-db] channel %s unbound', channelId);
  }

  /** 健康探针:每 60s 调一次,检测 Weixin 进程仍在 + DB 可读;失败 unbind+rebind。 */
  private async healthCheck(): Promise<void> {
    for (const [cid, runtime] of this.runtimes) {
      const ok = this.probeAlive(runtime);
      if (!ok) {
        this.logger.warn('[weixin-db] cid=%s health failed, rebinding', cid);
        this.unbindChannel(cid);
        try {
          const channel = await this.channelRepo.findOne({ where: { id: cid } });
          if (channel) await this.bindChannel(channel, runtime.onInbound);
        } catch (err: any) {
          this.logger.error('[weixin-db] rebind cid=%s failed: %s', cid, err.message);
        }
      }
    }
  }

  private probeAlive(runtime: ChannelRuntime): boolean {
    try {
      fs.accessSync(runtime.paths.messageDb, fs.constants.R_OK);
    } catch { return false; }
    try {
      process.kill(runtime.weixinPid, 0);
      return true;
    } catch { return false; }
  }

  private currentWhitelistSync(channelId: number): Set<string> {
    return this.whitelistCache.get(channelId) ?? new Set();
  }

  /** 外部(controller /add /toggle /delete)改动白名单后调用。 */
  async refreshWhitelist(channelId: number): Promise<void> {
    const groups = await this.groupRepo.find({
      where: { channelId, status: 1 },
    });
    const names = new Set<string>();
    for (const g of groups) {
      if (g.roomName && g.roomName.trim()) names.add(g.roomName.trim());
    }
    this.whitelistCache.set(channelId, names);
  }

  /** 供 healthCheck / bindChannel 内部用: 确保在启动 reader 前缓存已 warm。 */
  async ensureWhitelistLoaded(channelId: number): Promise<void> {
    if (!this.whitelistCache.has(channelId)) {
      await this.refreshWhitelist(channelId);
    }
  }
}
