import Database = require('better-sqlite3');
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { ArchiveMessage, SyncState } from './archive_types.js';

const CREATE_MESSAGE_SQL = `
CREATE TABLE IF NOT EXISTS message (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  channel_id    INTEGER NOT NULL,
  room_id       TEXT    NOT NULL,
  room_name     TEXT,
  msg_table     TEXT    NOT NULL,
  local_id      INTEGER NOT NULL,
  server_id     TEXT,
  local_type    INTEGER NOT NULL,
  sender_wxid   TEXT,
  sender_remark TEXT,
  sender_nick_name TEXT,
  sender_display_name TEXT,
  create_time   INTEGER NOT NULL,
  content       TEXT,
  raw_hex       TEXT,
  synced_at     INTEGER NOT NULL,
  UNIQUE(room_id, local_id)
)`;

const CREATE_INDEX_ROOM_TIME = `
CREATE INDEX IF NOT EXISTS idx_msg_room_time ON message(room_id, create_time DESC)`;

const CREATE_INDEX_TIME = `
CREATE INDEX IF NOT EXISTS idx_msg_create_time ON message(create_time DESC)`;

const CREATE_SYNC_STATE_SQL = `
CREATE TABLE IF NOT EXISTS sync_state (
  room_id          TEXT PRIMARY KEY,
  room_name        TEXT,
  last_sync_ts     INTEGER NOT NULL DEFAULT 0,
  last_sync_count  INTEGER NOT NULL DEFAULT 0,
  total_count      INTEGER NOT NULL DEFAULT 0,
  last_sync_at     INTEGER NOT NULL,
  last_error       TEXT
)`;

const MESSAGE_SCHEMA_COLUMNS: Record<string, string> = {
  sender_remark: 'TEXT',
  sender_nick_name: 'TEXT',
  sender_display_name: 'TEXT',
};

/**
 * 每个 channel 一个 archive sqlite 文件。
 * 文件路径: `<dataDir>/weixin-archive/cid-<channelId>.db`
 * Single-writer 模式: 调用方通过 channel-level mutex 串行化写入。
 */
export class ArchiveDb {
  private constructor(private readonly db: Database.Database) {}

  static archivePath(channelId: number, dataDir: string): string {
    return path.join(dataDir, 'weixin-archive', `cid-${channelId}.db`);
  }

  static openOrCreate(channelId: number, dataDir: string): ArchiveDb {
    const file = ArchiveDb.archivePath(channelId, dataDir);
    fs.mkdirSync(path.dirname(file), { recursive: true });
    const db = new Database(file);
    db.pragma('journal_mode = WAL');
    db.pragma('synchronous = NORMAL');
    db.exec(CREATE_MESSAGE_SQL);
    ArchiveDb.ensureMessageColumns(db);
    db.exec(CREATE_INDEX_ROOM_TIME);
    db.exec(CREATE_INDEX_TIME);
    db.exec(CREATE_SYNC_STATE_SQL);
    return new ArchiveDb(db);
  }

  private static ensureMessageColumns(db: Database.Database): void {
    const cols = db.prepare('PRAGMA table_info(message)').all() as { name: string }[];
    const existing = new Set(cols.map(c => c.name));
    for (const [name, type] of Object.entries(MESSAGE_SCHEMA_COLUMNS)) {
      if (!existing.has(name)) {
        db.exec(`ALTER TABLE message ADD COLUMN ${name} ${type}`);
      }
    }
  }

