/**
 * Field - Form field wrapper combining label, content, hint, and error
 * @countermeasure/web-components/primitives/field
 */

import { type FieldConfig } from './types'
import { resolveContainer, uniqueId } from '../utils'

export type { FieldConfig } from './types'

export class Field {
  public readonly el: HTMLDivElement

  private labelEl: HTMLLabelElement | null = null
  private labelTextNode: Text | null = null
  private requiredEl: HTMLSpanElement | null = null
  private readonly contentEl: HTMLDivElement
  private readonly hintEl: HTMLDivElement
  private readonly errorEl: HTMLDivElement
  private readonly hintId: string
  private readonly errorId: string

  constructor(config: FieldConfig) {
    const fieldId = uniqueId('cmm-field')
    this.hintId = `${fieldId}-hint`
    this.errorId = `${fieldId}-error`

    // Create the wrapper element
    this.el = document.createElement('div')
    this.el.className = 'cmm-field'

    if (config.className) {
      this.el.classList.add(config.className)
    }

    // Create label if provided
    if (config.label !== undefined) {
      this.labelEl = document.createElement('label')
      this.labelEl.className = 'cmm-field__label'

      if (config.htmlFor !== undefined) {
        this.labelEl.htmlFor = config.htmlFor
      }

      this.labelTextNode = document.createTextNode(config.label)
      this.labelEl.appendChild(this.labelTextNode)

      if (config.required === true) {
        this.appendRequiredIndicator()
      }

      this.el.appendChild(this.labelEl)
    }

    // Create content slot
    this.contentEl = document.createElement('div')
    this.contentEl.className = 'cmm-field__content'
    this.el.appendChild(this.contentEl)

    // Create hint element
    this.hintEl = document.createElement('div')
    this.hintEl.className = 'cmm-field__hint'
    this.hintEl.id = this.hintId
    if (config.hint !== undefined) {
      this.hintEl.textContent = config.hint
    }
    this.el.appendChild(this.hintEl)

    // Create error element
    this.errorEl = document.createElement('div')
    this.errorEl.className = 'cmm-field__error'
    this.errorEl.id = this.errorId
    this.errorEl.setAttribute('role', 'alert')
    if (config.error !== undefined) {
      this.errorEl.textContent = config.error
      this.el.classList.add('cmm-field--error')
    }
    this.el.appendChild(this.errorEl)

    // Mount if container provided
    if (config.container !== undefined) {
      const parent = resolveContainer(config.container)
      parent.appendChild(this.el)
    }
  }

  /** Update the label text */
  setLabel(label: string): void {
    if (this.labelEl !== null && this.labelTextNode !== null) {
      this.labelTextNode.textContent = label
    }
  }

  /** Show or hide an error message. Pass null to clear. */
  setError(error: string | null): void {
    if (error !== null) {
      this.errorEl.textContent = error
      this.el.classList.add('cmm-field--error')
    } else {
      this.errorEl.textContent = ''
      this.el.classList.remove('cmm-field--error')
    }
  }

  /** Show or hide hint text. Pass null to clear. */
  setHint(hint: string | null): void {
    if (hint !== null) {
      this.hintEl.textContent = hint
    } else {
      this.hintEl.textContent = ''
    }
  }

  /** Toggle the required indicator */
  setRequired(required: boolean): void {
    if (required && this.requiredEl === null) {
      this.appendRequiredIndicator()
    } else if (!required && this.requiredEl !== null) {
      this.requiredEl.remove()
      this.requiredEl = null
    }
  }

  /** Returns the content container div for mounting input components */
  getContentEl(): HTMLDivElement {
    return this.contentEl
  }

  /** Returns the hint element ID for aria-describedby linkage */
  getHintId(): string {
    return this.hintId
  }

  /** Returns the error element ID for aria-describedby linkage */
  getErrorId(): string {
    return this.errorId
  }

  /**
   * Connect an input element to this field for automatic aria-describedby linkage.
   * Sets up aria-describedby with hint and error IDs.
   * Also updates the label's htmlFor if the input has an ID.
   *
   * @param input The input element to connect (can be HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, or any element with aria-describedby support)
   */
  connect(input: HTMLElement): void {
    // Build aria-describedby value with both hint and error IDs
    // Screen readers will read whichever has content
    const describedBy = `${this.hintId} ${this.errorId}`
    input.setAttribute('aria-describedby', describedBy)

    // If input has an ID, update the label's htmlFor
    const inputId = input.getAttribute('id')
    if (inputId !== null && this.labelEl !== null) {
      this.labelEl.htmlFor = inputId
    }

    // If input doesn't have an ID but needs one for label association, generate one
    if (inputId === null && this.labelEl !== null) {
      const generatedId = uniqueId('cmm-field-input')
      input.setAttribute('id', generatedId)
      this.labelEl.htmlFor = generatedId
    }
  }

  /**
   * Disconnect an input element by removing the aria-describedby attribute.
   */
  disconnect(input: HTMLElement): void {
    input.removeAttribute('aria-describedby')
  }

  /** Remove from the DOM */
  destroy(): void {
    this.el.remove()
  }

  private appendRequiredIndicator(): void {
    if (this.labelEl === null) {
      return
    }
    this.requiredEl = document.createElement('span')
    this.requiredEl.className = 'cmm-field__required'
    this.requiredEl.setAttribute('aria-hidden', 'true')
    this.requiredEl.textContent = '*'
    this.labelEl.appendChild(this.requiredEl)
  }
}
