/**
 * FirestoreService - A wrapper around Firebase Firestore providing type-safe operations.
 * This service needs to be instantiated with a Firestore database instance.
 *
 * @example
 * // 1️⃣ Basic Setup
 * import { initializeApp } from 'firebase/app';
 * import { getFirestore } from 'firebase/firestore';
 * import { FirestoreService } from '@serge-ivo/firestore-client';
 *
 * // Initialize Firebase app first
 * const app = initializeApp(firebaseConfig);
 * const db = getFirestore(app);
 *
 * // Create an instance of FirestoreService
 * const firestoreService = new FirestoreService(db);
 *
 * @example
 * // 2️⃣ Using the Service Instance
 * // Use the created instance to call methods
 * const user = await firestoreService.getDocument<User>('users/user123');
 *
 * @example
 * // 3️⃣ Common Error Cases
 * // ❌ Don't instantiate without a valid Firestore instance
 * // const invalidService = new FirestoreService(null); // Throws error
 *
 * // ✅ Correct usage:
 * const firestoreService = new FirestoreService(db);
 * const result = await firestoreService.getDocument('users/user123');
 */

// src/services/FirestoreService.ts
import { initializeApp, FirebaseOptions } from "firebase/app";
import {
  addDoc,
  arrayRemove,
  arrayUnion,
  collection,
  CollectionReference,
  connectFirestoreEmulator,
  deleteDoc,
  deleteField,
  doc,
  DocumentReference,
  endBefore,
  FieldValue,
  Firestore,
  getDoc,
  getDocs,
  limit,
  onSnapshot,
  orderBy,
  query,
  QueryConstraint,
  setDoc,
  SetOptions,
  startAfter,
  Timestamp,
  updateDoc,
  where,
  WriteBatch,
  writeBatch,
  getFirestore,
} from "firebase/firestore";

// Correctly import the local factory function
import createFirestoreDataConverter from "./FirestoreDataConverter";
import { FirestoreModel } from "./firestoreModel";
import RequestLimiter from "./RequestLimiter";
import { AuthService } from "./AuthService"; // Import AuthService

export type FilterOperator =
  | "=="
  | "!="
  | "<"
  | "<="
  | ">"
  | ">="
  | "array-contains"
  | "in"
  | "array-contains-any"
  | "not-in";

interface QueryOptions {
  where?: Array<{ field: string; op: FilterOperator; value: any }>;
  orderBy?: Array<{ field: string; direction?: "asc" | "desc" }>;
  limit?: number;
  startAfter?: any;
  endBefore?: any;
}

export class FirestoreService {
  // Store db as a private readonly instance variable
  private readonly db: Firestore;
  // private readonly app: FirebaseApp; // Store the FirebaseApp instance - Removed as it's unused within the class
  public readonly auth: AuthService; // Expose AuthService instance

  /**
   * Creates an instance of FirestoreService.
   * @param {FirebaseOptions} firebaseConfig - The Firebase configuration object.
   * @throws Error if firebaseConfig is not provided or invalid.
   */
  constructor(firebaseConfig: FirebaseOptions) {
    // Basic check for firebaseConfig object
    if (
      !firebaseConfig ||
      typeof firebaseConfig !== "object" ||
      !firebaseConfig.apiKey // Check for a key property like apiKey as a basic validation
    ) {
      throw new Error(
        "Valid Firebase configuration object is required for FirestoreService constructor"
      );
    }

    // Initialize Firebase app and Firestore instance
    try {
      const app = initializeApp(firebaseConfig);
      // this.app = app; // Store the app instance - Removed as it's unused within the class
      this.db = getFirestore(app);
      this.auth = new AuthService(app); // Initialize AuthService
      console.log(
        "FirestoreService and AuthService instances created and Firebase initialized successfully."
      );
    } catch (error) {
      console.error("Error initializing Firebase:", error);
      throw new Error("Failed to initialize Firebase within FirestoreService");
    }
  }

  // --- Path Validation Methods (can remain private static or become private instance methods) ---
  // Let's make them instance methods for consistency, though static is also fine.
  private validatePathBasic(path: string): void {
    if (!path) {
      throw new Error("Path cannot be empty");
    }
    if (path.startsWith("/") || path.endsWith("/")) {
      throw new Error("Path cannot start or end with '/'");
    }
  }

