import { type IUser } from '@api-client/core/models/store/User.js'
import type { InvitationSchema } from '@api-client/core/models/store/Invitation.js'
import type { ContextListOptions, ContextListResult } from '@api-client/core/events/BaseEvents.js'
import { Model } from '../core/Model.js'
import { exponentialBackoffDelay, type RetryOptions, shouldRetryApiError, retry } from './retry_logic.js'
import { fromError } from '../exceptions/builder.js'
import type SdkService from '../services/SdkService.js'
import { LiveData } from '../reactive/LiveData.js'

enum ModelState {
  /**
   * Not doing anything.
   */
  idle,
  /**
   * Currently querying for file data.
   */
  reading,
}

export type OrgUser = InvitationSchema | IUser

interface PageInfo {
  cursor?: string
  ended: boolean
}

export default class UsersModel extends Model<LiveData<OrgUser[]>> {
  /**
   * The current state of the model.
   */
  state: ModelState = ModelState.idle
  /**
   * The organization ID this model is associated with.
   */
  oid: string
  /**
   * A reference to the SDK service used to perform API calls.
   */
  sdk: SdkService

  /**
   * Pagination information for invitations.
   */
  invitationsInfo: PageInfo

  /**
   * Pagination information for users.
   */
  usersInfo: PageInfo

  get loading(): boolean {
    return this.state === ModelState.reading
  }

  override get value(): LiveData<OrgUser[]> {
    return this.raw as LiveData<OrgUser[]>
  }

  constructor(oid: string, sdk: SdkService) {
    super()
    this.oid = oid
    this.sdk = sdk
    this.invitationsInfo = { ended: false }
    this.usersInfo = { ended: false }
    this.raw = new LiveData<OrgUser[]>([])
  }

  /**
   * Lists users in the organization.
   * It performs a paginated query to the API.
   * Call this method again to fetch the next page of results.
   */
  async list(): Promise<void> {
    if (this.state === ModelState.idle) {
      this.debounce(() => this.#fetchPage())
    }
    await this.taskComplete
  }

  /**
   * Checks if the model has any users.
   * @returns True if there are users, false otherwise.
   */
  hasUsers(): boolean {
    return this.value.value.length > 0
  }

  /**
   * Finds a user by their key.
   */
  find(key: string): OrgUser | undefined {
    return this.value.value.find((user) => user.key === key)
  }

  /**
   * Adds a user to the model.
   * @param user The user to add to the model.
   */
  add(user: OrgUser): void {
    if (this.find(user.key)) {
      throw new Error(`User with key ${user.key} already exists.`)
    }
    this.value.value.push(user)
  }

  /**
   * Replaces an existing user in the model.
   * If the user does not exist, it will be added.
   * @param key The key of the user to remove.
   */
  replace(user: OrgUser): void {
    const index = this.value.value.findIndex((u) => u.key === user.key)
    if (index === -1) {
      return this.add(user)
    }
    this.value.value[index] = user
  }

  /**
   * Removes a user from the model.
   * @param key The key of the user to remove.
   */
  remove(key: string): void {
    const index = this.value.value.findIndex((user) => user.key === key)
    if (index === -1) {
      return
    }
    this.value.value.splice(index, 1)
  }

  async #fetchPage(): Promise<void> {
    if (this.invitationsInfo.ended && this.usersInfo.ended) {
      return
    }
    this.state = ModelState.reading
    try {
      await this.#fetchUsers()
      await this.#fetchInvitations()
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }

  async #fetchUsers(): Promise<void> {
    const operation = async (): Promise<ContextListResult<IUser>> => {
      const options: ContextListOptions = {
        limit: this.limit,
        sort: 'name',
      }
      if (this.usersInfo.cursor) {
        options.cursor = this.usersInfo.cursor
      }
      return this.sdk.organizations.users.list(this.oid, options)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    const users = await retry(operation, retryOptions)
    this.value.set(this.value.value.concat(users.items))
    this.usersInfo.cursor = users.cursor
    if (users.items.length < this.limit) {
      this.usersInfo.ended = true
    }
  }

  async #fetchInvitations(): Promise<void> {
    const operation = async (): Promise<ContextListResult<InvitationSchema>> => {
      const options: ContextListOptions = {
        limit: this.limit,
      }
      if (this.usersInfo.cursor) {
        options.cursor = this.usersInfo.cursor
      }
      return this.sdk.organizations.invitations.list(this.oid)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    const invitations = await retry(operation, retryOptions)
    this.value.set(this.value.value.concat(invitations.items))
    this.usersInfo.cursor = invitations.cursor
    if (invitations.items.length < this.limit) {
      this.usersInfo.ended = true
    }
  }

  async deactivate(uid: string): Promise<void> {
    const { oid } = this
    if (!uid) {
      throw new Error('User ID (uid) is not set. Cannot deactivate user.')
    }
    this.state = ModelState.reading
    const operation = async (): Promise<IUser> => {
      return this.sdk.organizations.users.deactivate(oid, uid)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    try {
      const updated = await retry(operation, retryOptions)
      this.replace(updated)
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }

  async activate(uid: string): Promise<void> {
    const { oid } = this
    if (!uid) {
      throw new Error('User ID (uid) is not set. Cannot activate user.')
    }
    this.state = ModelState.reading
    const operation = async (): Promise<IUser> => {
      return this.sdk.organizations.users.activate(oid, uid)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    try {
      const updated = await retry(operation, retryOptions)
      this.replace(updated)
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }

  async deleteUser(uid: string): Promise<void> {
    const { oid } = this
    if (!uid) {
      throw new Error('User ID (uid) is not set. Cannot delete user.')
    }
    this.state = ModelState.reading
    const operation = async (): Promise<void> => {
      return this.sdk.organizations.users.delete(oid, uid)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    try {
      await retry(operation, retryOptions)
      this.remove(uid)
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }

  async deleteInvitation(key: string): Promise<void> {
    const { oid } = this
    if (!key) {
      throw new Error('Invitation key is not set. Cannot delete invitation.')
    }
    this.state = ModelState.reading
    const operation = async (): Promise<void> => {
      await this.sdk.organizations.invitations.delete(oid, key)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    try {
      await retry(operation, retryOptions)
      this.remove(key)
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }

  async resendInvitation(key: string): Promise<void> {
    const { oid } = this
    if (!key) {
      throw new Error('Invitation key is not set. Cannot resend invitation.')
    }
    this.state = ModelState.reading
    const operation = async (): Promise<InvitationSchema> => {
      return this.sdk.organizations.invitations.resend(oid, key)
    }
    const retryOptions: RetryOptions = {
      retries: this.retryCount,
      delayMs: exponentialBackoffDelay,
      shouldRetry: shouldRetryApiError as (error: Error) => boolean,
    }
    try {
      const updated = await retry(operation, retryOptions)
      this.replace(updated)
    } catch (e) {
      const err = fromError(e as Error)
      this.notifyError(err)
      throw err
    } finally {
      this.state = ModelState.idle
    }
  }
}
