/* eslint-disable @typescript-eslint/no-unused-vars */

import type { Exception } from '@api-client/core/exceptions/exception.js'

/**
 * Defines the options for the retry mechanism.
 */
export interface RetryOptions {
  /**
   * The maximum number of retry attempts.
   * Defaults to 3.
   */
  retries?: number
  /**
   * The delay in milliseconds before the next retry.
   * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
   * Defaults to 1000ms.
   */
  delayMs?: number | ((attempt: number) => number)
  /**
   * An optional function that determines if a retry should be attempted based on the error.
   * If not provided, all errors will trigger a retry up to the maximum number of retries.
   * @param error The error that occurred.
   * @returns True if a retry should be attempted, false otherwise.
   */
  shouldRetry?: (error: Error) => boolean
}

/**
 * Calculates an exponential backoff delay for retry attempts.
 * The delay doubles with each attempt.
 *
 * @param attempt The current retry attempt number (should be 1-indexed).
 * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
 * @returns The calculated delay in milliseconds.
 */
export function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {
  if (attempt <= 0) return initialDelay
  // For attempt 1, delay is initialDelay * 2^0 = initialDelay
  return initialDelay * Math.pow(2, attempt - 1)
}
/**
 * Checks if the API error should be retried based on its status code.
 * If the status code is 404, it returns false (do not retry).
 * For other status codes, it returns true (retry).
 * @param error The error to check.
 */
export function shouldRetryApiError(error: Exception): boolean {
  const { status } = error
  if (status === 404) {
    // Do not retry on 404 errors
    return false
  }
  return true
}
/**
 * The base class for models.
 *
 * Models wrap a logic related to manipulating model data, querying for data from the API,
 * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.
 *
 * The application should use these models in order to centralize object manipulation.
 *
 * @template T The underlying domain model.
 */
export abstract class Model<T> extends EventTarget {
  /**
   * The underlying raw object.
   * Do not modify properties of the object directly.
   * Use one of the methods provided by the model.
   * Direct changes will not be reflected in the UI
   * and may cause issues when processing the object.
   */
  raw?: T

  /**
   * The timeout for the query debouncer.
   * When any property change this is the time the element will wait
   * until the actual query is made.
   */
  debounceTimeout = 100

  /**
   * A reference to the current debouncer.
   */
  debouncerValue?: number | NodeJS.Timeout

  /**
   * Default limit for list queries.
   */
  limit = 30

  /**
   * A flag that helps to determine whether the `taskComplete` is setup.
   */
  #hasPendingTaskPromise = false

  /**
   * A hidden value for the `taskComplete` getter.
   */
  #taskComplete?: Promise<void>

  /**
   * The resolver to call when the debounced task completes.
   */
  #taskResolver?: () => void

  /**
   * @type A promise resolved when the debounced task finished.
   */
  get taskComplete(): Promise<void> | undefined {
    return this.#taskComplete
  }

  /**
   * A getter for the raw value.
   */
  get value(): T | undefined {
    return this.raw
  }

  /**
   * @param obj The source object to use.
   */
  constructor(obj?: T) {
    super()
    this.raw = obj
    this.#setUpdatePromise()
  }

  /**
   * Creates a new instance of the data. It often invokes API call.
   * This is what happens after the user triggers "create new" flow.
   *
   * After this method is calls, the `raw` object is set.
   *
   * @param data The partial data to create.
   * @returns The created object.
   */
  create(data: Partial<T>): Promise<T> {
    throw new Error(`Not implemented.`)
  }

  debounce(callback: (...args: unknown[]) => unknown): void {
    if (this.debouncerValue) {
      clearTimeout(this.debouncerValue)
    }
    if (!this.#hasPendingTaskPromise) {
      this.#setUpdatePromise()
    }
    this.debouncerValue = setTimeout(async () => {
      this.debouncerValue = undefined
      try {
        await callback.call(this)
      } catch (e) {
        const err = e as Error
        this.notifyError(err)
        throw err
      } finally {
        this.resolveTaskPromise()
      }
    }, this.debounceTimeout)
  }

  notifyError(error: Error): void {
    this.dispatchEvent(
      new CustomEvent<Error>('error', {
        detail: error,
      })
    )
  }

  #setUpdatePromise(): void {
    this.#taskComplete = new Promise<void>((resolve) => {
      this.#taskResolver = resolve
      this.#hasPendingTaskPromise = true
    })
  }

  protected resolveTaskPromise(): void {
    if (!this.#hasPendingTaskPromise) {
      return
    }
    this.#hasPendingTaskPromise = false
    const resolver = this.#taskResolver
    if (resolver) {
      resolver()
    }
  }

  /**
   * Executes an asynchronous operation with a retry mechanism.
   * @param operation A function that returns a Promise to be executed.
   * @param options Optional retry configurations.
   * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
   * @example
   * ```typescript
   * override async create(data: Partial<IUser>): Promise<IUser> {
   *   const operation = async (): Promise<UserData> => {
   *     // ... perform API call to create user
   *   }
   *   const retryOptions: RetryOptions = {
   *     retries: 5,
   *     delayMs: exponentialBackoffDelay,
   *     shouldRetry: shouldRetryApiError,
   *   }
   *   this.raw = await this.retry(operation)
   *   this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
   *   return this.raw;
   * }
   * ```
   */
  protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {
    const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options

    let lastError: Error | undefined

    for (let attempt = 0; attempt < retries; attempt++) {
      try {
        return await operation()
      } catch (e) {
        lastError = e as Error
        if (!shouldRetry(lastError) || attempt === retries - 1) {
          // Do not retry if shouldRetry returns false or if it's the last attempt
          this.notifyError(lastError)
          throw lastError
        }

        const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs
        if (delay > 0) {
          await new Promise((resolve) => setTimeout(resolve, delay))
        }
      }
    }
    // This part should ideally not be reached if retries > 0,
    // as the loop would throw on the last attempt.
    // However, to satisfy TypeScript and guard against retries = 0:
    if (lastError) {
      this.notifyError(lastError)
      throw lastError
    }
    // Should not happen if operation is called at least once.
    // Adding a generic error for completeness.
    const err = new Error('Retry operation failed without a specific error.')
    this.notifyError(err)
    throw err
  }
}