  private validateCollectionPathSegments(path: string): void {
    this.validatePathBasic(path);
    const segments = path.split("/");
    if (segments.length % 2 !== 1) {
      throw new Error(
        "Collection path must have an odd number of segments (e.g., 'users' or 'users/123/posts')"
      );
    }
  }

  private validateDocumentPathSegments(path: string): void {
    this.validatePathBasic(path);
    const segments = path.split("/");
    if (segments.length % 2 !== 0) {
      throw new Error(
        "Document path must have an even number of segments (e.g., 'users/123' or 'users/123/posts/456')"
      );
    }
    if (segments.length < 2) {
      throw new Error("Document path must have at least two segments.");
    }
  }

  private validateDocumentPath(path: string): void {
    this.validateDocumentPathSegments(path);
  }
  // --- End Path Validation ---

  // --- Instance Methods (previously static, now use this.db) ---

  /**
   * Connects this service instance to the Firestore emulator.
   * Note: This should ideally be done once globally if possible.
   * @param {number} firestoreEmulatorPort - The port the emulator is running on.
   */
  connectEmulator(firestoreEmulatorPort: number): void {
    // Note: Emulator connection is typically done once globally,
    // but providing it as an instance method allows flexibility if needed.
    connectFirestoreEmulator(this.db, "localhost", firestoreEmulatorPort);
    console.log("🔥 Connected instance to Firestore Emulator");
  }

  // Private helpers now use this.db and are instance methods
  private docRef<T>(path: string): DocumentReference<T> {
    this.validateDocumentPath(path);
    return doc(this.db, path).withConverter(createFirestoreDataConverter<T>());
  }

  private colRef<T>(path: string): CollectionReference<T> {
    // Use the imported factory function
    return collection(this.db, path).withConverter(
      createFirestoreDataConverter<T>()
    );
  }

  // Public API methods are now instance methods
  /**
   * Retrieves a single document from Firestore by its full path.
   * @template T The expected type of the document data.
   * @param {string} docPath The full path to the document (e.g., 'users/userId').
   * @returns {Promise<T | null>} A promise resolving to the document data or null if not found.
   */
  async getDocument<T>(docPath: string): Promise<T | null> {
    RequestLimiter.logDocumentRequest(docPath);
    const docSnap = await getDoc(this.docRef<T>(docPath));
    return docSnap.exists() ? docSnap.data() : null;
  }

  /**
   * Adds a new document to a specified collection.
   * @template T The type of the data being added.
   * @param {string} collectionPath The path to the collection (e.g., 'posts', 'users/userId/tasks').
   * @param {T} data The data for the new document.
   * @returns {Promise<string | undefined>} A promise resolving to the new document's ID, or undefined on failure.
   */
  async addDocument<T>(
    collectionPath: string,
    data: T
  ): Promise<string | undefined> {
    this.validateCollectionPathSegments(collectionPath); // Validate path first
    RequestLimiter.logGeneralRequest();
    const docRef = await addDoc(this.colRef<T>(collectionPath), data);
    return docRef.id;
  }

  /**
   * Updates specific fields of an existing document.
   * @param {string} docPath The full path to the document.
   * @param {Record<string, any>} data An object containing the fields to update.
   * @returns {Promise<void>}
   */
  async updateDocument(
    docPath: string,
    data: Record<string, any>
  ): Promise<void> {
    this.validateDocumentPath(docPath); // Ensure doc path is valid before update
    RequestLimiter.logGeneralRequest();
    // Use the raw doc ref without converter for partial updates
    await updateDoc(doc(this.db, docPath), data);
  }

  /**
   * Creates or overwrites a document completely.
   * @template T The type of the data being set.
   * @param {string} docPath The full path to the document.
   * @param {T} data The data for the document.
   * @param {object} [options] Optional settings. `merge: true` merges data instead of overwriting.
   * @returns {Promise<void>}
   */
  async setDocument<T>(
    docPath: string,
    data: T,
    options: { merge?: boolean } = { merge: true }
  ): Promise<void> {
    this.validateDocumentPath(docPath);
    RequestLimiter.logGeneralRequest();
    await setDoc(this.docRef<T>(docPath), data, options);
  }

