import * as Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import { resolveDataDir } from '../../../comm/data-dir.js';
import {
  MemoryProvider,
  MemoryEntry,
  MemorySearchOpts,
  MemoryPageOpts,
  MemoryPageResult,
  MemoryCountOpts,
} from './provider.js';

const INIT_SQL = `
CREATE TABLE IF NOT EXISTS memory (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  agent_name TEXT NOT NULL,
  user_id TEXT NOT NULL,
  type TEXT NOT NULL,
  name TEXT NOT NULL,
  content TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  metadata TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
`;

const FTS_SQL = `
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
  name, content, description,
  content='memory', content_rowid='id',
  tokenize='trigram'
);
`;

const TRIGGER_SQL = `
CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON memory BEGIN
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
END;

CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON memory BEGIN
  INSERT INTO memory_fts(memory_fts, rowid, name, content, description)
  VALUES ('delete', old.id, old.name, old.content, old.description);
  INSERT INTO memory_fts(rowid, name, content, description)
  VALUES (new.id, new.name, new.content, new.description);
END;
`;

function toEntry(row: any): MemoryEntry {
  let metadata: Record<string, any> | undefined;
  if (row.metadata) {
    try {
      metadata = JSON.parse(row.metadata);
    } catch {
      metadata = undefined;
    }
  }

  return {
    id: row.id,
    agentName: row.agent_name,
    userId: row.user_id,
    type: row.type,
    name: row.name,
    content: row.content,
    description: row.description,
    metadata,
    createdAt: new Date(row.created_at),
    updatedAt: new Date(row.updated_at),
  };
}

export class SqliteMemoryProvider implements MemoryProvider {
  private db: Database.Database;

  constructor(dbPath?: string) {
    const resolvedPath =
      dbPath ?? path.join(resolveDataDir(), 'memory', 'memory.db');
    fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
    this.db = new Database(resolvedPath);
    this.db.pragma('journal_mode = WAL');
    this.db.pragma('busy_timeout = 5000');
    this.migrateCheckConstraint();
    this.db.exec(INIT_SQL);
    this.db.exec(FTS_SQL);
    this.db.exec(TRIGGER_SQL);
    this.db
      .prepare("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')")
      .run();
  }

  private migrateCheckConstraint(): void {
    const row = this.db
      .prepare(
        "SELECT sql FROM sqlite_master WHERE type='table' AND name='memory'"
      )
      .get() as { sql: string } | undefined;
    if (
      !row ||
      !row.sql.includes(
        "CHECK(type IN ('user', 'project', 'feedback', 'reference'))"
      )
    )
      return;

    const migrate = this.db.transaction(() => {
      this.db.exec(`
        CREATE TABLE memory_new (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          agent_name TEXT NOT NULL,
          user_id TEXT NOT NULL,
          type TEXT NOT NULL,
          name TEXT NOT NULL,
          content TEXT NOT NULL,
          description TEXT NOT NULL DEFAULT '',
          metadata TEXT,
          created_at TEXT DEFAULT (datetime('now')),
          updated_at TEXT DEFAULT (datetime('now'))
        );
        INSERT INTO memory_new (id, agent_name, user_id, type, name, content, description, metadata, created_at, updated_at)
        SELECT id, agent_name, user_id, type, name, content, description, metadata, created_at, updated_at FROM memory;
        DROP TABLE memory;
        ALTER TABLE memory_new RENAME TO memory;
        CREATE INDEX IF NOT EXISTS idx_agent_user ON memory(agent_name, user_id);
      `);
      this.db.exec('DROP TABLE IF EXISTS memory_fts');
    });

    migrate();
  }

