'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const Database = require('better-sqlite3');

const CURSOR_HOME = () => path.join(os.homedir(), '.cursor');
const CACHE = new Map();
const SHA256_HEX_RE = /^[a-f0-9]{64}$/i;

function _decodeMaybeHexJson(value) {
  if (value == null) return null;
  const raw = Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
  const trimmed = raw.trim();
  const candidates = [trimmed];
  if (/^[0-9a-f]+$/i.test(trimmed) && trimmed.length % 2 === 0) {
    try { candidates.unshift(Buffer.from(trimmed, 'hex').toString('utf8')); } catch {}
  }
  for (const value of candidates) {
    const candidate = String(value || '').trim();
    if (!candidate || candidate[0] !== '{') continue;
    try { return JSON.parse(candidate); } catch {}
  }
  return null;
}

function _readMeta(db) {
  const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('0')
    || db.prepare('SELECT value FROM meta LIMIT 1').get();
  return _decodeMaybeHexJson(row?.value);
}

function _bufferFromBlob(value) {
  if (Buffer.isBuffer(value)) return value;
  if (value instanceof Uint8Array) return Buffer.from(value);
  return Buffer.from(String(value || ''), 'utf8');
}

function _orderedBlobRefs(buffer, idSet) {
  if (!Buffer.isBuffer(buffer) || buffer.length < 32 || idSet.size === 0) return [];
  // Find every 32-byte blob-id (graph edge) embedded at any byte offset, in order of first
  // appearance. The naive form allocated a 32-byte subarray AND hex-encoded it at EVERY
  // offset (O(n) Buffer.subarray + hexSlice) — the dominant main-thread freeze in CPU
  // profiles (~26% of non-idle: hexSlice + FastBuffer churn). Hex-encode the WHOLE buffer
  // ONCE (one native call) then slide a 64-char window over the hex string at byte-aligned
  // (even) offsets: no per-offset Buffer allocation, ~3× faster, and O(n) regardless of how
  // many blob ids the session has (the per-id Buffer.indexOf alternative degrades to O(ids×n)
  // as conversations grow). Output is byte-for-byte identical to the naive scan.
  const hexAll = buffer.toString('hex');
  const end = hexAll.length - 64; // byte offset i ↔ hex offset 2i; window is 64 hex chars
  const refs = [];
  const seen = new Set();
  for (let h = 0; h <= end; h += 2) {
    const hex = hexAll.slice(h, h + 64);
    if (!idSet.has(hex) || seen.has(hex)) continue;
    seen.add(hex);
    refs.push(hex);
  }
  return refs;
}

function _reachableBlobIds(rootBlobId, blobsById) {
  const root = String(rootBlobId || '').trim();
  if (!SHA256_HEX_RE.test(root) || !blobsById.has(root)) return [];
  const idSet = new Set(blobsById.keys());
  const seen = new Set([root]);
  const order = [];
  const queue = [root];
  while (queue.length) {
    const id = queue.shift();
    order.push(id);
    const blob = blobsById.get(id);
    if (!blob || blob[0] === 0x7b) continue; // JSON message blobs do not carry graph edges.
    for (const ref of _orderedBlobRefs(blob, idSet)) {
      if (ref === id || seen.has(ref)) continue;
      seen.add(ref);
      queue.push(ref);
    }
  }
  return order;
}

function _contentText(content, role) {
  if (typeof content === 'string') return content;
  if (!Array.isArray(content)) return '';
  const parts = [];
  for (const item of content) {
    if (!item || typeof item !== 'object') continue;
    if (item.type === 'text' && typeof item.text === 'string') parts.push(item.text);
    else if (role === 'tool' && typeof item.result === 'string') parts.push(item.result);
  }
  return parts.join('\n\n').trim();
}

function _normalizeCursorUserText(text) {
  const raw = String(text || '').trim();
  if (!raw) return '';
  const query = raw.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
  if (query) return query[1].trim();
  if (/^<user_info>/i.test(raw)) return '';
  if (/^\[Previous conversation summary\]/i.test(raw)) return '';
  if (/^Your conversation was summarized due to context constraints\./i.test(raw)) return '';
  return raw;
}

function _normalizeCursorAssistantText(text) {
  const raw = String(text || '').trim();
  if (!raw) return '';
  if (raw === 'redacted-reasoning' || raw === 'tool-call') return '';
  return raw;
}

