import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize";
import { IMapperRegistry } from "../mappers";

export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
  protected readonly _database!: Sequelize;
  protected readonly _registry!: IMapperRegistry;

  constructor(params: { mapperRegistry: IMapperRegistry; database: Sequelize }) {
    this._registry = params.mapperRegistry;
    this._database = params.database;
  }

  protected convertCriteria(criteria: Criteria): FindOptions {
    return new CriteriaToSequelizeConverter().convert(criteria);
  }

  protected async _findAll(
    model: ModelDefined<any, any>,
    params: any = {},
    transaction?: Transaction
  ): Promise<any[]> {
    return model.findAll({
      transaction,
      ...params,
    });
  }

  protected _findById(
    model: ModelDefined<any, any>,
    id: string,
    params: any = {},
    transaction?: Transaction
  ): Promise<any> {
    return model.findByPk(id.toString(), {
      transaction,
      ...params,
    });
  }

  protected async _getBy(
    model: ModelDefined<any, any>,
    field: string,
    value: any,
    params: any = {},
    transaction?: Transaction
  ): Promise<any> {
    const where: { [key: string]: any } = {};

    where[field] = value;

    return model.findOne({
      where,
      transaction,
      ...params,
    });
  }

  protected async _getById(
    model: ModelDefined<any, any>,
    id: UniqueID | string,
    params: any = {},
    transaction?: Transaction
  ): Promise<any> {
    return model.findByPk(id.toString(), {
      transaction,
      ...params,
    });
  }

  protected async _exists(
    model: ModelDefined<any, any>,
    field: string,
    value: any,
    transaction?: Transaction
  ): Promise<boolean> {
    const where: { [key: string]: any } = {};
    where[field] = value;

    const count: number = await model.count({
      where,
      transaction,
    });

    return Promise.resolve(Boolean(count !== 0));
  }

  protected async _save(
    model: ModelDefined<any, any>,
    id: UniqueID,
    data: any,
    params: any = {},
    transaction?: Transaction
  ): Promise<void> {
    if (await this._exists(model, "id", id.toString())) {
      await model.update(
        {
          ...data,
          id: undefined,
        },
        {
          where: { id: id.toString() },
          transaction: transaction,
          ...params,
        }
      );
    } else {
      await model.create(
        {
          ...data,
          id: id.toString(),
        },
        {
          include: [{ all: true }],
          transaction: transaction,
          ...params,
        }
      );
    }
  }

  protected async _deleteById(
    model: ModelDefined<any, any>,
    id: UniqueID,
    force = false,
    transaction?: Transaction
  ) {
    await model.destroy({
      where: {
        id: id.toString(),
      },
      transaction,
      force,
    });
  }

  /**
   * 🔹 Manejo de errores de la base de datos con personalización
   * @param error Error lanzado por Sequelize
   * @param errorMapper Función opcional para personalizar errores específicos
   */
  protected _handleDatabaseError(
    error: unknown,
    errorMapper?: (error: Error) => string | null
  ): Result<never, Error> {
    const _error = error as Error;

    //logger.error({ message: `Database error: ${_error.message}`, label: "SequelizeRepository" });

    // Si la clase hija proporciona un mapeo personalizado, lo usa
    if (errorMapper) {
      const mappedMessage = errorMapper(_error);
      if (mappedMessage) {
        return Result.fail(new Error(mappedMessage));
      }
    }

    // Mapa de errores genéricos para la base de datos
    const ERROR_MAP = new Map<string, string>([
      ["SequelizeConnectionError", "Database connection lost"],
      ["SequelizeTimeoutError", "Database request timed out"],
    ]);

    return Result.fail(new Error(ERROR_MAP.get(_error.name) || "Unexpected database error"));
  }
}
