import { Collection, Entity, ICollection, Result } from "@shared/contexts";
import { Model, ValidationError } from "sequelize";
import { FieldValueError, RequiredFieldMissingError } from "../../domain/errors";
import { InfrastructureError } from "../InfrastructureError";

export type MapperParamsType = Record<string, any>;

export interface ISequelizeMapper<
  TModel extends Model,
  TModelAttributes,
  TEntity extends Entity<any>,
> {
  mapToDomain(source: TModel, params?: MapperParamsType): TEntity;
  mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection<TEntity>;
  mapArrayAndCountToDomain(
    source: TModel[],
    totalCount: number,
    params?: MapperParamsType
  ): Collection<TEntity>;

  mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes;
  mapCollectionToPersistence(
    source: ICollection<TEntity>,
    params?: MapperParamsType
  ): TModelAttributes[];
}

export abstract class SequelizeMapper<
  TModel extends Model = any,
  TModelAttributes = any,
  TEntity extends Entity<any> = any,
> implements ISequelizeMapper<TModel, TModelAttributes, TEntity>
{
  public constructor(protected props: any) {}

  public mapToDomain(source: TModel, params?: MapperParamsType): TEntity {
    return this.toDomainMappingImpl(source, params);
  }

  public mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection<TEntity> {
    return this.mapArrayAndCountToDomain(source, source ? source.length : 0, params);
  }

  public mapArrayAndCountToDomain(
    source: TModel[],
    totalCount: number,
    params?: MapperParamsType
  ): Collection<TEntity> {
    const items = source
      ? source.map((value, index: number) => this.toDomainMappingImpl!(value, { index, ...params }))
      : [];

    return new Collection(items, totalCount);
  }

  public mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes {
    return this.toPersistenceMappingImpl(source, params);
  }

  public mapCollectionToPersistence(
    source: ICollection<TEntity>,
    params?: MapperParamsType
  ): TModelAttributes[] {
    return source.items.map((value: TEntity, index: number) =>
      this.toPersistenceMappingImpl!(value, { index, ...params })
    );
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  protected toDomainMappingImpl(source: TModel, params?: MapperParamsType): TEntity {
    throw InfrastructureError.create(
      InfrastructureError.UNEXCEPTED_ERROR,
      'Method "toDomainMappingImpl" not implemented!'
    );
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  protected toPersistenceMappingImpl(source: TEntity, params?: MapperParamsType): TModelAttributes {
    throw InfrastructureError.create(
      InfrastructureError.UNEXCEPTED_ERROR,
      'Method "toPersistenceMappingImpl" not implemented!'
    );
  }

  protected handleRequiredFieldError(key: string, error: Error) {
    throw RequiredFieldMissingError.field(key, error.message);
  }

  protected handleInvalidFieldError(key: string, error: ValidationError) {
    throw FieldValueError.field(key, error.message);
  }

  protected mapsValue(
    row: TModel,
    key: string,
    customMapFn: (value: any, params: MapperParamsType) => Result<any, Error>,
    params: MapperParamsType = {
      defaultValue: null,
    }
  ) {
    const value = row?.dataValues[key] ?? params.defaultValue;
    const valueOrError = customMapFn(value, params);

    if (valueOrError.isFailure) {
      this._handleFailure(valueOrError.error, key);
    }

    return valueOrError.object;

    /*let value = params.defaultValue;

    if (!row || typeof row !== "object") {
      console.debug(`Data row has not keys! Key ${key} not exists in data row!`);
    } else if (!Object.hasOwn(row.dataValues, key)) {
      console.debug(`Key ${key} not exists in data row!`);
    } else {
      value = row.getDataValue(key);
    }

    const valueOrError = customMapFn(value, params);

    if (valueOrError.isFailure) {
      this.handleFailure(valueOrError.error, key);
    }
    return valueOrError.object;*/
  }

  protected mapsAssociation(
    row: TModel,
    associationName: string,
    customMapper: any,
    params: MapperParamsType = {}
  ) {
    if (!customMapper)
      throw InfrastructureError.create(
        InfrastructureError.UNEXCEPTED_ERROR,
        'Custom mapper undefined at "mapsAssociation"!'
      );

    const { filter, ...otherParams } = params;
    let associationRows = row?.dataValues[associationName] ?? [];

    if (filter)
      associationRows = Array.isArray(associationRows)
        ? associationRows.filter(filter)
        : filter(associationRows);

    const customMapFn = Array.isArray(associationRows)
      ? customMapper.mapArrayToDomain
      : customMapper.mapToDomain;

    if (!customMapFn)
      throw InfrastructureError.create(
        InfrastructureError.UNEXCEPTED_ERROR,
        'Custom mapper function undefined at "mapsAssociation"!'
      );

    const associatedDataOrError = customMapFn(associationRows, otherParams);

    if (associatedDataOrError.isFailure)
      this._handleFailure(associatedDataOrError.error, associationName);
    return associatedDataOrError.object;

    /*if (!customMapper) {
      throw InfrastructureError.create(
        InfrastructureError.UNEXCEPTED_ERROR,
        'Custom mapper undefined at "mapsAssociation"!'
      );
    }

    const { filter, ...otherParams } = params;

    let associationRows = [];

    if (Object.keys(row).length === 0) {
      console.debug(
        `Data row has not keys! Association ${associationName} not exists in data row!`
      );
    } else if (!Object.hasOwn(row.dataValues, associationName)) {
      console.debug(`Association ${associationName} not exists in data row!`);
    } else {
      associationRows = row.getDataValue(associationName);
    }

    const customMapFn =
      Array.isArray(associationRows) && associationRows.length > 0
        ? customMapper.mapArrayToDomain
        : customMapper.mapToDomain;

    if (filter) {
      associationRows = Array.isArray(associationRows)
        ? associationRows.filter(filter)
        : filter(associationRows);
    }

    if (!customMapFn) {
      throw InfrastructureError.create(
        InfrastructureError.UNEXCEPTED_ERROR,
        'Custom mapper function undefined at "mapsAssociation"!'
      );
    }
    const associatedDataOrError = customMapFn(associationRows, otherParams);

    if (associatedDataOrError.isFailure) {
      this.handleFailure(associatedDataOrError.error, associationName);
    }
    return associatedDataOrError.object;*/
  }

  private _handleFailure(error: Error, key: string) {
    if (error instanceof ValidationError) {
      this.handleInvalidFieldError(key, error);
    } else {
      this.handleRequiredFieldError(key, error);
    }
  }
}
