import nodemailer, { Transporter, SendMailOptions } from 'nodemailer';
import { readFile } from 'fs/promises';
import validator from 'validator';
import { EventEmitter } from 'events';
import mongoose, { Schema, Model, Document } from 'mongoose';
import { Sequelize, DataTypes, Model as SequelizeModel, Optional, Op } from 'sequelize';
import Handlebars from 'handlebars';

/**
 * Configuration for the mail transporter.
 * @typedef {Object} MailConfig
 * @property {string} host - The SMTP host (e.g., smtp.gmail.com).
 * @property {number} port - The SMTP port (e.g., 587).
 * @property {string} user - The SMTP username (e.g., your-email@gmail.com).
 * @property {string} pass - The SMTP password or app-specific password.
 * @property {boolean} [secure] - Whether to use a secure connection (defaults to true if port is 465).
 */
interface MailConfig {
  host: string;
  port: number;
  user: string;
  pass: string;
  secure?: boolean;
}

/**
 * Options for configuring the WaitlistMailer.
 * @typedef {Object} WaitlistMailerOptions
 * @property {string} [companyName] - The name of the company for email templates.
 * @property {string} [mongoUri] - The MongoDB connection URI (e.g., mongodb://localhost:27017/waitlistdb).
 * @property {Object} [sqlConfig] - Configuration for SQL databases.
 * @property {'postgres' | 'mysql'} sqlConfig.dialect - The SQL dialect (PostgreSQL or MySQL).
 * @property {string} sqlConfig.host - The SQL database host.
 * @property {number} sqlConfig.port - The SQL database port.
 * @property {string} sqlConfig.username - The SQL database username.
 * @property {string} sqlConfig.password - The SQL database password.
 * @property {string} sqlConfig.database - The SQL database name.
 */
interface WaitlistMailerOptions {
  companyName?: string;
  mongoUri?: string;
  sqlConfig?: {
    dialect: 'postgres' | 'mysql' | 'sqlite';
    host: string;
    port: number;
    username: string;
    password: string;
    database: string;
  };
}
/**
 * Attributes for the Waitlist model.
 * @typedef {Object} WaitlistAttributes
 * @property {string} email - The email address.
 * @property {Date} [createdAt] - The creation date of the record.
 */
interface WaitlistAttributes {
  email: string;
  createdAt?: Date;
}

/**
 * Creation attributes for the Waitlist model.
 * @typedef {Object} WaitlistCreationAttributes
 * @extends {Optional<WaitlistAttributes, 'email' | 'createdAt'>}
 */
interface WaitlistCreationAttributes extends Optional<WaitlistAttributes, 'email' | 'createdAt'> {}

/**
 * Sequelize model for the Waitlist table.
 * @class WaitlistSequelize
 * @extends {SequelizeModel<WaitlistAttributes, WaitlistCreationAttributes>}
 * @implements {WaitlistAttributes}
 */
class WaitlistSequelize extends SequelizeModel<WaitlistAttributes, WaitlistCreationAttributes> implements WaitlistAttributes {
  declare email: string;
  declare createdAt: Date;
}

/**
 * Mongoose schema for the Waitlist collection.
 * @constant {Schema} WaitlistSchema
 */
const WaitlistSchema = new Schema<Document & WaitlistAttributes>({
  email: { type: String, required: true, unique: true, index: true },
  createdAt: { type: Date, default: Date.now },
});

/**
 * Mongoose model for the Waitlist collection.
 * @constant {Model<Document & WaitlistAttributes>} WaitlistModel
 */
const WaitlistModel: Model<Document & WaitlistAttributes> = mongoose.model<Document & WaitlistAttributes>('Waitlist', WaitlistSchema);

/**
 * Enum for storage types.
 * @enum {string}
 */
export enum StorageType {
  Local = 'local',
  Db = 'db',
  Sql = 'sql',
}

/**
 * Main class for managing waitlists and sending confirmation emails.
 * @class WaitlistMailer
 * @extends {EventEmitter}
 */
export class WaitlistMailer extends EventEmitter {
  private storage: StorageType;
  private waitlist: Set<string>;
  private transporter: Transporter;
  private fromEmail: string;
  private companyName: string;
  private mongoUri?: string;
  private sqlConnection?: Sequelize;
  private initialized: boolean = false;

