import * as plugins from './plugins.js';
import { SmartdataDb } from './classes.db.js';
import { SmartdataDbCursor } from './classes.cursor.js';
import { SmartDataDbDoc, type IIndexOptions, getSearchableFields } from './classes.doc.js';
import { SmartdataDbWatcher } from './classes.watcher.js';
import { CollectionFactory } from './classes.collectionfactory.js';

export interface IFindOptions {
  limit?: number;
}

/**
 *
 */
export interface IDocValidationFunc<T> {
  (doc: T): boolean;
}

export type TDelayed<TDelayedArg> = () => TDelayedArg;

const collectionFactory = new CollectionFactory();

/**
 * This is a decorator that will tell the decorated class what dbTable to use
 * @param dbArg
 */
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
  return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
    const decoratedClass = class extends constructor {
      public static className = constructor.name;
      public static get collection() {
        if (!(dbArg instanceof SmartdataDb)) {
          dbArg = dbArg();
        }
        return collectionFactory.getCollection(constructor.name, dbArg);
      }
      public get collection() {
        if (!(dbArg instanceof SmartdataDb)) {
          dbArg = dbArg();
        }
        return collectionFactory.getCollection(constructor.name, dbArg);
      }
    };
    return decoratedClass;
  };
}

export interface IManager {
  db: SmartdataDb;
}

export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T): T => {
  (dbDocArg as any).prototype.defaultManager = managerArg;
  return dbDocArg;
};

/**
 * This is a decorator that will tell the decorated class what dbTable to use
 * @param dbArg
 */
export function managed<TManager extends IManager>(managerArg?: TManager | TDelayed<TManager>) {
  return function classDecorator<T extends { new (...args: any[]): any }>(constructor: T) {
    const decoratedClass = class extends constructor {
      public static className = constructor.name;
      public static get collection() {
        let dbArg: SmartdataDb;
        if (!managerArg) {
          dbArg = this.prototype.defaultManager.db;
        } else if (managerArg['db']) {
          dbArg = (managerArg as TManager).db;
        } else {
          dbArg = (managerArg as TDelayed<TManager>)().db;
        }
        return collectionFactory.getCollection(constructor.name, dbArg);
      }
      public get collection() {
        let dbArg: SmartdataDb;
        if (!managerArg) {
          //console.log(this.defaultManager.db);
          //process.exit(0)
          dbArg = this.defaultManager.db;
        } else if (managerArg['db']) {
          dbArg = (managerArg as TManager).db;
        } else {
          dbArg = (managerArg as TDelayed<TManager>)().db;
        }
        return collectionFactory.getCollection(constructor.name, dbArg);
      }
      public static get manager() {
        let manager: TManager;
        if (!managerArg) {
          manager = this.prototype.defaultManager;
        } else if (managerArg['db']) {
          manager = managerArg as TManager;
        } else {
          manager = (managerArg as TDelayed<TManager>)();
        }
        return manager;
      }
      public get manager() {
        let manager: TManager;
        if (!managerArg) {
          manager = this.defaultManager;
        } else if (managerArg['db']) {
          manager = managerArg as TManager;
        } else {
          manager = (managerArg as TDelayed<TManager>)();
        }
        return manager;
      }
    };
    return decoratedClass;
  };
}

/**
 * @dpecrecated use @managed instead
 */
export const Manager = managed;

