import Joi from "joi";
import { RuleValidator } from "../../../RuleValidator";
import { Result } from "../../Result";
import { ValueObject } from "../../ValueObject";
import {
  INITIAL_PAGE_INDEX,
  INITIAL_PAGE_SIZE,
  MAX_PAGE_SIZE,
  MIN_PAGE_INDEX,
  MIN_PAGE_SIZE,
} from "./defaults";

export interface IOffsetPagingProps {
  offset: number | string | undefined;
  limit: number | string | undefined;
}

export interface IOffsetPaging {
  offset: number;
  limit: number;
}

export class OffsetPaging extends ValueObject<IOffsetPaging> {
  public static readonly LIMIT_DEFAULT_VALUE: number = INITIAL_PAGE_SIZE;
  public static readonly LIMIT_MINIMAL_VALUE: number = MIN_PAGE_SIZE;
  public static readonly LIMIT_MAXIMAL_VALUE: number = MAX_PAGE_SIZE;

  public static readonly OFFSET_DEFAULT_VALUE: number = INITIAL_PAGE_INDEX;
  public static readonly OFFSET_MINIMAL_VALUE: number = MIN_PAGE_INDEX;
  public static readonly OFFSET_MAXIMAL_VALUE: number = Number.MAX_SAFE_INTEGER;

  public static createWithMaxLimit(): Result<OffsetPaging> {
    return OffsetPaging.create({
      offset: OffsetPaging.OFFSET_DEFAULT_VALUE,
      limit: OffsetPaging.LIMIT_MAXIMAL_VALUE,
    });
  }

  public static createWithDefaultValues(): Result<OffsetPaging> {
    return OffsetPaging.create({
      offset: OffsetPaging.OFFSET_DEFAULT_VALUE,
      limit: OffsetPaging.LIMIT_DEFAULT_VALUE,
    });
  }

  public static create(paginationProps: IOffsetPagingProps) {
    const offset = paginationProps.offset || this.OFFSET_DEFAULT_VALUE;
    const limit = paginationProps.limit || this.LIMIT_DEFAULT_VALUE;

    const validatedProps = this.validate(offset, limit);

    if (validatedProps.isFailure) {
      return Result.fail(validatedProps.error);
    }

    const paging: IOffsetPaging = validatedProps.object as IOffsetPaging;

    return Result.ok<OffsetPaging>(new OffsetPaging(paging));
  }

  private static validate(offset: string | number, limit: string | number) {
    const numberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, offset);

    if (numberOrError.isFailure) {
      return numberOrError;
    }

    const _offset = typeof offset === "string" ? parseInt(offset, 10) : offset;

    const offsetValidate = RuleValidator.validate(
      Joi.number().min(OffsetPaging.OFFSET_MINIMAL_VALUE).max(OffsetPaging.OFFSET_MAXIMAL_VALUE),
      offset
    );

    if (offsetValidate.isFailure) {
      return Result.fail(
        new Error(`Page need to be larger than or equal to ${OffsetPaging.OFFSET_MINIMAL_VALUE}.`)
      );
    }

    // limit
    const limitNumberOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_NUMBER, limit);

    if (limitNumberOrError.isFailure) {
      return limitNumberOrError;
    }

    const _limit = typeof limit === "string" ? parseInt(limit, 10) : limit;

    const limitValidate = RuleValidator.validate(
      Joi.number().min(0).max(OffsetPaging.LIMIT_MAXIMAL_VALUE),
      offset
    );

    if (limitValidate.isFailure) {
      return Result.fail(
        new Error(`Page size need to be smaller than ${OffsetPaging.LIMIT_MAXIMAL_VALUE}`)
      );
    }

    return Result.ok({
      offset: _offset,
      limit: _limit,
    });
  }

  private constructor(props: IOffsetPaging) {
    super(props);
  }

  get offset(): number {
    return this.props.offset;
  }

  get limit(): number {
    return this.props.limit;
  }

  public next(): OffsetPaging {
    return new OffsetPaging({
      offset: this.props.offset + this.props.limit,
      limit: this.props.limit,
    });
  }

  public toJSON(): string {
    return JSON.stringify(this.toObject());
  }

  public toPrimitive(): string {
    throw new Error("NOT IMPLEMENT OffsetPaging.toPrimitive()");
  }

  public toObject(): Record<string, any> {
    return {
      limit: this.props.limit,
      offset: this.props.offset,
    };
  }
}
