import * as plugins from './plugins.js';

import { SmartdataDb } from './classes.db.js';
import { SmartdataDbCursor } from './classes.cursor.js';
import { type IManager, SmartdataCollection } from './classes.collection.js';
import { SmartdataDbWatcher } from './classes.watcher.js';
import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';

export type TDocCreation = 'db' | 'new' | 'mixed';



export function globalSvDb() {
  return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
    console.log(`called svDb() on >${target.constructor.name}.${key}<`);
    if (!target.globalSaveableProperties) {
      target.globalSaveableProperties = [];
    }
    target.globalSaveableProperties.push(key);
  };
}

/**
 * saveable - saveable decorator to be used on class properties
 */
export function svDb() {
  return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
    console.log(`called svDb() on >${target.constructor.name}.${key}<`);
    if (!target.saveableProperties) {
      target.saveableProperties = [];
    }
    target.saveableProperties.push(key);
  };
}

/**
 * searchable - marks a property as searchable with Lucene query syntax
 */
export function searchable() {
  return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
    // Attach to class constructor for direct access
    const ctor = target.constructor as any;
    if (!Array.isArray(ctor.searchableFields)) {
      ctor.searchableFields = [];
    }
    ctor.searchableFields.push(key);
  };
}

// Escape user input for safe use in MongoDB regular expressions
function escapeForRegex(input: string): string {
  return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * unique index - decorator to mark a unique index
 */
export function unI() {
  return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
    console.log(`called unI on >>${target.constructor.name}.${key}<<`);

    // mark the index as unique
    if (!target.uniqueIndexes) {
      target.uniqueIndexes = [];
    }
    target.uniqueIndexes.push(key);

    // and also save it
    if (!target.saveableProperties) {
      target.saveableProperties = [];
    }
    target.saveableProperties.push(key);
  };
}

/**
 * Options for MongoDB indexes
 */
export interface IIndexOptions {
  background?: boolean;
  unique?: boolean;
  sparse?: boolean;
  expireAfterSeconds?: number;
  [key: string]: any;
}

/**
 * index - decorator to mark a field for regular indexing
 */
export function index(options?: IIndexOptions) {
  return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
    console.log(`called index() on >${target.constructor.name}.${key}<`);
    
    // Initialize regular indexes array if it doesn't exist
    if (!target.regularIndexes) {
      target.regularIndexes = [];
    }
    
    // Add this field to regularIndexes with its options
    target.regularIndexes.push({
      field: key,
      options: options || {}
    });
    
    // Also ensure it's marked as saveable
    if (!target.saveableProperties) {
      target.saveableProperties = [];
    }
    
    if (!target.saveableProperties.includes(key)) {
      target.saveableProperties.push(key);
    }
  };
}

export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
  // Special case: detect MongoDB operators and pass them through directly
  const topLevelOperators = ['$and', '$or', '$nor', '$not', '$text', '$where', '$regex'];
  for (const key of Object.keys(filterArg)) {
    if (topLevelOperators.includes(key)) {
      return filterArg; // Return the filter as-is for MongoDB operators
    }
  }

  // Original conversion logic for non-MongoDB query objects
  const convertedFilter: { [key: string]: any } = {};

  const convertFilterArgument = (keyPathArg2: string, filterArg2: any) => {
    if (Array.isArray(filterArg2)) {
      // Directly assign arrays (they might be using operators like $in or $all)
      convertFilterArgument(keyPathArg2, filterArg2[0]);
    } else if (typeof filterArg2 === 'object' && filterArg2 !== null) {
      for (const key of Object.keys(filterArg2)) {
        if (key.startsWith('$')) {
          convertedFilter[keyPathArg2] = filterArg2;
          return;
        } else if (key.includes('.')) {
          throw new Error('keys cannot contain dots');
        }
      }
      for (const key of Object.keys(filterArg2)) {
        convertFilterArgument(`${keyPathArg2}.${key}`, filterArg2[key]);
      }
    } else {
      convertedFilter[keyPathArg2] = filterArg2;
    }
  };

  for (const key of Object.keys(filterArg)) {
    convertFilterArgument(key, filterArg[key]);
  }
  return convertedFilter;
};