  /**
   * Deletes a document from Firestore.
   * @param {string} docPath The full path to the document.
   * @returns {Promise<void>}
   */
  async deleteDocument(docPath: string): Promise<void> {
    this.validateDocumentPath(docPath);
    RequestLimiter.logGeneralRequest();
    // Get the raw doc ref for deletion
    await deleteDoc(doc(this.db, docPath));
  }

  /**
   * Deletes all documents within a specified collection or subcollection.
   * Use with caution, especially on large collections.
   * @param {string} collectionPath The path to the collection (e.g., 'users', 'users/userId/posts').
   * @returns {Promise<void>}
   */
  async deleteCollection(collectionPath: string): Promise<void> {
    this.validateCollectionPathSegments(collectionPath);
    RequestLimiter.logGeneralRequest();
    const batch = writeBatch(this.db);
    // Use colRef without converter for deletion query
    const snapshot = await getDocs(collection(this.db, collectionPath));
    snapshot.docs.forEach((d) => batch.delete(d.ref));
    await batch.commit();
  }

  /**
   * Subscribes to real-time updates for a single document.
   * @template T The expected type of the document data.
   * @param {string} docPath The full path to the document.
   * @param {(data: T | null) => void} callback The function to call with document data (or null) on updates.
   * @returns {() => void} A function to unsubscribe from updates.
   */
  subscribeToDocument<T>(
    docPath: string,
    callback: (data: T | null) => void
  ): () => void {
    this.validateDocumentPath(docPath);
    RequestLimiter.logSubscriptionRequest(docPath);
    const unsubscribe = onSnapshot(this.docRef<T>(docPath), (docSnap) => {
      callback(docSnap.exists() ? docSnap.data() : null);
    });
    return unsubscribe;
  }

  /**
   * Subscribes to real-time updates for a collection.
   * @template T The expected type of the documents in the collection.
   * @param {string} collectionPath The path to the collection.
   * @param {(data: T[]) => void} callback The function to call with an array of document data on updates.
   * @returns {() => void} A function to unsubscribe from updates.
   */
  subscribeToCollection<T>(
    collectionPath: string,
    callback: (data: T[]) => void
  ): () => void {
    this.validateCollectionPathSegments(collectionPath);
    RequestLimiter.logSubscriptionRequest(collectionPath);
    const unsubscribe = onSnapshot(
      query(this.colRef<T>(collectionPath)),
      (snapshot) => {
        const data = snapshot.docs.map((d) => d.data());
        callback(data);
      }
    );
    return unsubscribe;
  }

  /**
   * Subscribes to real-time updates for a collection, automatically instantiating FirestoreModel subclasses.
   * @template T A type extending FirestoreModel.
   * @param {new (...args: any[]) => T} model The constructor of the FirestoreModel subclass.
   * @param {string} collectionPath The path to the collection.
   * @param {(data: T[]) => void} callback The function to call with an array of instantiated models on updates.
   * @returns {() => void} A function to unsubscribe from updates.
   */
  subscribeToCollection2<T extends FirestoreModel>(
    model: new (...args: any[]) => T,
    collectionPath: string,
    callback: (data: T[]) => void
  ): () => void {
    this.validateCollectionPathSegments(collectionPath);
    RequestLimiter.logSubscriptionRequest(collectionPath);

    // Use the factory function to get the converter
    const converter = createFirestoreDataConverter<any>(); // Use <any> or a base type for raw data

    const unsubscribe = onSnapshot(
      query(this.colRef<any>(collectionPath)).withConverter(converter),
      (snapshot) => {
        // Map the raw data, then instantiate the specific model class
        const data = snapshot.docs.map((doc) => {
          const rawData = doc.data(); // Get data converted by the converter
          // Manually instantiate the correct model subclass
          return new model(rawData, doc.id);
        });
        callback(data);
      }
    );
    return unsubscribe;
  }

