import type { Activity } from '../core/Activity.js'
import type { Fragment } from '../core/Fragment.js'
import { type UiDialogClosingReason } from '../md/dialog/internals/Dialog.js'
import { UiDialogElement } from '../md/dialog/ui-dialog.js'
import { bound } from '../decorators/bound.js'

import '../md/dialog/ui-dialog.js'

export type RenderAs = 'message' | 'error' | 'picker'

export interface ActionOptions {
  /**
   * The label to show on the action button.
   * Default labels are `OK` for the positive action and `Cancel` for the negative action.
   */
  label?: string

  /**
   * When set it sets the value on the action button.
   */
  value?: string

  /**
   * The action to call when the action is triggered.
   * @param value Optional value returned by the dialog.
   */
  action?: (value?: unknown) => void
}

export interface PositiveActionOptions extends ActionOptions {
  /**
   * The type of the button. Default is `submit`.
   * If set to `button`, the dialog will not close on click.
   */
  type?: 'submit' | 'button'
}

/**
 * Options for unsaved changes detection
 */
export interface UnsavedChangesOptions {
  /**
   * Function that returns true if there are unsaved changes
   */
  hasUnsavedChanges: () => boolean
  /**
   * Title for the unsaved changes warning dialog
   */
  warningTitle?: string
  /**
   * Message for the unsaved changes warning dialog
   */
  warningMessage?: string
  /**
   * Label for the "Discard Changes" button
   */
  discardLabel?: string
  /**
   * Label for the "Keep Editing" button
   */
  keepEditingLabel?: string
}

/**
 * A programmatic access to the dialog UI.
 * It hook ups to the `Activity` lifecycle methods to control the dialog.
 * When the activity is destroyed, the dialog is destroyed along with it.
 * When the activity is paused, the dialog is hidden, but not removed from the DOM.
 * The dialog then becomes visible again when the activity resumes.
 */
export class DialogBuilder {
  /**
   * The title of the dialog.
   */
  static title: string | undefined
  /**
   * The title of the dialog.
   * When set, it takes precedence over the static title.
   */
  protected title: string | undefined
  /**
   * The description of the dialog.
   * It is not used by default, but can be set to add custom content.
   */
  protected description: string | undefined
  /**
   * The positive action that closes the dialog.
   * It is not used by default, but can be set to add custom actions.
   */
  protected positiveAction: PositiveActionOptions | undefined
  /**
   * The negative action that closes the dialog.
   * It is not used by default, but can be set to add custom actions.
   */
  protected negativeAction: ActionOptions | undefined
  /**
   * The body of the dialog. It is not used by default, but can be set to add custom content.
   */
  protected body: HTMLElement | undefined
  /**
   * The CSS classes to add to the dialog element.
   * It is not used by default, but can be set to add custom classes.
   */
  protected classes: string[] | undefined
  /**
   * When rendered, it sets the corresponding class name to the dialog.
   */
  protected renderAs: RenderAs = 'message'

  /**
   * The root element to render the dialog into.
   * By default the dialog is rendered into the document body.
   */
  protected renderRoot?: HTMLElement | undefined

  /**
   * When set to true, the dialog will be rendered as a modal dialog.
   */
  protected modal: boolean | undefined

  /**
   * The dialog element that is created by this builder.
   */
  dialog?: UiDialogElement
  /**
   * Options for unsaved changes detection
   */
  protected unsavedChangesOptions?: UnsavedChangesOptions
  /**
   * Reference to the unsaved changes warning dialog
   */
  unsavedChangesDialog?: DialogBuilder

  /**
   * Creates a new dialog builder.
   * @param parent The parent activity or fragment that the dialog is associated with.
   * It will listen to the activity lifecycle events to control the dialog visibility.
   * When not provided, the dialog will not be controlled by the activity lifecycle.
   */
  constructor(private parent?: Activity | Fragment | undefined) {
    parent?.addEventListener('activity:destroy', this.handleActivityDestroy)
    parent?.addEventListener('activity:pause', this.handleActivityPause)
    parent?.addEventListener('activity:resume', this.handleActivityResume)
    parent?.addEventListener('fragment:destroy', this.handleActivityDestroy)
    parent?.addEventListener('fragment:pause', this.handleActivityPause)
    parent?.addEventListener('fragment:resume', this.handleActivityResume)
  }

  /**
   * Sets the dialog title.
   * @param title The title of the dialog to set. Undefined/empty to clear.
   * @returns The instance of this class for chaining.
   */
  setTitle(title?: string): this {
    this.title = title
    return this
  }

