import * as $core from './core';
import * as $error from './error';
const MAX_DELAY_TIME = 120 * 1000;
const MIN_DELAY_TIME = 100;
export class BackoffPolicy {
  policy: string;
  constructor(option: { [key: string]: any }) {
    this.policy = option.policy;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    throw Error('un-implement');
  }

  static newBackoffPolicy(option: { [key: string]: any }): BackoffPolicy {
    switch (option.policy) {
      case 'Fixed':
        return new FixedBackoffPolicy(option);
      case 'Random':
        return new RandomBackoffPolicy(option);
      case 'Exponential':
        return new ExponentialBackoffPolicy(option);
      case 'EqualJitter':
      case 'ExponentialWithEqualJitter':
        return new EqualJitterBackoffPolicy(option);
      case 'FullJitter':
      case 'ExponentialWithFullJitter':
        return new FullJitterBackoffPolicy(option);
    }
  }
}


class FixedBackoffPolicy extends BackoffPolicy {
  period: number;
  constructor(option: { [key: string]: any }) {
    super(option);
    this.period = option.period;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    return this.period;
  }
}

class RandomBackoffPolicy extends BackoffPolicy {
  period: number;
  cap: number;
  constructor(option: { [key: string]: any }) {
    super(option);
    this.period = option.period;
    this.cap = option.cap || 20 * 1000;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    const randomTime = Math.floor(Math.random() * (ctx.retriesAttempted * this.period));
    if (randomTime > this.cap) {
      return this.cap;
    }
    return randomTime;
  }
}

class ExponentialBackoffPolicy extends BackoffPolicy {
  period: number;
  cap: number;
  constructor(option: { [key: string]: any }) {
    super(option);
    this.period = option.period;
    //default value: 3 days
    this.cap = option.cap || 3 * 24 * 60 * 60 * 1000;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    const randomTime = Math.pow(2, ctx.retriesAttempted * this.period);
    if (randomTime > this.cap) {
      return this.cap;
    }
    return randomTime;
  }
}

class EqualJitterBackoffPolicy extends BackoffPolicy {
  period: number;
  cap: number;
  constructor(option: { [key: string]: any }) {
    super(option);
    this.period = option.period;
    //default value: 3 days
    this.cap = option.cap || 3 * 24 * 60 * 60 * 1000;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    const ceil = Math.min(this.cap, Math.pow(2, ctx.retriesAttempted * this.period));
    return ceil / 2 + Math.floor(Math.random() * (ceil / 2 + 1));
  }
}

class FullJitterBackoffPolicy extends BackoffPolicy {
  period: number;
  cap: number;
  constructor(option: { [key: string]: any }) {
    super(option);
    this.period = option.period;
    //default value: 3 days
    this.cap = option.cap || 3 * 24 * 60 * 60 * 1000;
  }

  getDelayTime(ctx: RetryPolicyContext): number {
    const ceil = Math.min(this.cap, Math.pow(2, ctx.retriesAttempted * this.period));
    return Math.floor(Math.random() * ceil);
  }
}


export class RetryCondition {
  maxAttempts: number;
  backoff: BackoffPolicy;
  exception: string[];
  errorCode: string[];
  maxDelay: number;
  constructor(condition: { [key: string]: any }) {
    this.maxAttempts = condition.maxAttempts;
    this.backoff = condition.backoff && BackoffPolicy.newBackoffPolicy(condition.backoff);
    this.exception = condition.exception;
    this.errorCode = condition.errorCode;
    this.maxDelay = condition.maxDelay;
  }
}


export class RetryOptions {
  retryable: boolean;
  retryCondition: RetryCondition[];
  noRetryCondition: RetryCondition[];
  constructor(options: { [key: string]: any }) {
    this.retryable = options.retryable;
    this.retryCondition = (options.retryCondition || []).map((condition: { [key: string]: any; }) => {
      return new RetryCondition(condition);
    });

    this.noRetryCondition = (options.noRetryCondition || []).map((condition: { [key: string]: any; }) => {
      return new RetryCondition(condition);
    });
  }
}

export class RetryPolicyContext {
  key: string;
  retriesAttempted: number;
  httpRequest: $core.Request;
  httpResponse: $core.Response;
  exception: $error.ResponseError | $error.BaseError;
  constructor(options: { [key: string]: any }) {
    this.key = options.key;
    this.retriesAttempted = options.retriesAttempted || 0;
    this.httpRequest = options.httpRequest || null;
    this.httpResponse = options.httpResponse || null;
    this.exception = options.exception || null;
  }
}

export function shouldRetry(options: RetryOptions, ctx: RetryPolicyContext): boolean {
  if (ctx.retriesAttempted === 0) {
    return true;
  }
  if (!options || !options.retryable) {
    return false;
  }
  const retriesAttempted = ctx.retriesAttempted;
  const ex = ctx.exception;
  let conditions = options.noRetryCondition;
  for (let i = 0; i < conditions.length; i++) {
    const condition = conditions[i];
    if (condition.exception.includes(ex.name) || condition.errorCode.includes(ex.code)) {
      return false;
    }
  }
  conditions = options.retryCondition;
  for (let i = 0; i < conditions.length; i++) {
    const condition = conditions[i];
    if (!condition.exception.includes(ex.name) && !condition.errorCode.includes(ex.code)) {
      continue;
    }
    if (retriesAttempted >= condition.maxAttempts) {
      return false;
    }
    return true;
  }
  return false;
}

export function getBackoffDelay(options: RetryOptions, ctx: RetryPolicyContext): number {
  const ex = ctx.exception;
  const conditions = options.retryCondition;
  for (let i = 0; i < conditions.length; i++) {
    const condition = conditions[i];
    if (!condition.exception.includes(ex.name) && !condition.errorCode.includes(ex.code)) {
      continue;
    }
    const maxDelay = condition.maxDelay || MAX_DELAY_TIME;
    const retryAfter = (ctx.exception as $error.ResponseError).retryAfter;
    if (retryAfter !== undefined) {
      return Math.min(retryAfter, maxDelay);
    }

    if (!condition.backoff) {
      return MIN_DELAY_TIME;
    }
    return Math.min(condition.backoff.getDelayTime(ctx), maxDelay);
  }
  return MIN_DELAY_TIME;
}