import initSqlJsModule from 'sql.js';
const initSqlJs = initSqlJsModule.default || initSqlJsModule;
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url';

const URL_RE = /https?:\/\/[^\s<>]+/i;
const MENTION_RE = /@(\w+)/g;

const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS messages (
  id TEXT PRIMARY KEY,
  channel_id TEXT NOT NULL,
  sender_id TEXT,
  username TEXT,
  content TEXT,
  created_at INTEGER,
  has_attachment INTEGER DEFAULT 0,
  has_link INTEGER DEFAULT 0,
  has_mention INTEGER DEFAULT 0,
  mentioned_users TEXT,
  attachment_filename TEXT,
  attachment_type TEXT,
  attachment_meta TEXT DEFAULT '',
  pinned INTEGER DEFAULT 0,
  reply_to_id TEXT
);

CREATE INDEX IF NOT EXISTS idx_channel ON messages(channel_id);
CREATE INDEX IF NOT EXISTS idx_sender ON messages(sender_id);
CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at);
`;

let sqlPromise = null;

function getSql() {
  if (!sqlPromise) {
    sqlPromise = initSqlJs({ locateFile: () => wasmUrl });
  }
  return sqlPromise;
}

export default class SearchDatabase {
  constructor(storageAdapter, cryptoAdapter) {
    this.storage = storageAdapter;
    this.crypto = cryptoAdapter;
    this.db = null;
    this.dbKey = null;
    this._dirty = false;
    this._saveTimer = null;
    this._opened = false;
    this._userId = null;
  }

  isOpen() {
    return this._opened && this.db !== null;
  }

  async open(dbKeyHex, userId) {
    if (this._opened) return;
    this.dbKey = dbKeyHex;
    this._userId = userId;

    const SQL = await getSql();

    // Try loading from storage
    let loaded = false;
    try {
      const blob = await this.storage.load(userId);
      if (blob && blob.length > 0) {
        const json = new TextDecoder().decode(blob);
        const { content, iv, tag } = JSON.parse(json);
        const decrypted = await this.crypto.decryptData(content, dbKeyHex, iv, tag);
        const bytes = hexToBytes(decrypted);
        this.db = new SQL.Database(bytes);
        loaded = true;
        // Drop old FTS5 artifacts from previous schema
        try {
          this.db.run('DROP TRIGGER IF EXISTS messages_ai');
          this.db.run('DROP TRIGGER IF EXISTS messages_ad');
          this.db.run('DROP TRIGGER IF EXISTS messages_au');
        } catch {}
        try { this.db.run('DROP TABLE IF EXISTS messages_fts'); } catch {}
        // Migrate: add attachment_meta column if missing
        try { this.db.run("ALTER TABLE messages ADD COLUMN attachment_meta TEXT DEFAULT ''"); } catch {}
        console.log('Search DB loaded from encrypted storage');
      }
    } catch (err) {
      console.warn('Search DB decrypt failed, starting fresh:', err.message);
    }

    if (!loaded) {
      this.db = new SQL.Database();
      this.db.run(SCHEMA_SQL);
      console.log('Search DB created fresh');
    }

    this._opened = true;
    this._dirty = false;
  }

  async close() {
    if (!this._opened) return;
    if (this._saveTimer) {
      clearTimeout(this._saveTimer);
      this._saveTimer = null;
    }
    if (this._dirty) {
      await this.save();
    }
    if (this.db) {
      this.db.close();
      this.db = null;
    }
    this.dbKey = null;
    this._opened = false;
    this._userId = null;
  }

  async save() {
    if (!this.db || !this.dbKey || !this._userId) return;
    try {
      const data = this.db.export();
      const hex = bytesToHex(data);
      const encrypted = await this.crypto.encryptData(hex, this.dbKey);
      const json = JSON.stringify({ content: encrypted.content, iv: encrypted.iv, tag: encrypted.tag });
      const bytes = new TextEncoder().encode(json);
      await this.storage.save(this._userId, bytes);
      this._dirty = false;
      console.log('Search DB saved');
    } catch (err) {
      console.error('Search DB save error:', err);
    }
  }

  _scheduleSave() {
    if (this._saveTimer) return;
    this._saveTimer = setTimeout(() => {
      this._saveTimer = null;
      this.save();
    }, 30000);
  }

  indexMessages(messages) {
    if (!this.db || messages.length === 0) return;

    this.db.run('BEGIN TRANSACTION');
    try {
      const stmt = this.db.prepare(
        `INSERT OR REPLACE INTO messages (id, channel_id, sender_id, username, content, created_at, has_attachment, has_link, has_mention, mentioned_users, attachment_filename, attachment_type, attachment_meta, pinned, reply_to_id)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
      );

      for (const msg of messages) {
        let content = msg.content || '';
        let hasAttachment = 0;
        let hasLink = 0;
        let hasMention = 0;
        let mentionedUsers = '';
        let attachmentFilename = '';
        let attachmentType = '';
        let attachmentMeta = '';

        // Parse attachment
        try {
          if (content.startsWith('{')) {
            const parsed = JSON.parse(content);
            if (parsed.type === 'attachment') {
              hasAttachment = 1;
              attachmentFilename = parsed.filename || '';
              attachmentType = parsed.mimeType || '';
              attachmentMeta = content;
              content = `[File: ${attachmentFilename}]`;
            }
          }
        } catch {}

        // Check for links
        if (URL_RE.test(content)) hasLink = 1;

        // Check for mentions
        const mentions = [];
        let m;
        while ((m = MENTION_RE.exec(content)) !== null) {
          mentions.push(m[1]);
        }
        if (mentions.length > 0) {
          hasMention = 1;
          mentionedUsers = mentions.join(',');
        }

        const createdAt = typeof msg.created_at === 'number'
          ? msg.created_at
          : new Date(msg.created_at).getTime();

        stmt.run([
          msg.id,
          msg.channel_id,
          msg.sender_id || null,
          msg.username || null,
          content,
          createdAt,
          hasAttachment,
          hasLink,
          hasMention,
          mentionedUsers || null,
          attachmentFilename || null,
          attachmentType || null,
          attachmentMeta || null,
          msg.pinned ? 1 : 0,
          msg.replyToId || null,
        ]);
      }

      stmt.free();
      this.db.run('COMMIT');
      this._dirty = true;
      this._scheduleSave();
    } catch (err) {
      try { this.db.run('ROLLBACK'); } catch {}
      console.error('Search DB indexing error:', err);
    }
  }

  isIndexed(messageId) {
    if (!this.db) return false;
    try {
      const stmt = this.db.prepare('SELECT 1 FROM messages WHERE id = ?');
      stmt.bind([messageId]);
      const found = stmt.step();
      stmt.free();
      return found;
    } catch {
      return false;
    }
  }

  search({ query, channelId, senderId, senderName, hasLink, hasAttachment, hasImage, hasVideo, hasFile, hasMention, before, after, pinned, limit = 50, offset = 0 }) {
    if (!this.db) return [];

    try {
      let sql, params = [];
      const conditions = [];
      let queryWords = [];

      if (query && query.trim()) {
        queryWords = query.trim().split(/\s+/).filter(Boolean);
        sql = `SELECT m.* FROM messages m WHERE 1=1`;
        for (const word of queryWords) {
          conditions.push('(m.content LIKE ? OR m.username LIKE ? OR m.attachment_filename LIKE ?)');
          const pattern = `%${word}%`;
          params.push(pattern, pattern, pattern);
        }
      } else {
        sql = `SELECT m.* FROM messages m WHERE 1=1`;
      }

      if (channelId) {
        conditions.push('m.channel_id = ?');
        params.push(channelId);
      }
      if (senderId) {
        conditions.push('m.sender_id = ?');
        params.push(senderId);
      }
      if (senderName) {
        conditions.push('m.username = ?');
        params.push(senderName);
      }
      if (hasLink) {
        conditions.push('m.has_link = 1');
      }
      if (hasAttachment) {
        conditions.push('m.has_attachment = 1');
      }
      if (hasImage) {
        conditions.push("m.has_attachment = 1 AND m.attachment_type LIKE 'image/%'");
      }
      if (hasVideo) {
        conditions.push("m.has_attachment = 1 AND m.attachment_type LIKE 'video/%'");
      }
      if (hasFile) {
        conditions.push("m.has_attachment = 1 AND m.attachment_type NOT LIKE 'image/%' AND m.attachment_type NOT LIKE 'video/%'");
      }
      if (hasMention) {
        conditions.push('m.has_mention = 1');
      }
      if (before) {
        conditions.push('m.created_at < ?');
        params.push(new Date(before).getTime());
      }
      if (after) {
        conditions.push('m.created_at > ?');
        params.push(new Date(after).getTime());
      }
      if (pinned) {
        conditions.push('m.pinned = 1');
      }

      if (conditions.length > 0) {
        sql += ' AND ' + conditions.join(' AND ');
      }

      sql += ' ORDER BY m.created_at DESC LIMIT ? OFFSET ?';
      params.push(limit, offset);

      const stmt = this.db.prepare(sql);
      stmt.bind(params);

      const results = [];
      while (stmt.step()) {
        const row = stmt.getAsObject();
        results.push({
          id: row.id,
          channel_id: row.channel_id,
          sender_id: row.sender_id,
          username: row.username,
          content: row.content,
          created_at: row.created_at,
          has_attachment: !!row.has_attachment,
          has_link: !!row.has_link,
          pinned: !!row.pinned,
          attachment_type: row.attachment_type || '',
          attachment_meta: row.attachment_meta || '',
          snippet: queryWords.length > 0
            ? generateSnippet(row.content || '', queryWords)
            : row.content,
          reply_to_id: row.reply_to_id,
        });
      }
      stmt.free();
      return results;
    } catch (err) {
      console.error('Search DB query error:', err);
      return [];
    }
  }

  getStats() {
    if (!this.db) return { count: 0 };
    try {
      const result = this.db.exec('SELECT COUNT(*) as cnt FROM messages');
      return { count: result[0]?.values[0]?.[0] || 0 };
    } catch {
      return { count: 0 };
    }
  }
}

function generateSnippet(content, queryWords) {
  if (!content) return '';
  const lower = content.toLowerCase();
  // Find earliest match position
  let firstIdx = content.length;
  for (const word of queryWords) {
    const idx = lower.indexOf(word.toLowerCase());
    if (idx !== -1 && idx < firstIdx) firstIdx = idx;
  }
  if (firstIdx === content.length) firstIdx = 0;

  // Extract ~80 chars of context around the match
  const start = Math.max(0, firstIdx - 40);
  const end = Math.min(content.length, firstIdx + 40);
  let slice = content.slice(start, end);
  if (start > 0) slice = '...' + slice;
  if (end < content.length) slice = slice + '...';

  // Escape HTML then wrap matches in <mark>
  slice = slice.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  for (const word of queryWords) {
    const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    slice = slice.replace(new RegExp(escaped, 'gi'), m => `<mark>${m}</mark>`);
  }
  return slice;
}

function hexToBytes(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
  }
  return bytes;
}

function bytesToHex(bytes) {
  let hex = '';
  for (let i = 0; i < bytes.length; i++) {
    hex += bytes[i].toString(16).padStart(2, '0');
  }
  return hex;
}