  /**
   * Sets the dialog description.
   * @param title The value to to set. Undefined/empty to clear.
   * @returns The instance of this class for chaining.
   */
  setDescription(description: string): this {
    this.description = description
    return this
  }

  /**
   * Sets the positive action that closes the dialog.
   *
   * @param action The action to call when the positive action is triggered.
   * @param label The label of the positive button. Default to "OK"
   * @returns The instance of this class for chaining.
   */
  setPositiveAction(options?: PositiveActionOptions): this {
    this.positiveAction = options
    return this
  }

  /**
   * Sets the negative action that closes the dialog.
   *
   * @param action The action to call when the negative action is triggered.
   * @param label The label of the negative button. Default to "Cancel"
   * @returns The instance of this class for chaining.
   */
  setNegativeAction(options?: ActionOptions): this {
    this.negativeAction = options
    return this
  }

  /**
   * By default, the dialog is rendered as a message dialog. Set to a different type to
   * change the rendering.
   *
   * @param renderAs The value for the type of the dialog.
   * @returns The instance of this class for chaining.
   */
  setRenderAs(renderAs: RenderAs): this {
    this.renderAs = renderAs
    return this
  }

  /**
   * Sets up unsaved changes detection for the dialog.
   * When the user tries to close the dialog with unsaved changes, a warning dialog will be shown.
   * @param options Configuration for unsaved changes detection
   * @returns The instance of this class for chaining.
   */
  setUnsavedChangesDetection(options: UnsavedChangesOptions): this {
    this.unsavedChangesOptions = options
    return this
  }

  /**
   * Adds custom CSS classes to the dialog element.
   * These classes will be added to the dialog element when it is created.
   *
   * @param classes The CSS classes to add to the dialog element.
   * @returns The instance of this class for chaining.
   */
  setClasses(classes: string[]): this {
    this.classes = classes
    return this
  }

  /**
   * Sets the body of the dialog. It is not used by default, but can be set to add custom content.
   * The body will be added to the dialog element.
   *
   * @param body The body element to set.
   * @returns The instance of this class for chaining.
   */
  setBody(body: HTMLElement): this {
    this.body = body
    return this
  }

  /**
   * Sets the root element to render the dialog into.
   * By default the dialog is rendered into the document body.
   *
   * @param renderRoot The root element to render the dialog into.
   * @returns The instance of this class for chaining.
   */
  setRenderRoot(renderRoot: HTMLElement): this {
    this.renderRoot = renderRoot
    return this
  }

  /**
   * Sets the dialog to be modal.
   * When set to true, the dialog will be rendered as a modal dialog.
   * The dialog will block interaction with the rest of the page until it is closed.
   *
   * @param modal When true, the dialog will be rendered as a modal dialog.
   * @returns The instance of this class for chaining.
   */
  setModal(modal: boolean): this {
    this.modal = modal
    return this
  }

  /**
   * Creates the dialog and returns the instance of it.
   * The dialog is not yet added to the DOM nor it is opened.
   *
   * @returns The instance of the dialog class.
   */
  build(): UiDialogElement {
    const dialog = new UiDialogElement()
    dialog.classList.add('activity-dialog')
    dialog.classList.add(this.renderAs)
    if (this.classes && this.classes.length > 0) {
      dialog.classList.add(...this.classes)
    }
    if (this.parent) {
      dialog.classList.add(this.parent.constructor.name.toLocaleLowerCase())
    }
    if (this.title) {
      const title = document.createElement('span')
      title.slot = 'title'
      title.classList.add('dialog-title')
      title.textContent = this.title
      dialog.appendChild(title)
    }
    if (this.description) {
      const description = document.createElement('div')
      description.classList.add('dialog-description')
      description.textContent = this.description
      dialog.appendChild(description)
    }
    if (this.body) {
      dialog.appendChild(this.body)
    }
    if (this.negativeAction) {
      const { label = 'Cancel' } = this.negativeAction
      const cancelButton = document.createElement('ui-button')
      cancelButton.slot = 'button'
      cancelButton.setAttribute('color', 'text')
      cancelButton.value = 'dismiss'
      cancelButton.textContent = label
      dialog.appendChild(cancelButton)
    }
    if (this.positiveAction) {
      const { label = 'OK', value, type } = this.positiveAction
      const confirmButton = document.createElement('ui-button')
      confirmButton.slot = 'button'
      confirmButton.setAttribute('color', 'text')
      if (value) {
        confirmButton.value = value
      }
      if (type) {
        confirmButton.type = type
      }
      confirmButton.textContent = label
      dialog.appendChild(confirmButton)
    }
    // Add both closing and close event listeners
    dialog.addEventListener('closing', this.handleDialogClosing)
    dialog.addEventListener('close', this.handleDialogClosed)

    return dialog
  }