  insertMessages(rows: ArchiveMessage[]): number {
    if (rows.length === 0) return 0;
    const now = Math.floor(Date.now() / 1000);
    const stmt = this.db.prepare(`
      INSERT OR IGNORE INTO message
        (channel_id, room_id, room_name, msg_table, local_id, server_id, local_type,
         sender_wxid, sender_remark, sender_nick_name, sender_display_name,
         create_time, content, raw_hex, synced_at)
      VALUES
        (@channelId, @roomId, @roomName, @msgTable, @localId, @serverId, @localType,
         @senderWxid, @senderRemark, @senderNickName, @senderDisplayName,
         @createTime, @content, @rawHex, @syncedAt)
    `);
    const backfillIdentityStmt = this.db.prepare(`
      UPDATE message SET
        sender_wxid = CASE WHEN @senderWxid IS NOT NULL THEN @senderWxid ELSE sender_wxid END,
        sender_remark = CASE WHEN @senderRemark IS NOT NULL THEN @senderRemark ELSE sender_remark END,
        sender_nick_name = CASE WHEN @senderNickName IS NOT NULL THEN @senderNickName ELSE sender_nick_name END,
        sender_display_name = CASE WHEN @senderDisplayName IS NOT NULL THEN @senderDisplayName ELSE sender_display_name END
      WHERE room_id = @roomId AND local_id = @localId
    `);
    const txn = this.db.transaction((batch: ArchiveMessage[]) => {
      let inserted = 0;
      for (const r of batch) {
        const params = {
          channelId: r.channelId,
          roomId: r.roomId,
          roomName: r.roomName,
          msgTable: r.msgTable,
          localId: r.localId,
          serverId: r.serverId,
          localType: r.localType,
          senderWxid: r.senderWxid,
          senderRemark: r.senderRemark,
          senderNickName: r.senderNickName,
          senderDisplayName: r.senderDisplayName,
          createTime: r.createTime,
          content: r.content,
          rawHex: r.rawHex,
          syncedAt: now,
        };
        const info = stmt.run(params);
        if (info.changes > 0) inserted++;
        else backfillIdentityStmt.run(params);
      }
      return inserted;
    });
    return txn(rows);
  }

  getSyncState(roomId: string): SyncState | null {
    const r = this.db.prepare(`
      SELECT room_id, room_name, last_sync_ts, last_sync_count, total_count, last_sync_at, last_error
      FROM sync_state WHERE room_id = ?
    `).get(roomId) as any;
    if (!r) return null;
    return {
      roomId: r.room_id,
      roomName: r.room_name,
      lastSyncTs: r.last_sync_ts,
      lastSyncCount: r.last_sync_count,
      totalCount: r.total_count,
      lastSyncAt: r.last_sync_at,
      lastError: r.last_error,
    };
  }

  updateSyncState(roomId: string, patch: Partial<Omit<SyncState, 'roomId'>>): void {
    const existing = this.getSyncState(roomId);
    const merged: SyncState = existing
      ? {
          roomId,
          roomName: patch.roomName !== undefined ? patch.roomName : existing.roomName,
          lastSyncTs: patch.lastSyncTs !== undefined ? patch.lastSyncTs : existing.lastSyncTs,
          lastSyncCount: patch.lastSyncCount !== undefined ? patch.lastSyncCount : existing.lastSyncCount,
          totalCount: patch.totalCount !== undefined ? patch.totalCount : existing.totalCount,
          lastSyncAt: patch.lastSyncAt !== undefined ? patch.lastSyncAt : existing.lastSyncAt,
          lastError: patch.lastError !== undefined ? patch.lastError : existing.lastError,
        }
      : {
          roomId,
          roomName: patch.roomName ?? null,
          lastSyncTs: patch.lastSyncTs ?? 0,
          lastSyncCount: patch.lastSyncCount ?? 0,
          totalCount: patch.totalCount ?? 0,
          lastSyncAt: patch.lastSyncAt ?? Math.floor(Date.now() / 1000),
          lastError: patch.lastError ?? null,
        };
    this.db.prepare(`
      INSERT INTO sync_state
        (room_id, room_name, last_sync_ts, last_sync_count, total_count, last_sync_at, last_error)
      VALUES (?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(room_id) DO UPDATE SET
        room_name       = excluded.room_name,
        last_sync_ts    = excluded.last_sync_ts,
        last_sync_count = excluded.last_sync_count,
        total_count     = excluded.total_count,
        last_sync_at    = excluded.last_sync_at,
        last_error      = excluded.last_error
    `).run(
      merged.roomId,
      merged.roomName,
      merged.lastSyncTs,
      merged.lastSyncCount,
      merged.totalCount,
      merged.lastSyncAt,
      merged.lastError,
    );
  }

  countMessages(roomId: string): number {
    const r = this.db.prepare(`SELECT COUNT(*) AS c FROM message WHERE room_id = ?`).get(roomId) as any;
    return r.c as number;
  }

  close(): void {
    this.db.close();
  }
}