  /**
   * Creates an instance of WaitlistMailer.
   * @param {StorageType} [storage=StorageType.Local] - The storage type (local, db, or sql).
   * @param {MailConfig} mailConfig - The mail configuration.
   * @param {WaitlistMailerOptions} [options] - Additional options for the mailer.
   * @throws {Error} If mailConfig parameters are invalid.
   */
  constructor(storage: StorageType = StorageType.Local, mailConfig: MailConfig, options?: WaitlistMailerOptions) {
    super();
    this.storage = storage;
    this.waitlist = new Set();
    this.companyName = options?.companyName || 'Your Company';
    this.mongoUri = options?.mongoUri;
    this.fromEmail = mailConfig.user;

    // Validate mailConfig
    if (!mailConfig.host || !mailConfig.port || !mailConfig.user || !mailConfig.pass) {
      throw new Error('Invalid mail configuration: host, port, user, and pass are required');
    }

    // Initialize SQL connection if configured
    if (options?.sqlConfig) {
      this.sqlConnection = new Sequelize({
        dialect: options.sqlConfig.dialect,
        host: options.sqlConfig.host,
        port: options.sqlConfig.port,
        username: options.sqlConfig.username,
        password: options.sqlConfig.password,
        database: options.sqlConfig.database,
        logging: false,
      });
    }

    // Initialize Nodemailer transporter
    this.transporter = nodemailer.createTransport({
      host: mailConfig.host,
      port: mailConfig.port,
      secure: mailConfig.secure ?? mailConfig.port === 465,
      auth: {
        user: mailConfig.user,
        pass: mailConfig.pass,
      },
    });

    // Initialize the mailer asynchronously
    this.initialize()
      .then(() => console.log('WaitlistMailer initialized'))
      .catch(error => this.handleError('initialize', 'Initialization failed', error));
  }

  // ==================== Private Methods ====================

  /**
   * Initializes the mailer, including database connections and data loading.
   * @private
   * @returns {Promise<void>}
   */
  private async initialize(): Promise<void> {
    await this.verifyTransporter();
    await this.initializeStorage();
    await this.loadInitialData();
    this.initialized = true;
    this.emit('onInitialized');
  }

