// firebase.js — storage adapter wrapping Firebase SDK. Default impl (upstream-unchanged).
// Matches interface of memory.js / ws.js so App.js calls stay identical.
//
// App.js imports SDK via this module (doc, setDoc, etc re-exported) for both
// the adapter (createFirebaseStorage) and direct SDK calls. ONE import path.

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import {
  getFirestore, doc, setDoc, getDoc as getDocReal, getDocs as getDocsReal, addDoc, collection,
  onSnapshot, updateDoc, deleteDoc, query, where, orderBy, limit, writeBatch, getCountFromServer,
} from 'firebase/firestore';

// Adapter call recorder (instrumentation, no behavior change).
// Tests assert adapter.subscribeDoc called (catches raw-SDK bypass like DisplayView).
const ADAPTER_CALLS = [];
function recordAdapterCall(entry) { ADAPTER_CALLS.push({ ...entry, ts: Date.now() }); }
export function getAdapterCalls() { return [...ADAPTER_CALLS]; }
export function resetAdapterCalls() { ADAPTER_CALLS.length = 0; }

// Path helpers mirror App.js getPath object.
const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default';
const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`;

export const getPath = {
  campaigns: () => `${PUBLIC_DATA_PATH}/campaigns`,
  campaign: (id) => `${PUBLIC_DATA_PATH}/campaigns/${id}`,
  encounters: (campaignId) => `${PUBLIC_DATA_PATH}/campaigns/${campaignId}/encounters`,
  encounter: (campaignId, encounterId) => `${PUBLIC_DATA_PATH}/campaigns/${campaignId}/encounters/${encounterId}`,
  activeDisplay: () => `${PUBLIC_DATA_PATH}/activeDisplay/status`,
  logs: () => `${PUBLIC_DATA_PATH}/logs`
};

let firebaseApp = null;
let dbInstance = null;
let authInstance = null;

export function initFirebase() {
  const config = {
    apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
    authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
    storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.REACT_APP_FIREBASE_APP_ID
  };
  const requiredKeys = ['apiKey', 'authDomain', 'projectId', 'appId'];
  const missing = requiredKeys.filter(k => !config[k]);
  if (missing.length > 0) {
    console.error(`CRITICAL: Missing Firebase config: ${missing.join(', ')}`);
    return false;
  }
  try {
    firebaseApp = initializeApp(config);
    dbInstance = getFirestore(firebaseApp);
    authInstance = getAuth(firebaseApp);
    return true;
  } catch (err) {
    console.error('Firebase init failed:', err);
    return false;
  }
}

export function getDb() { return dbInstance; }
export function getAuthInstance() { return authInstance; }

// ============================================================================
// STORAGE ADAPTER
// ============================================================================
// Wraps SDK in the storage interface (getDoc/setDoc/etc).
// App.js can now import { storage } and call storage.setDoc(path, data).
// Hooks (useFirestoreDocument etc) still use SDK directly for now.

// Translate neutral {__type} constraints (src/storage/index.js builders) to
// SDK builders. The client SDK has no offset, so emulate it: widen limit to
// offset+limit and report offsetN for the caller to slice off the leading
// docs. Read cost matches native offset (admin SDK also bills skipped docs);
// the server adapter pushes offset into SQL instead. Unknown constraint types
// are dropped — a raw object handed to query() throws and white-screens.
function toSdkConstraints(queryConstraints) {
  let offsetN = 0;
  let limitN = null;
  const fbConstraints = [];
  for (const c of queryConstraints || []) {
    if (!c || !c.__type) continue;
    if (c.__type === 'where') fbConstraints.push(where(c.field, c.op, c.value));
    else if (c.__type === 'orderBy') fbConstraints.push(orderBy(c.field, c.dir || 'asc'));
    else if (c.__type === 'limit') limitN = c.n;
    else if (c.__type === 'offset') offsetN = c.offset || 0;
  }
  if (limitN !== null) fbConstraints.push(limit(limitN + offsetN));
  return { fbConstraints, offsetN };
}

export function createFirebaseStorage() {
  const db = dbInstance;
  if (!db) throw new Error('Firestore not initialized. Call initFirebase() first.');

  return {
    async getDoc(path) {
      const snap = await getDocReal(doc(db, path));
      return snap.exists() ? { id: snap.id, ...snap.data() } : null;
    },

    async setDoc(path, data, opts = {}) {
      recordAdapterCall({ fn: 'setDoc', path, data, opts });
      await setDoc(doc(db, path), data, opts.merge ? { merge: true } : undefined);
    },

    async updateDoc(path, patch) {
      recordAdapterCall({ fn: 'updateDoc', path, patch });
      await updateDoc(doc(db, path), patch);
    },

    async deleteDoc(path) {
      await deleteDoc(doc(db, path));
    },

    async addDoc(collectionPath, data) {
      const ref = await addDoc(collection(db, collectionPath), data);
      return { id: ref.id, path: `${collectionPath}/${ref.id}` };
    },

    async getCollection(collectionPath, queryConstraints = []) {
      const { fbConstraints, offsetN } = toSdkConstraints(queryConstraints);
      const q = fbConstraints.length ? query(collection(db, collectionPath), ...fbConstraints) : collection(db, collectionPath);
      const snapshot = await getDocsReal(q);
      return snapshot.docs.slice(offsetN).map(d => ({ id: d.id, ...d.data() }));
    },

    async countCollection(collectionPath) {
      const snapshot = await getCountFromServer(collection(db, collectionPath));
      return snapshot.data().count;
    },

    async batchWrite(ops) {
      const batch = writeBatch(db);
      for (const op of ops) {
        if (op.type === 'set') batch.set(doc(db, op.path), op.data);
        else if (op.type === 'delete') batch.delete(doc(db, op.path));
        else if (op.type === 'update') batch.update(doc(db, op.path), op.data);
      }
      await batch.commit();
    },

    // Bulk delete collection (optionally where-filtered). Firestore has no
    // single bulk-by-path op: fetch matching docs, batch-delete in chunks
    // of 500 (firestore batch limit). DEV ONLY.
    async deleteCollection(path, whereField, whereValue) {
      if (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test') {
        throw new Error('deleteCollection: dev only');
      }
      let q = collection(db, path);
      if (whereField) q = query(q, where(whereField, '==', whereValue));
      const snap = await getDocsReal(q);
      const docs = snap.docs;
      let count = 0;
      for (let i = 0; i < docs.length; i += 500) {
        const batch = writeBatch(db);
        for (const d of docs.slice(i, i + 500)) batch.delete(d.ref);
        await batch.commit();
        count += Math.min(500, docs.length - i);
      }
      return count;
    },

    // Transactional undo via batch (atomic in firestore). Apply updates +
    // flip undone flag in single commit.
    async undo({ logPath, undo, redo = false }) {
      const batch = writeBatch(db);
      const updates = redo ? (undo.redo || undo.updates) : undo.updates;
      batch.update(doc(db, undo.encounterPath), updates);
      batch.update(doc(db, logPath), { undone: !redo });
      await batch.commit();
    },

    // Subscribe = onSnapshot. cb fires immediately + on change. Returns unsubscribe.
    subscribeDoc(path, cb, errCb) {
      recordAdapterCall({ fn: 'subscribeDoc', path });
      return onSnapshot(doc(db, path), (snap) => {
        cb(snap.exists() ? { id: snap.id, ...snap.data() } : null);
      }, (err) => {
        console.error(`subscribeDoc ${path}:`, err);
        if (typeof errCb === 'function') errCb(err);
      });
    },

    subscribeCollection(collectionPath, cb, queryConstraints = [], errCb) {
      recordAdapterCall({ fn: 'subscribeCollection', path: collectionPath });
      const { fbConstraints, offsetN } = toSdkConstraints(queryConstraints);
      const q = fbConstraints.length > 0
        ? query(collection(db, collectionPath), ...fbConstraints)
        : collection(db, collectionPath);
      return onSnapshot(q, (snap) => {
        cb(snap.docs.slice(offsetN).map(d => ({ id: d.id, ...d.data() })));
      }, (err) => {
        console.error(`subscribeCollection ${collectionPath}:`, err);
        if (typeof errCb === 'function') errCb(err);
      });
    },

    dispose() { /* SDK managed; no-op */ },
  };
}

// Re-export SDK pieces App.js uses directly (until full refactor).
// orderBy/limit NOT re-exported: App uses neutral builders from index.js.
export {
  doc, setDoc, updateDoc, deleteDoc, addDoc, collection, onSnapshot,
  query, writeBatch,
};
