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

export interface IFirebirdMapper<
  TModel extends FirebirdModel,
  TEntity extends Entity<any>,
> {
  mapToDomain(source: TModel, params?: Record<string, any>): TEntity;

  mapArrayToDomain(
    source: TModel[],
    params?: Record<string, any>
  ): Collection<TEntity>;

  mapArrayAndCountToDomain(
    source: TModel[],
    totalCount: number,
    params?: Record<string, any>
  ): Collection<TEntity>;
}

export abstract class FirebirdMapper<
  TModel extends FirebirdModel = any,
  TModelAttributes = any,
  TEntity extends Entity<any> = any,
> implements IFirebirdMapper<TModel, TEntity>
{
  public constructor(protected props: any) {}

  public mapToDomain(source: TModel, params?: Record<string, any>): TEntity {
    return this.toDomainMappingImpl(source, params);
  }

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

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

  protected toDomainMappingImpl(
    source: TModel,
    params?: Record<string, any>
  ): TEntity {
    throw InfrastructureError.create(
      InfrastructureError.UNEXCEPTED_ERROR,
      'Method "toDomainMappingImpl" 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: Record<string, any>
    ) => Result<any, Error>,
    params: Record<string, any> = {
      defaultValue: null,
    }
  ) {
    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: Record<string, any> = {}
  ) {
    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;

    //const associatedData = row[association.accessors.get]();
    //return associatedData;
  }

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