  /**
   * Creates the dialog by calling the `build()` function,
   * sets the `dialog` property, appends it to the body,
   * and finally opens the dialog as modal.
   *
   * This method does not return `this` as you can't manipulate the dialog anymore.
   */
  open(): void {
    if (!this.dialog) {
      this.dialog = this.build()
      const parent = this.renderRoot ?? document.body
      parent.appendChild(this.dialog)
    }
    if (this.modal) {
      this.dialog.modal = true
    }
    this.dialog.open = true
  }

  /**
   * Closes the dialog. The dialog behaves like the a negative action was triggered.
   */
  close(): void {
    if (this.dialog) {
      this.dialog.open = false
    }
  }

  @bound private handleActivityDestroy(): void {
    this.dispose()
  }

  @bound private handleActivityPause(): void {
    this.dialog?.classList.add('hidden')
  }

  @bound private handleActivityResume(): void {
    this.dialog?.classList.remove('hidden')
  }

  /**
   * Handles the cancelable 'closing' event to check for unsaved changes
   */
  @bound private handleDialogClosing(event: Event): void {
    // If no unsaved changes detection is set up, allow closing
    if (!this.unsavedChangesOptions) {
      return
    }

    // Check if there are unsaved changes
    if (!this.unsavedChangesOptions.hasUnsavedChanges()) {
      return
    }

    // Prevent the dialog from closing
    event.preventDefault()

    // Show unsaved changes warning dialog
    this.showUnsavedChangesWarning()
  }

  /**
   * Shows a warning dialog when there are unsaved changes
   */
  showUnsavedChangesWarning(): void {
    const options = this.unsavedChangesOptions
    if (!options) {
      return
    }

    this.unsavedChangesDialog = new DialogBuilder(this.parent)
      .setTitle(options.warningTitle || 'Unsaved Changes')
      .setDescription(options.warningMessage || 'You have unsaved changes. Are you sure you want to discard them?')
      .setRenderAs('error')
      .setModal(this.modal || false)
      .setPositiveAction({
        label: options.discardLabel || 'Discard Changes',
        value: 'confirm',
        action: () => {
          // Close the warning dialog
          this.unsavedChangesDialog?.dispose()
          this.unsavedChangesDialog = undefined

          // Force close the main dialog without checking for unsaved changes
          this.forceClose()
        },
      })
      .setNegativeAction({
        label: options.keepEditingLabel || 'Keep Editing',
        value: 'dismiss',
        action: () => {
          // Just close the warning dialog, keep the main dialog open
          this.unsavedChangesDialog?.dispose()
          this.unsavedChangesDialog = undefined
        },
      })

    this.unsavedChangesDialog.open()
  }

  /**
   * Forces the dialog to close without checking for unsaved changes
   */
  protected forceClose(): void {
    if (this.dialog) {
      // Temporarily remove the closing event listener to avoid the check
      this.dialog.removeEventListener('closing', this.handleDialogClosing)

      // Close the dialog
      this.dialog.open = false

      // Re-add the closing event listener for future use
      this.dialog.addEventListener('closing', this.handleDialogClosing)
    }
  }

  @bound protected handleDialogClosed(event: Event): void {
    const { cancelled, value } = (event as CustomEvent<UiDialogClosingReason>).detail
    if (cancelled && this.negativeAction && this.negativeAction.action) {
      this.negativeAction.action(value)
    } else if (!cancelled && this.positiveAction && this.positiveAction.action) {
      this.positiveAction.action(value)
    }
    this.dispose()
  }

  /**
   * Removes the dialog from the DOM and removes all listeners.
   * The dialog does not call any lifecycle methods.
   */
  dispose(): void {
    this.dialog?.removeEventListener('closing', this.handleDialogClosing)
    this.dialog?.removeEventListener('close', this.handleDialogClosed)
    this.dialog?.remove()
    this.dialog = undefined

    // Clean up unsaved changes warning dialog if it exists
    this.unsavedChangesDialog?.dispose()
    this.unsavedChangesDialog = undefined

    this.parent?.removeEventListener('activity:destroy', this.handleActivityDestroy)
    this.parent?.removeEventListener('activity:pause', this.handleActivityPause)
    this.parent?.removeEventListener('activity:resume', this.handleActivityResume)
    this.parent?.removeEventListener('fragment:destroy', this.handleActivityDestroy)
    this.parent?.removeEventListener('fragment:pause', this.handleActivityPause)
    this.parent?.removeEventListener('fragment:resume', this.handleActivityResume)
  }
}