  /**
   * Verifies the Nodemailer transporter configuration.
   * @private
   * @returns {Promise<void>}
   * @throws {Error} If transporter verification fails.
   */
  private async verifyTransporter(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.transporter.verify(error => {
        if (error) {
          this.emit('onTransporterError', error);
          reject(error);
        } else {
          this.emit('onTransporterReady');
          resolve();
        }
      });
    });
  }

  /**
   * Initializes the database storage (MongoDB or SQL).
   * @private
   * @returns {Promise<void>}
   */
  private async initializeStorage(): Promise<void> {
    if (this.storage === StorageType.Db && this.mongoUri) {
      await mongoose.connect(this.mongoUri);
      this.emit('onDbConnected');
    } else if (this.storage === StorageType.Sql && this.sqlConnection) {
      await this.initializeSequelize();
      this.emit('onSqlConnected');
    }
  }

  /**
   * Initializes the Sequelize model and connection.
   * @private
   * @returns {Promise<void>}
   */
  private async initializeSequelize(): Promise<void> {
    if (!this.sqlConnection) return;

    WaitlistSequelize.init({
      email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true,
      },
      createdAt: {
        type: DataTypes.DATE,
        defaultValue: DataTypes.NOW,
      },
    }, {
      sequelize: this.sqlConnection,
      modelName: 'Waitlist',
    });

    await this.sqlConnection.authenticate();
    await WaitlistSequelize.sync();
  }

  /**
   * Loads initial data from the database into memory.
   * @private
   * @returns {Promise<void>}
   */
  private async loadInitialData(): Promise<void> {
    if (this.storage === StorageType.Db) {
      await this.loadFromMongo();
    } else if (this.storage === StorageType.Sql) {
      await this.loadFromSql();
    }
  }

  /**
   * Loads data from MongoDB.
   * @private
   * @returns {Promise<void>}
   */
  private async loadFromMongo(): Promise<void> {
    try {
      const docs = await WaitlistModel.find();
      docs.forEach(doc => this.waitlist.add(doc.email));
    } catch (error) {
      this.handleError('loadFromMongo', 'Failed to load from MongoDB', error);
    }
  }

  /**
   * Loads data from SQL.
   * @private
   * @returns {Promise<void>}
   */
  private async loadFromSql(): Promise<void> {
    if (!this.sqlConnection) return;

    try {
      const records = await WaitlistSequelize.findAll();
      records.forEach(record => this.waitlist.add(record.email));
    } catch (error) {
      this.handleError('loadFromSql', 'Failed to load from SQL', error);
    }
  }

  /**
   * Validates an email address using validator.js.
   * @private
   * @param {string} email - The email to validate.
   * @returns {{ isValid: boolean; message?: string }} - The validation result.
   */
  private validateEmail(email: string): { isValid: boolean; message?: string } {
    if (!validator.isEmail(email)) {
      return { isValid: false, message: 'Invalid email format' };
    }
    return { isValid: true };
  }

  /**
   * Handles errors and emits error events.
   * @private
   * @param {string} context - The context where the error occurred.
   * @param {string} message - A descriptive error message.
   * @param {unknown} error - The error object.
   */
  private handleError(context: string, message: string, error: unknown): void {
    const errorMessage = error instanceof Error ? error.message : String(error);
    const errorCode = error instanceof Error ? (error as any).code || 'UNKNOWN' : 'UNKNOWN';

    console.error(`[${context}] ${message}:`, errorMessage);
    this.emit('onError', {
      context,
      message,
      error: errorMessage,
      code: errorCode,
    });
  }

  /**
   * Persists an email to the database.
   * @private
   * @param {string} email - The email to persist.
   * @returns {Promise<void>}
   */
  private async persistEmail(email: string): Promise<void> {
    const record = { email, createdAt: new Date() };
    try {
      if (this.storage === StorageType.Db) {
        await new WaitlistModel(record).save();
      } else if (this.storage === StorageType.Sql) {
        await WaitlistSequelize.create(record);
      }
    } catch (error) {
      this.handleError('persistEmail', 'Failed to persist email', error);
      throw error; // Propagate error to caller
    }
  }

  /**
   * Removes an email from the database.
   * @private
   * @param {string} email - The email to remove.
   * @returns {Promise<void>}
   */
  private async removePersistedEmail(email: string): Promise<void> {
    try {
      if (this.storage === StorageType.Db) {
        await WaitlistModel.deleteOne({ email });
      } else if (this.storage === StorageType.Sql) {
        await WaitlistSequelize.destroy({ where: { email } });
      }
    } catch (error) {
      this.handleError('removePersistedEmail', 'Failed to remove email', error);
      throw error; // Propagate error to caller
    }
  }

  // ==================== Public API ====================

  /**
   * Checks if the mailer is initialized.
   * @returns {boolean} - True if initialized, false otherwise.
   */
  public isInitialized(): boolean {
    return this.initialized;
  }

  /**
   * Waits for the mailer to initialize.
   * @returns {Promise<void>}
   */
  public async waitForInitialization(): Promise<void> {
    if (this.initialized) return;
    return new Promise(resolve => this.once('onInitialized', resolve));
  }

  /**
   * Adds an email to the waitlist.
   * @param {string} email - The email to add.
   * @returns {Promise<boolean>} - True if the email was added successfully, false otherwise.
   * @throws {Error} If persistence fails.
   */
  public async addEmail(email: string): Promise<boolean> {
    if (!this.initialized) {
      this.handleError('addEmail', 'WaitlistMailer not initialized', new Error('Not initialized'));
      return false;
    }

    const validation = this.validateEmail(email);
    if (!validation.isValid) {
      this.emit('onValidationError', validation);
      return false;
    }

    if (this.waitlist.has(email)) {
      this.emit('onDuplicateEmail', email);
      return false;
    }

    this.waitlist.add(email);
    await this.persistEmail(email);
    this.emit('onEmailAdded', email);
    return true;
  }

  /**
   * Removes an email from the waitlist.
   * @param {string} email - The email to remove.
   * @returns {Promise<boolean>} - True if the email was removed successfully, false otherwise.
   * @throws {Error} If removal fails.
   */
  public async removeEmail(email: string): Promise<boolean> {
    if (!this.initialized || !this.waitlist.has(email)) {
      return false;
    }

    this.waitlist.delete(email);
    await this.removePersistedEmail(email);
    this.emit('onEmailRemoved', email);
    return true;
  }

  /**
   * Gets the current waitlist.
   * @returns {string[]} - An array of emails in the waitlist.
   */
  public getWaitlist(): string[] {
    return Array.from(this.waitlist);
  }

  /**
   * Clears the waitlist.
   * @returns {Promise<void>}
   */
  public async clearWaitlist(): Promise<void> {
    this.waitlist.clear();

    if (this.storage === StorageType.Db) {
      await WaitlistModel.deleteMany({});
    } else if (this.storage === StorageType.Sql) {
      await WaitlistSequelize.destroy({ where: {} });
    }

    this.emit('onWaitlistCleared');
  }

  /**
   * Sends a confirmation email.
   * @param {string} email - The email to send to.
   * @param {(email: string) => string} subjectTemplate - A function to generate the email subject.
   * @param {(email: string) => string} bodyTemplate - A function to generate the email body.
   * @returns {Promise<boolean>} - True if the email was sent successfully, false otherwise.
   */
  public async sendConfirmation(
    email: string,
    subjectTemplate: (email: string) => string,
    bodyTemplate: (email: string) => string
  ): Promise<boolean> {
    if (!this.waitlist.has(email)) {
      this.handleError('sendConfirmation', 'Email not in waitlist', new Error('Email not found'));
      return false;
    }

    try {
      const subject = subjectTemplate(email);
      const html = bodyTemplate(email).replace(/\[Company Name\]/g, this.companyName);

      const mailOptions: SendMailOptions = {
        from: `"${this.companyName}" <${this.fromEmail}>`,
        to: email,
        subject,
        html,
      };

      await this.transporter.sendMail(mailOptions);
      this.emit('onEmailSent', email);
      return true;
    } catch (error) {
      this.handleError('sendConfirmation', 'Failed to send confirmation', error);
      return false;
    }
  }

  /**
   * Sends a confirmation email using a template file.
   * @param {string} email - The email to send to.
   * @param {(email: string) => string} subjectTemplate - A function to generate the email subject.
   * @param {string} templatePath - The path to the template file.
   * @param {Record<string, string>} [replacements={}] - Replacements for the template.
   * @returns {Promise<boolean>} - True if the email was sent successfully, false otherwise.
   */
  public async sendConfirmationFromFile(
    email: string,
    subjectTemplate: (email: string) => string,
    templatePath: string,
    replacements: Record<string, string> = {}
  ): Promise<boolean> {
    try {
      const templateContent = await readFile(templatePath, 'utf8');
      if (!templateContent) {
        throw new Error('Template file is empty');
      }
      const template = Handlebars.compile(templateContent);
      const html = template({
        email,
        companyName: this.companyName,
        ...replacements,
      });

      return this.sendConfirmation(email, subjectTemplate, () => html);
    } catch (error) {
      this.handleError('sendConfirmationFromFile', 'Template processing failed', error);
      return false;
    }
  }

  /**
   * Sends a confirmation email with retry logic.
   * @param {string} email - The email to send to.
   * @param {(email: string) => string} subjectTemplate - A function to generate the email subject.
   * @param {(email: string) => string} bodyTemplate - A function to generate the email body.
   * @param {number} [maxRetries=3] - The maximum number of retry attempts.
   * @param {number} [retryDelay=1000] - The delay between retries in milliseconds.
   * @returns {Promise<boolean>} - True if the email was sent successfully, false otherwise.
   */
  public async sendConfirmationWithRetry(
    email: string,
    subjectTemplate: (email: string) => string,
    bodyTemplate: (email: string) => string,
    maxRetries: number = 3,
    retryDelay: number = 1000
  ): Promise<boolean> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const result = await this.sendConfirmation(email, subjectTemplate, bodyTemplate);
        if (result) return true;

        if (attempt < maxRetries) {
          await new Promise(resolve => setTimeout(resolve, retryDelay));
          this.emit('onEmailRetry', email, attempt + 1);
        }
      } catch (error) {
        this.handleError('sendConfirmationWithRetry', `Attempt ${attempt + 1} failed`, error);
      }
    }
    return false;
  }

  /**
   * Sends confirmation emails to all emails in the waitlist.
   * @param {(email: string) => string} subjectTemplate - A function to generate the email subject.
   * @param {(email: string) => string} bodyTemplate - A function to generate the email body.
   * @param {number} [maxRetries=3] - The maximum number of retry attempts per email.
   * @param {number} [retryDelay=1000] - The delay between retries in milliseconds.
   * @returns {Promise<number>} - The number of successfully sent emails.
   */
  public async sendBulkConfirmation(
    subjectTemplate: (email: string) => string,
    bodyTemplate: (email: string) => string,
    maxRetries: number = 3,
    retryDelay: number = 1000
  ): Promise<number> {
    const emails = this.getWaitlist();
    let successCount = 0;

    for (const email of emails) {
      const success = await this.sendConfirmationWithRetry(
        email,
        subjectTemplate,
        bodyTemplate,
        maxRetries,
        retryDelay
      );
      if (success) successCount++;
    }

    this.emit('onBulkConfirmationComplete', { successCount, total: emails.length });
    return successCount;
  }

  /**
   * Finds emails in the waitlist that match a pattern.
   * @param {string} pattern - The pattern to search for.
   * @returns {Promise<string[]>} - An array of matching emails.
   */
  public async findEmailsByPattern(pattern: string): Promise<string[]> {
    try {
      if (this.storage === StorageType.Local) {
        return Array.from(this.waitlist).filter(email =>
          email.toLowerCase().includes(pattern.toLowerCase())
        );
      }

      if (this.storage === StorageType.Db) {
        const regex = new RegExp(pattern, 'i');
        const docs = await WaitlistModel.find({ email: { $regex: regex } });
        return docs.map(doc => doc.email);
      }

      if (this.storage === StorageType.Sql) {
        const records = await WaitlistSequelize.findAll({
          where: { email: { [Op.like]: `%${pattern}%` } },
        });
        return records.map(record => record.email);
      }

      return [];
    } catch (error) {
      this.handleError('findEmailsByPattern', 'Search failed', error);
      return [];
    }
  }

  /**
   * Counts the number of emails in the waitlist within a date range.
   * @param {Date} [start] - The start date of the range.
   * @param {Date} [end] - The end date of the range.
   * @returns {Promise<number>} - The count of emails.
   */
  public async countWaitlistByDate(start?: Date, end?: Date): Promise<number> {
    try {
      if (this.storage === StorageType.Local) {
        return this.waitlist.size;
      }

      if (this.storage === StorageType.Db) {
        const query: any = {};
        if (start || end) {
          query.createdAt = {};
          if (start) query.createdAt.$gte = start;
          if (end) query.createdAt.$lte = end;
        }
        return WaitlistModel.countDocuments(query);
      }

      if (this.storage === StorageType.Sql) {
        const where: any = {};
        if (start && end) {
          where.createdAt = { [Op.between]: [start, end] };
        } else if (start) {
          where.createdAt = { [Op.gte]: start };
        } else if (end) {
          where.createdAt = { [Op.lte]: end };
        }
        return WaitlistSequelize.count({ where });
      }

      return 0;
    } catch (error) {
      this.handleError('countWaitlistByDate', 'Count failed', error);
      return 0;
    }
  }

  /**
   * Saves the waitlist to the database.
   * @returns {Promise<boolean>} - True if the waitlist was saved successfully, false otherwise.
   */
  public async saveWaitlist(): Promise<boolean> {
    try {
      const emails = this.getWaitlist();
  
      if (this.storage === StorageType.Db) {
        await WaitlistModel.deleteMany({});
        if (emails.length > 0) {
          await WaitlistModel.insertMany(emails.map(email => ({ email })));
        }
        this.emit('onWaitlistSaved', emails); 
        return true;
      }
  
      if (this.storage === StorageType.Sql) {
        await this.sqlConnection?.transaction(async t => {
          await WaitlistSequelize.destroy({ where: {}, transaction: t });
          if (emails.length > 0) {
            await WaitlistSequelize.bulkCreate(
              emails.map(email => ({ email })),
              { transaction: t }
            );
          }
        });
        this.emit('onWaitlistSaved', emails); 
        return true;
      }
  
      return true; // Local storage no necesita guardar
    } catch (error) {
      this.handleError('saveWaitlist', 'Save failed', error);
      return false;
    }
  }  

  /**
   * Closes all database connections.
   * @returns {Promise<void>}
   */
  public async close(): Promise<void> {
    try {
      if (this.storage === StorageType.Db) {
        await mongoose.disconnect();
      } else if (this.storage === StorageType.Sql && this.sqlConnection) {
        await this.sqlConnection.close();
      }
      this.emit('onClose');
    } catch (error) {
      this.handleError('close', 'Failed to close connections', error);
    }
  }
}