import { nanoid } from '../../nanoid.js'
import { InvitationKind } from '../kinds.js'
import type { IDeletion } from './Deletion.js'
import type { UserOrganizationGrantType } from './Organization.js'
import type { Operation } from '@api-client/json/types.js'
import { Builder } from '@api-client/json/patch/builder.js'

/**
 * The status of an invitation.
 * - `pending`: The invitation has been sent but not yet responded to.
 * - `accepted`: The invitation has been accepted by the user.
 * - `declined`: The invitation has been declined by the user.
 */
export type InvitationStatus = 'pending' | 'accepted' | 'declined'

/**
 * The schema for an invitation.
 * This defines the structure of an invitation object in the system.
 */
export interface InvitationSchema {
  /**
   * The kind of the invitation.
   */
  kind: typeof InvitationKind
  /**
   * The database ID of the invitation.
   */
  key: string
  /**
   * The user ID of the user who sent the invitation.
   */
  uid: string
  /**
   * The organization ID to which the invitation belongs.
   */
  oid: string
  /**
   * The email address of the user to whom the invitation is sent.
   */
  email: string
  /**
   * The name of the user to whom the invitation is sent.
   * This is optional and can be empty if the user is not known.
   */
  name?: string
  /**
   * The token used to respond to the invitation.
   */
  token: string
  /**
   * The timestamp when the invitation expires.
   * This is used to determine if the invitation is still valid.
   */
  expiresAt: number
  /**
   * The timestamp when the invitation was responded to.
   * This is set when the user accepts or declines the invitation.
   */
  respondedAt?: number
  /**
   * The status of the invitation.
   */
  status: InvitationStatus
  /**
   * The grant type the user will have in the organization if they accept the invitation.
   */
  grantType: UserOrganizationGrantType
  /**
   * Whether the file object is deleted.
   */
  deleted?: boolean
  /**
   * The information about the delete information.
   * Always set when the `delete` is true.
   */
  deletedInfo?: IDeletion
  /**
   * The timestamp when the invitation was created.
   */
  createdAt: number
  /**
   * The timestamp when the invitation was last updated.
   */
  updatedAt: number
  /**
   * Number of times the invitation was resent to the user.
   */
  resent: number
  /**
   * The timestamp when the invitation was last sent.
   * This is used to track when the invitation was last communicated to the user.
   */
  lastSentAt: number
}

/**
 * Represents an invitation to join an organization.
 * This class implements the `InvitationSchema` interface and provides methods to create and manipulate invitations.
 */
export class Invitation implements InvitationSchema {
  kind: typeof InvitationKind
  key: string
  uid: string
  oid: string
  email: string
  name: string | undefined
  token: string
  expiresAt: number
  respondedAt: number | undefined
  status: InvitationStatus
  grantType: UserOrganizationGrantType
  deleted: boolean
  deletedInfo: IDeletion | undefined
  createdAt: number
  updatedAt: number
  resent: number
  lastSentAt: number

  #builder?: Builder<InvitationSchema>

  /**
   * The patch builder for this invitation.
   */
  get builder(): Builder<InvitationSchema> {
    if (!this.#builder) {
      this.#builder = new Builder(this.toJSON())
    }
    return this.#builder
  }

  /**
   * Creates a full invitation schema with defaults.
   *
   * @param input The partial invitation schema.
   * @returns The invitation schema.
   */
  static createSchema(input: Partial<InvitationSchema> = {}): InvitationSchema {
    const {
      key = nanoid(),
      email,
      oid,
      grantType,
      uid = '',
      updatedAt = 0,
      createdAt = 0,
      expiresAt = 0,
      resent = 0,
      lastSentAt = 0,
      status = 'pending',
      token = '',
    } = input
    if (!email) {
      throw new Error('Email is required to create an invitation schema.')
    }
    if (!oid) {
      throw new Error('Organization ID is required to create an invitation schema.')
    }
    if (!grantType) {
      throw new Error('Grant type is required to create an invitation schema.')
    }
    const result: InvitationSchema = {
      kind: InvitationKind,
      key,
      uid,
      oid,
      email,
      grantType,
      token,
      expiresAt,
      status,
      createdAt,
      updatedAt,
      resent,
      lastSentAt,
    }
    if (input.name) {
      result.name = input.name
    }
    if (input.token) {
      result.token = input.token
    }
    if (input.expiresAt) {
      result.expiresAt = input.expiresAt
    }
    if (input.respondedAt) {
      result.respondedAt = input.respondedAt
    }
    if (input.status) {
      result.status = input.status
    }
    if (input.deleted) {
      result.deleted = input.deleted
      result.deletedInfo = input.deletedInfo
    }
    return result
  }

  constructor(input?: Partial<InvitationSchema>) {
    const init: InvitationSchema = Invitation.createSchema(input)
    this.kind = InvitationKind
    this.key = init.key
    this.uid = init.uid
    this.oid = init.oid
    this.email = init.email
    this.name = init.name
    this.token = init.token
    this.expiresAt = init.expiresAt
    this.respondedAt = init.respondedAt
    this.status = init.status
    this.grantType = init.grantType
    this.deleted = init.deleted || false
    this.deletedInfo = init.deletedInfo
    this.createdAt = init.createdAt
    this.updatedAt = init.updatedAt
    this.resent = init.resent
    this.lastSentAt = init.lastSentAt
  }

  toJSON(): InvitationSchema {
    const result: InvitationSchema = {
      kind: this.kind,
      key: this.key,
      uid: this.uid,
      oid: this.oid,
      email: this.email,
      token: this.token,
      expiresAt: this.expiresAt,
      status: this.status,
      grantType: this.grantType,
      deleted: this.deleted,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      resent: this.resent,
      lastSentAt: this.lastSentAt,
    }
    if (this.name) {
      result.name = this.name
    }
    if (this.respondedAt) {
      result.respondedAt = this.respondedAt
    }
    if (this.deletedInfo) {
      result.deletedInfo = { ...this.deletedInfo }
    }
    return result
  }

  /**
   * Retrieves the patch operations for this invitation.
   * This method builds a JSON Patch representation of the invitation,
   * which can be used to update the invitation in a database or API.
   *
   * Use one of the `set*` methods to modify the invitation before calling this method.
   *
   * @returns An array of patch operations.
   * If no changes have been made, it returns an empty array.
   */
  getPatch(): Operation[] {
    if (!this.#builder) {
      return []
    }
    return this.builder.build()
  }

  /**
   * Sets the name of the user to whom the invitation is sent.
   * If the name is the same as the current name, no operation is performed.
   *
   * @param name The name to set. If undefined, it will be removed.
   */
  setName(name?: string): void {
    if (name === this.name) {
      return
    }
    const hasName = !!this.name
    if (hasName && !name) {
      this.builder.remove('/name')
    } else {
      // The add method handles both adding and replacing
      this.builder.add('/name', name)
    }
    this.name = name
  }

  /**
   * Sets the expiration timestamp of the invitation.
   * @param expiresAt The timestamp when the invitation expires.
   * If the expiresAt is the same as the current expiresAt, no operation is performed.
   */
  setExpiresAt(expiresAt: number): void {
    if (this.expiresAt !== expiresAt) {
      this.builder.replace('/expiresAt', expiresAt)
      this.expiresAt = expiresAt
    }
  }

  /**
   * Sets the grant type of the user in the organization.
   */
  setGrantType(grantType: UserOrganizationGrantType): void {
    if (this.grantType !== grantType) {
      this.builder.replace('/grantType', grantType)
      this.grantType = grantType
    }
  }
}