export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends IManager = any> {
  /**
   * the collection object an Doc belongs to
   */
  public static collection: SmartdataCollection<any>;
  public collection: SmartdataCollection<any>;
  public static defaultManager;
  public static manager;
  public manager: TManager;

  // STATIC
  public static createInstanceFromMongoDbNativeDoc<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    mongoDbNativeDocArg: any,
  ): T {
    const newInstance = new this();
    (newInstance as any).creationStatus = 'db';
    for (const key of Object.keys(mongoDbNativeDocArg)) {
      newInstance[key] = mongoDbNativeDocArg[key];
    }
    return newInstance;
  }

  /**
   * gets all instances as array
   * @param this
   * @param filterArg
   * @returns
   */
  public static async getInstances<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
  ): Promise<T[]> {
    const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg));
    const returnArray = [];
    for (const foundDoc of foundDocs) {
      const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc);
      returnArray.push(newInstance);
    }
    return returnArray;
  }

  /**
   * gets the first matching instance
   * @param this
   * @param filterArg
   * @returns
   */
  public static async getInstance<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
  ): Promise<T> {
    const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg));
    if (foundDoc) {
      const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc);
      return newInstance;
    } else {
      return null;
    }
  }

  /**
   * get a unique id prefixed with the class name
   */
  public static async getNewId<T = any>(
    this: plugins.tsclass.typeFest.Class<T>,
    lengthArg: number = 20,
  ) {
    return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`;
  }

  /**
   * get cursor
   * @returns
   */
  public static async getCursor<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
  ) {
    const collection: SmartdataCollection<T> = (this as any).collection;
    const cursor: SmartdataDbCursor<T> = await collection.getCursor(
      convertFilterForMongoDb(filterArg),
      this as any as typeof SmartDataDbDoc,
    );
    return cursor;
  }

  public static async getCursorExtended<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
    modifierFunction = (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => cursorArg,
  ): Promise<SmartdataDbCursor<T>> {
    const collection: SmartdataCollection<T> = (this as any).collection;
    await collection.init();
    let cursor: plugins.mongodb.FindCursor<any> = collection.mongoDbCollection.find(
      convertFilterForMongoDb(filterArg),
    );
    cursor = modifierFunction(cursor);
    return new SmartdataDbCursor<T>(cursor, this as any as typeof SmartDataDbDoc);
  }

  /**
   * watch the collection
   * @param this
   * @param filterArg
   * @param forEachFunction
   */
  public static async watch<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
  ) {
    const collection: SmartdataCollection<T> = (this as any).collection;
    const watcher: SmartdataDbWatcher<T> = await collection.watch(
      convertFilterForMongoDb(filterArg),
      this as any,
    );
    return watcher;
  }

  /**
   * run a function for all instances
   * @returns
   */
  public static async forEach<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
    forEachFunction: (itemArg: T) => Promise<any>,
  ) {
    const cursor: SmartdataDbCursor<T> = await (this as any).getCursor(filterArg);
    await cursor.forEach(forEachFunction);
  }

  /**
   * returns a count of the documents in the collection
   */
  public static async getCount<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    filterArg: plugins.tsclass.typeFest.PartialDeep<T> = {} as any,
  ) {
    const collection: SmartdataCollection<T> = (this as any).collection;
    return await collection.getCount(filterArg);
  }

  /**
   * Create a MongoDB filter from a Lucene query string
   * @param luceneQuery Lucene query string
   * @returns MongoDB query object
   */
  public static createSearchFilter<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    luceneQuery: string,
  ): any {
    const searchableFields = (this as any).getSearchableFields();
    if (searchableFields.length === 0) {
      throw new Error(`No searchable fields defined for class ${this.name}`);
    }
    const adapter = new SmartdataLuceneAdapter(searchableFields);
    return adapter.convert(luceneQuery);
  }
  /**
   * List all searchable fields defined on this class
   */
  public static getSearchableFields(): string[] {
    const ctor = this as any;
    return Array.isArray(ctor.searchableFields) ? ctor.searchableFields : [];
  }

  /**
   * Search documents by text or field:value syntax, with safe regex fallback
   * @param query A search term or field:value expression
   * @returns Array of matching documents
   */
  public static async search<T>(
    this: plugins.tsclass.typeFest.Class<T>,
    query: string,
  ): Promise<T[]> {
    const searchableFields = (this as any).getSearchableFields();
    if (searchableFields.length === 0) {
      throw new Error(`No searchable fields defined for class ${this.name}`);
    }
    // empty query -> return all
    const q = query.trim();
    if (!q) {
      return await (this as any).getInstances({});
    }
    // simple exact field:value (no spaces, no wildcards, no quotes)
    // simple exact field:value (no spaces, wildcards, quotes)
    const simpleExact = q.match(/^(\w+):([^"'\*\?\s]+)$/);
    if (simpleExact) {
      const field = simpleExact[1];
      const value = simpleExact[2];
      if (!searchableFields.includes(field)) {
        throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
      }
      return await (this as any).getInstances({ [field]: value });
    }
    // quoted phrase across all searchable fields: exact match of phrase
    const quoted = q.match(/^"(.+)"$|^'(.+)'$/);
    if (quoted) {
      const phrase = quoted[1] || quoted[2] || '';
      // build regex that matches the exact phrase (allowing flexible whitespace)
      const parts = phrase.split(/\s+/).map((t) => escapeForRegex(t));
      const pattern = parts.join('\\s+');
      const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
      return await (this as any).getInstances({ $or: orConds });
    }
    // wildcard field:value (supports * and ?) -> direct regex on that field
    const wildcardField = q.match(/^(\w+):(.+[*?].*)$/);
    if (wildcardField) {
      const field = wildcardField[1];
      const pattern = wildcardField[2];
      if (!searchableFields.includes(field)) {
        throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
      }
      // escape regex special chars except * and ?, then convert wildcards
      const escaped = pattern.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
      const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
      return await (this as any).getInstances({ [field]: { $regex: regexPattern, $options: 'i' } });
    }
    // wildcard plain term across all fields (supports * and ?)
    if (!q.includes(':') && (q.includes('*') || q.includes('?'))) {
      // build wildcard regex pattern: escape all except * and ? then convert
      const escaped = q.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
      const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
      const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
      return await (this as any).getInstances({ $or: orConds });
    }
    // detect advanced Lucene syntax: field:value, wildcards, boolean, grouping
    const luceneSyntax = /(\w+:[^\s]+)|\*|\?|\bAND\b|\bOR\b|\bNOT\b|\(|\)/;
    if (luceneSyntax.test(q)) {
      const filter = (this as any).createSearchFilter(q);
      return await (this as any).getInstances(filter);
    }
    // multi-term unquoted -> AND of regex across fields for each term
    const terms = q.split(/\s+/);
    if (terms.length > 1) {
      const andConds = terms.map((term) => {
        const esc = escapeForRegex(term);
        const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
        return { $or: ors };
      });
      return await (this as any).getInstances({ $and: andConds });
    }
    // single term -> regex across all searchable fields
    const esc = escapeForRegex(q);
    const orConds = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
    return await (this as any).getInstances({ $or: orConds });
  }


  // INSTANCE

  // INSTANCE

  /**
   * how the Doc in memory was created, may prove useful later.
   */
  public creationStatus: TDocCreation = 'new';

  /**
   * updated from db in any case where doc comes from db
   */
  @globalSvDb()
  _createdAt: string = new Date().toISOString();

  /**
   * will be updated everytime the doc is saved
   */
  @globalSvDb()
  _updatedAt: string = new Date().toISOString();

  /**
   * an array of saveable properties of ALL doc
   */
  public globalSaveableProperties: string[];

  /**
   * unique indexes
   */
  public uniqueIndexes: string[];

  /**
   * regular indexes with their options
   */
  public regularIndexes: Array<{field: string, options: IIndexOptions}> = [];

  /**
   * an array of saveable properties of a specific doc
   */
  public saveableProperties: string[];

  /**
   * name
   */
  public name: string;

  /**
   * primary id in the database
   */
  public dbDocUniqueId: string;

  /**
   * class constructor
   */
  constructor() {}

  /**
   * saves this instance but not any connected items
   * may lead to data inconsistencies, but is faster
   */
  public async save() {
    // tslint:disable-next-line: no-this-assignment
    const self: any = this;
    let dbResult: any;

    this._updatedAt = new Date().toISOString();

    switch (this.creationStatus) {
      case 'db':
        dbResult = await this.collection.update(self);
        break;
      case 'new':
        dbResult = await this.collection.insert(self);
        this.creationStatus = 'db';
        break;
      default:
        console.error('neither new nor in db?');
    }
    return dbResult;
  }

  /**
   * deletes a document from the database
   */
  public async delete() {
    await this.collection.delete(this);
  }

  /**
   * also store any referenced objects to DB
   * better for data consistency
   */
  public saveDeep(savedMapArg: plugins.lik.ObjectMap<SmartDataDbDoc<any, any>> = null) {
    if (!savedMapArg) {
      savedMapArg = new plugins.lik.ObjectMap<SmartDataDbDoc<any, any>>();
    }
    savedMapArg.add(this);
    this.save();
    for (const propertyKey of Object.keys(this)) {
      const property: any = this[propertyKey];
      if (property instanceof SmartDataDbDoc && !savedMapArg.checkForObject(property)) {
        property.saveDeep(savedMapArg);
      }
    }
  }

  /**
   * updates an object from db
   */
  public async updateFromDb() {
    const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject());
    for (const key of Object.keys(mongoDbNativeDoc)) {
      this[key] = mongoDbNativeDoc[key];
    }
  }

  /**
   * creates a saveable object so the instance can be persisted as json in the database
   */
  public async createSavableObject(): Promise<TImplements> {
    const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
    const saveableProperties = [...this.globalSaveableProperties, ...this.saveableProperties];
    for (const propertyNameString of saveableProperties) {
      saveableObject[propertyNameString] = this[propertyNameString];
    }
    return saveableObject as TImplements;
  }

  /**
   * creates an identifiable object for operations that require filtering
   */
  public async createIdentifiableObject() {
    const identifiableObject: any = {}; // is not exposed to outside, so any is ok here
    for (const propertyNameString of this.uniqueIndexes) {
      identifiableObject[propertyNameString] = this[propertyNameString];
    }
    return identifiableObject;
  }
}