import { config } from "@/config";
import rTracer from "cls-rtracer";
import { DataTypes, Sequelize } from "sequelize";

import { initLogger } from "@/infrastructure/logger";
import * as glob from "glob";
import * as path from "path";
import { IAdapter } from "../../domain";
import { InfrastructureError } from "../InfrastructureError";
import {
  SequelizeBusinessTransaction,
  SequelizeBusinessTransactionType,
} from "./SequelizeBusinessTransaction";
import { ISequelizeModels } from "./SequelizeModel.interface";
import { ISequelizeQueryBuilder } from "./queryBuilder/SequelizeQueryBuilder";

//import * as dotenv from "dotenv";
//dotenv.config();

export interface ISequelizeAdapter extends IAdapter {
  queryBuilder: ISequelizeQueryBuilder;
  startTransaction: () => SequelizeBusinessTransactionType;
  getModel: (modelName: string) => any;
  hasModel: (modelName: string) => boolean;
}

export class SequelizeAdapter implements ISequelizeAdapter {
  // eslint-disable-next-line no-use-before-define
  private static instance: SequelizeAdapter;

  public static getInstance(params: { queryBuilder: ISequelizeQueryBuilder }): SequelizeAdapter {
    if (!SequelizeAdapter.instance) {
      SequelizeAdapter.instance = SequelizeAdapter.create(params);
    }

    return SequelizeAdapter.instance;
  }

  private static create(params: { queryBuilder: ISequelizeQueryBuilder }) {
    const { queryBuilder } = params;

    const connection = initConnection();
    const models = registerModels(connection);

    return new SequelizeAdapter(connection, models, queryBuilder);
  }

  private _connection: Sequelize;
  private _models: ISequelizeModels;
  private _queryBuilder: ISequelizeQueryBuilder;

  protected constructor(
    connection: Sequelize,
    models: ISequelizeModels,
    queryBuilder: ISequelizeQueryBuilder
  ) {
    this._connection = connection;
    this._models = models;
    this._queryBuilder = queryBuilder;
  }

  get queryBuilder(): ISequelizeQueryBuilder {
    return this._queryBuilder;
  }

  get connection(): Sequelize {
    return this._connection;
  }

  public startTransaction(): SequelizeBusinessTransactionType {
    return new SequelizeBusinessTransaction(this._connection);
  }

  public sync(params) {
    return this._connection.sync(params);
  }

  public getModel(modelName: string): any {
    if (this.hasModel(modelName)) {
      return this._models[modelName];
    }
    throw InfrastructureError.create(
      InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
      `[SequelizeAdapter] ${modelName} sequelize model not exists!`
    );
  }

  public hasModel(modelName: string): boolean {
    return !!this._models[modelName];
  }
}

function initConnection(): Sequelize {
  const { username, password, database, host, dialect, port } = config.database;
  const logger = initLogger(rTracer);

  logger.debug("=========================> CONECTO A SEQUELIZE");

  return new Sequelize(database, username, password, {
    host,
    dialect,
    port,
    dialectOptions: {
      multipleStatements: true,
      dateStrings: true,
      typeCast: true,
      //timezone: "Z",
    },
    pool: {
      max: 5,
      min: 0,
      acquire: 60000,
      idle: 10000,
    },
    logQueryParameters: true,
    logging: (sql, timing) => logger.debug(sql, timing),

    define: {
      charset: "utf8mb4",
      collate: "utf8mb4_unicode_ci",
      //freezeTableName: true,
      underscored: true,
      timestamps: true,
    },
  });
}

function registerModels(connection: Sequelize): ISequelizeModels {
  const cwd = path.resolve(`${__dirname}/../../../`);
  const models: ISequelizeModels = {};

  // Get all models
  const globOptions = {
    cwd,
    nocase: true,
    nodir: true,
    absolute: true,
  };

  glob.sync("**/*.model.{js,ts}", globOptions).forEach(function (file) {
    console.log(`>> ${file}`);

    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const modelDef = require(path.join(file)).default;
    const model = typeof modelDef === "function" ? modelDef(connection, DataTypes) : false;
    if (model) models[model.name] = model;
  });

  for (const modelName in models) {
    const model = models[modelName];

    if (model.associate) {
      model.associate(connection, models);
    }

    if (model.hooks) {
      model.hooks(connection);
    }
  }

  return models;
}