export class SmartdataCollection<T> {
  /**
   * the collection that is used
   */
  public mongoDbCollection: plugins.mongodb.Collection;
  public objectValidation: IDocValidationFunc<T> = null;
  public collectionName: string;
  public smartdataDb: SmartdataDb;
  public uniqueIndexes: string[] = [];
  public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];
  // flag to ensure text index is created only once
  private textIndexCreated: boolean = false;

  constructor(classNameArg: string, smartDataDbArg: SmartdataDb) {
    // tell the collection where it belongs
    this.collectionName = classNameArg;
    this.smartdataDb = smartDataDbArg;

    // tell the db class about it (important since Db uses different systems under the hood)
    this.smartdataDb.addCollection(this);
  }

  /**
   * makes sure a collection exists within MongoDb that maps to the SmartdataCollection
   */
  public async init() {
    if (!this.mongoDbCollection) {
      // connect this instance to a MongoDB collection
      const availableMongoDbCollections = await this.smartdataDb.mongoDb.collections();
      const wantedCollection = availableMongoDbCollections.find((collection) => {
        return collection.collectionName === this.collectionName;
      });
      if (!wantedCollection) {
        await this.smartdataDb.mongoDb.createCollection(this.collectionName);
        console.log(`Successfully initiated Collection ${this.collectionName}`);
      }
      this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName);
      // Auto-create a compound text index on all searchable fields
      const searchableFields = getSearchableFields(this.collectionName);
      if (searchableFields.length > 0 && !this.textIndexCreated) {
        // Build a compound text index spec
        const indexSpec: Record<string, 'text'> = {};
        searchableFields.forEach(f => { indexSpec[f] = 'text'; });
        // Cast to any to satisfy TypeScript IndexSpecification typing
        await this.mongoDbCollection.createIndex(indexSpec as any, { name: 'smartdata_text_index' });
        this.textIndexCreated = true;
      }
    }
  }

  /**
   * mark unique index
   */
  public markUniqueIndexes(keyArrayArg: string[] = []) {
    for (const key of keyArrayArg) {
      if (!this.uniqueIndexes.includes(key)) {
        this.mongoDbCollection.createIndex(key, {
          unique: true,
        });
        // make sure we only call this once and not for every doc we create
        this.uniqueIndexes.push(key);
      }
    }
  }

  /**
   * creates regular indexes for the collection
   */
  public createRegularIndexes(indexesArg: Array<{field: string, options: IIndexOptions}> = []) {
    for (const indexDef of indexesArg) {
      // Check if we've already created this index
      const indexKey = indexDef.field;
      if (!this.regularIndexes.some(i => i.field === indexKey)) {
        this.mongoDbCollection.createIndex(
          { [indexDef.field]: 1 }, // Simple single-field index
          indexDef.options
        );
        // Track that we've created this index
        this.regularIndexes.push(indexDef);
      }
    }
  }

  /**
   * adds a validation function that all newly inserted and updated objects have to pass
   */
  public addDocValidation(funcArg: IDocValidationFunc<T>) {
    this.objectValidation = funcArg;
  }

  /**
   * finds an object in the DbCollection
   */
  public async findOne(filterObject: any): Promise<any> {
    await this.init();
    const cursor = this.mongoDbCollection.find(filterObject);
    const result = await cursor.next();
    cursor.close();
    return result;
  }

  public async getCursor(
    filterObjectArg: any,
    dbDocArg: typeof SmartDataDbDoc,
  ): Promise<SmartdataDbCursor<any>> {
    await this.init();
    const cursor = this.mongoDbCollection.find(filterObjectArg);
    return new SmartdataDbCursor(cursor, dbDocArg);
  }

  /**
   * finds an object in the DbCollection
   */
  public async findAll(filterObject: any): Promise<any[]> {
    await this.init();
    const cursor = this.mongoDbCollection.find(filterObject);
    const result = await cursor.toArray();
    cursor.close();
    return result;
  }

  /**
   * watches the collection while applying a filter
   */
  public async watch(
    filterObject: any,
    smartdataDbDocArg: typeof SmartDataDbDoc,
  ): Promise<SmartdataDbWatcher> {
    await this.init();
    const changeStream = this.mongoDbCollection.watch(
      [
        {
          $match: filterObject,
        },
      ],
      {
        fullDocument: 'updateLookup',
      },
    );
    const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg);
    await smartdataWatcher.readyDeferred.promise;
    return smartdataWatcher;
  }

  /**
   * create an object in the database
   */
  public async insert(dbDocArg: T & SmartDataDbDoc<T, unknown>): Promise<any> {
    await this.init();
    await this.checkDoc(dbDocArg);
    this.markUniqueIndexes(dbDocArg.uniqueIndexes);
    
    // Create regular indexes if available
    if (dbDocArg.regularIndexes && dbDocArg.regularIndexes.length > 0) {
      this.createRegularIndexes(dbDocArg.regularIndexes);
    }
    
    const saveableObject = await dbDocArg.createSavableObject();
    const result = await this.mongoDbCollection.insertOne(saveableObject);
    return result;
  }

  /**
   * inserts object into the DbCollection
   */
  public async update(dbDocArg: T & SmartDataDbDoc<T, unknown>): Promise<any> {
    await this.init();
    await this.checkDoc(dbDocArg);
    const identifiableObject = await dbDocArg.createIdentifiableObject();
    const saveableObject = await dbDocArg.createSavableObject();
    const updateableObject: any = {};
    for (const key of Object.keys(saveableObject)) {
      if (identifiableObject[key]) {
        continue;
      }
      updateableObject[key] = saveableObject[key];
    }
    const result = await this.mongoDbCollection.updateOne(
      identifiableObject,
      { $set: updateableObject },
      { upsert: true },
    );
    return result;
  }

  public async delete(dbDocArg: T & SmartDataDbDoc<T, unknown>): Promise<any> {
    await this.init();
    await this.checkDoc(dbDocArg);
    const identifiableObject = await dbDocArg.createIdentifiableObject();
    await this.mongoDbCollection.deleteOne(identifiableObject);
  }

  public async getCount(filterObject: any) {
    await this.init();
    return this.mongoDbCollection.countDocuments(filterObject);
  }

  /**
   * checks a Doc for constraints
   * if this.objectValidation is not set it passes.
   */
  private checkDoc(docArg: T): Promise<void> {
    const done = plugins.smartpromise.defer<void>();
    let validationResult = true;
    if (this.objectValidation) {
      validationResult = this.objectValidation(docArg);
    }
    if (validationResult) {
      done.resolve();
    } else {
      done.reject('validation of object did not pass');
    }
    return done.promise;
  }
}