  /**
   * Fetches documents from a collection, optionally applying query constraints.
   * @template T The expected type of the documents.
   * @param {string} path The path to the collection.
   * @param {...QueryConstraint} queryConstraints Optional Firestore query constraints (where, orderBy, limit, etc.).
   * @returns {Promise<T[]>} A promise resolving to an array of document data.
   */
  async fetchCollection<T>(
    path: string,
    ...queryConstraints: QueryConstraint[]
  ): Promise<T[]> {
    this.validateCollectionPathSegments(path);
    RequestLimiter.logCollectionFetchRequest(path);
    const snapshot = await getDocs(
      queryConstraints.length > 0
        ? query(this.colRef<T>(path), ...queryConstraints)
        : this.colRef<T>(path)
    );
    return snapshot.docs.map((d) => d.data());
  }

  /**
   * Queries a Firestore collection using a structured options object.
   * @template T The expected type of the document data.
   * @param {string} collectionPath The path to the collection.
   * @param {QueryOptions} [options={}] Optional query constraints (where, orderBy, limit, startAfter, endBefore).
   * @returns {Promise<T[]>} A promise resolving to an array of document data.
   */
  async queryCollection<T>(
    collectionPath: string,
    options: QueryOptions = {}
  ): Promise<T[]> {
    this.validateCollectionPathSegments(collectionPath);
    RequestLimiter.logGeneralRequest();

    const colReference = this.colRef<T>(collectionPath);
    const constraints: QueryConstraint[] = [];

    if (options.where) {
      options.where.forEach((w) => {
        constraints.push(where(w.field, w.op, w.value));
      });
    }
    if (options.orderBy) {
      options.orderBy.forEach((o) => {
        constraints.push(orderBy(o.field, o.direction));
      });
    }
    if (options.startAfter) {
      constraints.push(startAfter(options.startAfter));
    }
    if (options.endBefore) {
      constraints.push(endBefore(options.endBefore));
    }
    // Apply limit LAST as recommended by Firestore docs
    if (options.limit) {
      constraints.push(limit(options.limit));
    }

    const q = query(colReference, ...constraints);
    const snapshot = await getDocs(q);
    return snapshot.docs.map((d) => d.data());
  }

  // --- Batch Operations ---
  /**
   * Returns a new Firestore WriteBatch associated with this service instance's database.
   * @returns {WriteBatch}
   */
  getBatch(): WriteBatch {
    RequestLimiter.logGeneralRequest();
    return writeBatch(this.db);
  }

  /**
   * Updates specific fields of multiple documents in a batch.
   * @param {WriteBatch} batch The Firestore WriteBatch to update.
   * @param {string} docPath The full path to the document.
   * @param {object} data An object containing the fields to update.
   */
  updateInBatch(
    batch: WriteBatch,
    docPath: string,
    data: { [key: string]: FieldValue | Partial<unknown> | undefined }
  ): void {
    this.validateDocumentPath(docPath);
    const docRef = doc(this.db, docPath); // Use raw doc ref
    batch.update(docRef, data);
  }

  /**
   * Sets a document in a batch.
   * @template T The type of the data being set.
   * @param {WriteBatch} batch The Firestore WriteBatch to set.
   * @param {string} docPath The full path to the document.
   * @param {T} data The data for the document.
   * @param {SetOptions} [options] Optional settings. `merge: true` merges data instead of overwriting.
   */
  setInBatch<T>(
    batch: WriteBatch,
    docPath: string,
    data: T,
    options: SetOptions = {}
  ): void {
    this.validateDocumentPath(docPath);
    // Use docRef with converter for type safety during set
    const docRef = this.docRef<T>(docPath);
    batch.set(docRef, data, options);
  }

  /**
   * Deletes a document in a batch.
   * @param {WriteBatch} batch The Firestore WriteBatch to delete.
   * @param {string} docPath The full path to the document.
   */
  deleteInBatch(batch: WriteBatch, docPath: string): void {
    this.validateDocumentPath(docPath);
    const docRef = doc(this.db, docPath); // Use raw doc ref
    batch.delete(docRef);
  }

  // --- Static Utility Methods (Do not depend on instance state) ---
  /**
   * Provides access to Firestore FieldValue constants (e.g., arrayUnion, arrayRemove).
   */
  static getFieldValue() {
    return { arrayUnion, arrayRemove };
  }

  /**
   * Returns a Firestore Timestamp for the current time.
   */
  static getTimestamp() {
    return Timestamp.now();
  }

  /**
   * Returns a special value used to delete a field during an update.
   */
  static deleteField() {
    return deleteField();
  }
}