function _messageFromBlobJson(value, index, meta) {
  if (!value || typeof value !== 'object') return null;
  const role = String(value.role || '').trim();
  if (role !== 'user' && role !== 'assistant') return null;
  const rawText = _contentText(value.content, role);
  const text = role === 'user'
    ? _normalizeCursorUserText(rawText)
    : _normalizeCursorAssistantText(rawText);
  if (!text) return null;
  const baseMs = Number(meta?.createdAt || 0);
  const timestamp = Number.isFinite(baseMs) && baseMs > 0
    ? new Date(baseMs + index).toISOString()
    : '';
  return {
    role,
    text,
    timestamp,
    provider: 'cursor',
    metadata: {
      provider: 'cursor',
      source: 'cursor-local-store',
      cursorAgentId: String(meta?.agentId || ''),
      cursorMessageId: String(value.id || ''),
    },
  };
}

function _workspaceUriFromBlob(buffer) {
  const text = _bufferFromBlob(buffer).toString('utf8');
  const match = text.match(/file:\/\/[^\x00-\x1f\x7f]+/);
  return match ? match[0] : '';
}

function _workspacePathFromUri(uri) {
  const raw = String(uri || '').trim();
  if (!raw.startsWith('file://')) return '';
  let decoded = '';
  try {
    decoded = decodeURIComponent(raw.replace(/^file:\/\//, ''));
  } catch {
    decoded = raw.replace(/^file:\/\//, '');
  }
  if (!decoded || fs.existsSync(decoded)) return decoded;
  // Cursor's root blob is binary/protobuf-like. The workspace URI is embedded
  // as a string, and the following protobuf bytes can occasionally be printable
  // ASCII, so a raw string scan may capture one or two bytes past the path.
  // Trim only when that recovers a real path; deleted workspaces keep the raw
  // decoded value so callers can still inspect the store metadata.
  for (let i = decoded.length - 1; i > 0; i--) {
    const candidate = decoded.slice(0, i);
    if (fs.existsSync(candidate)) return candidate;
  }
  return decoded;
}

function _statSignature(storeDbPath) {
  const parts = [];
  for (const fp of [storeDbPath, `${storeDbPath}-wal`, `${storeDbPath}-shm`]) {
    try {
      const st = fs.statSync(fp);
      parts.push(`${st.size}:${Math.floor(st.mtimeMs)}`);
    } catch {
      parts.push('0:0');
    }
  }
  return parts.join('|');
}

function loadCursorStore(storeDbPath) {
  const fp = path.resolve(String(storeDbPath || ''));
  const signature = _statSignature(fp);
  const cached = CACHE.get(fp);
  if (cached && cached.signature === signature) return cached.result;

  const db = new Database(fp, { readonly: true, fileMustExist: true });
  try {
    const meta = _readMeta(db);
    if (!meta?.latestRootBlobId) return null;
    const rows = db.prepare('SELECT id, data FROM blobs').all();
    const blobsById = new Map();
    for (const row of rows) {
      const id = String(row.id || '').trim();
      if (SHA256_HEX_RE.test(id)) blobsById.set(id, _bufferFromBlob(row.data));
    }
    const rootBlob = blobsById.get(String(meta.latestRootBlobId || '').trim()) || null;
    const workspaceUri = rootBlob ? _workspaceUriFromBlob(rootBlob) : '';
    const reachableIds = _reachableBlobIds(meta.latestRootBlobId, blobsById);
    const messages = [];
    reachableIds.forEach((id, index) => {
      const blob = blobsById.get(id);
      if (!blob || blob[0] !== 0x7b) return;
      try {
        const message = _messageFromBlobJson(JSON.parse(blob.toString('utf8')), index, meta);
        if (message) messages.push(message);
      } catch {}
    });
    const result = {
      storeDbPath: fp,
      agentId: String(meta.agentId || ''),
      name: String(meta.name || ''),
      createdAtMs: Number(meta.createdAt || 0) || 0,
      lastUsedModel: String(meta.lastUsedModel || ''),
      workspaceUri,
      workspacePath: _workspacePathFromUri(workspaceUri),
      messages,
    };
    CACHE.set(fp, { signature, result });
    return result;
  } finally {
    db.close();
  }
}

function _listCursorStoreDbPaths(cursorHome = CURSOR_HOME()) {
  const chatsDir = path.join(cursorHome, 'chats');
  let workspaceDirs = [];
  try { workspaceDirs = fs.readdirSync(chatsDir, { withFileTypes: true }); } catch { return []; }
  const paths = [];
  for (const workspaceEntry of workspaceDirs) {
    if (!workspaceEntry.isDirectory()) continue;
    const workspaceDir = path.join(chatsDir, workspaceEntry.name);
    let agentDirs = [];
    try { agentDirs = fs.readdirSync(workspaceDir, { withFileTypes: true }); } catch { continue; }
    for (const agentEntry of agentDirs) {
      if (!agentEntry.isDirectory()) continue;
      const storePath = path.join(workspaceDir, agentEntry.name, 'store.db');
      if (fs.existsSync(storePath)) paths.push(storePath);
    }
  }
  return paths;
}

function _parseTimeMs(value) {
  if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
  const text = String(value || '').trim();
  if (!text) return 0;
  const parsed = Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(text) ? text : text.replace(' ', 'T') + 'Z');
  return Number.isFinite(parsed) ? parsed : 0;
}

function _samePath(a, b) {
  if (!a || !b) return false;
  try { return path.resolve(a) === path.resolve(b); } catch { return String(a) === String(b); }
}

function findCursorStoresForSession(options = {}) {
  const cwd = String(options.cwd || '').trim();
  const createdAtMs = _parseTimeMs(options.createdAt || options.created_at);
  const updatedAtMs = _parseTimeMs(options.updatedAt || options.updated_at);
  const targetMs = updatedAtMs || createdAtMs || Date.now();
  const agentIds = new Set((options.agentSessionIds || options.agentIds || [])
    .map(id => String(id || '').trim())
    .filter(Boolean));
  const stores = [];
  for (const storeDbPath of _listCursorStoreDbPaths(options.cursorHome)) {
    let store;
    try { store = loadCursorStore(storeDbPath); } catch { continue; }
    if (!store) continue;
    const agentMatch = store.agentId && agentIds.has(store.agentId);
    const cwdMatch = cwd && _samePath(store.workspacePath, cwd);
    if (!agentMatch && !cwdMatch) continue;
    const created = store.createdAtMs || 0;
    const distanceMs = created && targetMs ? Math.abs(created - targetMs) : Number.MAX_SAFE_INTEGER;
    stores.push({
      ...store,
      _score: {
        agentMatch: agentMatch ? 1 : 0,
        cwdMatch: cwdMatch ? 1 : 0,
        distanceMs,
        createdAtMs: created,
      },
    });
  }
  stores.sort((a, b) => (
    b._score.agentMatch - a._score.agentMatch
    || b._score.cwdMatch - a._score.cwdMatch
    || a._score.distanceMs - b._score.distanceMs
    || b._score.createdAtMs - a._score.createdAtMs
    || a.storeDbPath.localeCompare(b.storeDbPath)
  ));
  return stores;
}

function loadCursorConversationForSession(options = {}) {
  const stores = findCursorStoresForSession(options);
  for (const store of stores) {
    if (store.messages.length > 0) return store;
  }
  return stores[0] || null;
}

// Map a reconstructed cursor store to the fields db.importSessionConversation expects, so a
// cursor conversation lands in session_conversations exactly like a Claude/Codex import and
// the normal cache read path serves it. Pure (no I/O) → unit-testable. messages already carry
// the {role,text,timestamp,provider,metadata} shape the renderer expects.
function cursorStoreToConversationFields(store) {
  const messages = Array.isArray(store?.messages) ? store.messages : [];
  let user_msg_count = 0;
  let assistant_msg_count = 0;
  let first_message = '';
  let first_assistant_text = '';
  let last_user_content = '';
  for (const m of messages) {
    if (!m || typeof m !== 'object') continue;
    if (m.role === 'user') {
      user_msg_count += 1;
      if (!first_message) first_message = String(m.text || '');
      if (m.text) last_user_content = String(m.text);
    } else if (m.role === 'assistant') {
      assistant_msg_count += 1;
      if (!first_assistant_text) first_assistant_text = String(m.text || '');
    }
  }
  return {
    messages,
    user_msg_count,
    assistant_msg_count,
    first_message,
    last_user_content,
    first_assistant_text,
    project_path: String(store?.workspacePath || ''),
    model_provider: 'cursor',
    model_id: String(store?.lastUsedModel || ''),
    session_created_at: Number(store?.createdAtMs) > 0 ? new Date(Number(store.createdAtMs)).toISOString() : '',
  };
}

module.exports = {
  loadCursorStore,
  findCursorStoresForSession,
  loadCursorConversationForSession,
  cursorStoreToConversationFields,
  statSignature: _statSignature, // cheap size:mtime gate for incremental import
  _test: {
    _decodeMaybeHexJson,
    _orderedBlobRefs,
    _reachableBlobIds,
    _normalizeCursorUserText,
    _parseTimeMs,
  },
};