  async save(
    entry: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>
  ): Promise<MemoryEntry> {
    const stmt = this.db.prepare(`
      INSERT INTO memory (agent_name, user_id, type, name, content, description, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
    const info = stmt.run(
      entry.agentName,
      entry.userId,
      entry.type,
      entry.name,
      entry.content,
      entry.description ?? '',
      entry.metadata ? JSON.stringify(entry.metadata) : null
    );
    return (await this.getById(info.lastInsertRowid as number))!;
  }

  async update(
    id: number,
    partial: Partial<
      Pick<
        MemoryEntry,
        'name' | 'content' | 'description' | 'type' | 'metadata'
      >
    >
  ): Promise<MemoryEntry> {
    const sets: string[] = [];
    const values: any[] = [];
    if (partial.name !== undefined) {
      sets.push('name = ?');
      values.push(partial.name);
    }
    if (partial.content !== undefined) {
      sets.push('content = ?');
      values.push(partial.content);
    }
    if (partial.description !== undefined) {
      sets.push('description = ?');
      values.push(partial.description);
    }
    if (partial.type !== undefined) {
      sets.push('type = ?');
      values.push(partial.type);
    }
    if (partial.metadata !== undefined) {
      sets.push('metadata = ?');
      values.push(JSON.stringify(partial.metadata));
    }
    sets.push("updated_at = datetime('now')");
    values.push(id);
    this.db
      .prepare(`UPDATE memory SET ${sets.join(', ')} WHERE id = ?`)
      .run(...values);
    return (await this.getById(id))!;
  }

  async delete(id: number): Promise<void> {
    this.db.prepare('DELETE FROM memory WHERE id = ?').run(id);
  }

  async search(query: string, opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    const limit = opts.limit ?? 10;
    const ftsQuery = `"${query.replace(/"/g, '""')}"`;
    let sql = `
      SELECT m.* FROM memory m
      JOIN memory_fts f ON m.id = f.rowid
      WHERE memory_fts MATCH ?
        AND m.agent_name = ?
    `;
    const params: any[] = [ftsQuery, opts.agentName];
    if (opts.userId) {
      sql += ' AND m.user_id = ?';
      params.push(opts.userId);
    }
    if (opts.type) {
      sql += ' AND m.type = ?';
      params.push(opts.type);
    }
    sql += ' ORDER BY rank LIMIT ?';
    params.push(limit);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async list(opts: MemorySearchOpts): Promise<MemoryEntry[]> {
    let sql = 'SELECT * FROM memory WHERE agent_name = ?';
    const params: any[] = [opts.agentName];
    if (opts.userId) {
      sql += ' AND user_id = ?';
      params.push(opts.userId);
    }
    if (opts.type) {
      sql += ' AND type = ?';
      params.push(opts.type);
    }
    sql += ' ORDER BY updated_at DESC LIMIT ?';
    params.push(opts.limit ?? 10);
    const rows = this.db.prepare(sql).all(...params);
    return rows.map(toEntry);
  }

  async page(opts: MemoryPageOpts): Promise<MemoryPageResult> {
    let countSql = 'SELECT COUNT(*) as total FROM memory WHERE 1=1';
    let dataSql = 'SELECT * FROM memory WHERE 1=1';
    const params: any[] = [];
    const countParams: any[] = [];

    if (opts.agentName) {
      countSql += ' AND agent_name = ?';
      dataSql += ' AND agent_name = ?';
      params.push(opts.agentName);
      countParams.push(opts.agentName);
    }
    if (opts.userId) {
      countSql += ' AND user_id = ?';
      dataSql += ' AND user_id = ?';
      params.push(opts.userId);
      countParams.push(opts.userId);
    }
    if (opts.type) {
      countSql += ' AND type = ?';
      dataSql += ' AND type = ?';
      params.push(opts.type);
      countParams.push(opts.type);
    }
    if (opts.keyWord) {
      const kw = `%${opts.keyWord}%`;
      countSql += ' AND (name LIKE ? OR content LIKE ? OR description LIKE ?)';
      dataSql += ' AND (name LIKE ? OR content LIKE ? OR description LIKE ?)';
      params.push(kw, kw, kw);
      countParams.push(kw, kw, kw);
    }

    const { total } = this.db.prepare(countSql).get(...countParams) as {
      total: number;
    };
    dataSql += ' ORDER BY updated_at DESC LIMIT ? OFFSET ?';
    params.push(opts.size, (opts.page - 1) * opts.size);
    const rows = this.db.prepare(dataSql).all(...params);
    return {
      list: rows.map(toEntry),
      pagination: { page: opts.page, size: opts.size, total },
    };
  }

  async count(opts: MemoryCountOpts): Promise<number> {
    let sql = 'SELECT COUNT(*) as total FROM memory WHERE 1=1';
    const params: any[] = [];
    if (opts.agentName) {
      sql += ' AND agent_name = ?';
      params.push(opts.agentName);
    }
    if (opts.userId) {
      sql += ' AND user_id = ?';
      params.push(opts.userId);
    }
    if (opts.type) {
      sql += ' AND type = ?';
      params.push(opts.type);
    }
    if (opts.keyWord) {
      const kw = `%${opts.keyWord}%`;
      sql += ' AND (name LIKE ? OR content LIKE ? OR description LIKE ?)';
      params.push(kw, kw, kw);
    }
    const { total } = this.db.prepare(sql).get(...params) as { total: number };
    return total;
  }

  async getById(id: number): Promise<MemoryEntry | null> {
    const row = this.db.prepare('SELECT * FROM memory WHERE id = ?').get(id);
    return row ? toEntry(row) : null;
  }

  async close(): Promise<void> {
    this.db.close();
  }
}
