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

import { SmartdataCollection } from './classes.collection.js';
import { EasyStore } from './classes.easystore.js';

import { logger } from './logging.js';

/**
 * interface - indicates the connection status of the db
 */
export type TConnectionStatus = 'initial' | 'disconnected' | 'connected' | 'failed';

export class SmartdataDb {
  smartdataOptions: plugins.tsclass.database.IMongoDescriptor;
  mongoDbClient!: plugins.mongodb.MongoClient;
  mongoDb!: plugins.mongodb.Db;
  status: TConnectionStatus;
  statusConnectedDeferred = plugins.smartpromise.defer();
  smartdataCollectionMap = new plugins.lik.ObjectMap<SmartdataCollection<any>>();

  constructor(smartdataOptions: plugins.tsclass.database.IMongoDescriptor) {
    this.smartdataOptions = smartdataOptions;
    this.status = 'initial';
  }

  // easystore
  public async createEasyStore<T = unknown>(nameIdArg: string): Promise<EasyStore<T>> {
    const easyStore = new EasyStore<T>(nameIdArg, this);
    return easyStore;
  }

  // basic connection stuff ----------------------------------------------

  /**
   * connects to the database that was specified during instance creation
   */
  public async init(): Promise<any> {
    try {
      // Safely encode credentials to handle special characters
      const encodedUser = this.smartdataOptions.mongoDbUser 
        ? encodeURIComponent(this.smartdataOptions.mongoDbUser) 
        : '';
      const encodedPass = this.smartdataOptions.mongoDbPass 
        ? encodeURIComponent(this.smartdataOptions.mongoDbPass) 
        : '';
      
      const finalConnectionUrl = this.smartdataOptions.mongoDbUrl
        .replace('<USERNAME>', encodedUser)
        .replace('<username>', encodedUser)
        .replace('<USER>', encodedUser)
        .replace('<user>', encodedUser)
        .replace('<PASSWORD>', encodedPass)
        .replace('<password>', encodedPass)
        .replace('<DBNAME>', this.smartdataOptions.mongoDbName || '')
        .replace('<dbname>', this.smartdataOptions.mongoDbName || '');

      const clientOptions: plugins.mongodb.MongoClientOptions = {
        maxPoolSize: (this.smartdataOptions as any).maxPoolSize ?? 100,
        maxIdleTimeMS: (this.smartdataOptions as any).maxIdleTimeMS ?? 300000, // 5 minutes default
        serverSelectionTimeoutMS: (this.smartdataOptions as any).serverSelectionTimeoutMS ?? 30000,
        socketTimeoutMS: (this.smartdataOptions as any).socketTimeoutMS ?? 30000, // 30 seconds default — prevents hung operations from holding connections
        retryWrites: true,
      };

      this.mongoDbClient = await plugins.mongodb.MongoClient.connect(finalConnectionUrl, clientOptions);
      this.mongoDb = this.mongoDbClient.db(this.smartdataOptions.mongoDbName);
      this.status = 'connected';
      this.statusConnectedDeferred.resolve();
      logger.log('info', `Connected to database ${this.smartdataOptions.mongoDbName}`);
    } catch (error) {
      this.status = 'disconnected';
      this.statusConnectedDeferred.reject(error);
      logger.log('error', `Failed to connect to database ${this.smartdataOptions.mongoDbName}: ${(error as Error).message}`);
      throw error;
    }
  }

  /**
   * closes the connection to the databse
   */
  public async close(): Promise<any> {
    await this.mongoDbClient.close();
    this.status = 'disconnected';
    logger.log('info', `disconnected from database ${this.smartdataOptions.mongoDbName}`);
  }
  /**
   * Start a MongoDB client session for transactions
   */
  public startSession(): plugins.mongodb.ClientSession {
    return this.mongoDbClient.startSession();
  }

  // handle table to class distribution

  public addCollection(SmartdataCollectionArg: SmartdataCollection<any>) {
    this.smartdataCollectionMap.add(SmartdataCollectionArg);
  }

  /**
   * Gets a collection's name and returns a SmartdataCollection instance
   * @param nameArg
   * @returns DbTable
   */
  public async getSmartdataCollectionByName<T>(nameArg: string): Promise<SmartdataCollection<T>> {
    const resultCollection = await this.smartdataCollectionMap.find(async (dbTableArg) => {
      return dbTableArg.collectionName === nameArg;
    });
    return resultCollection;
  }
}
