import { nothing, html, type PropertyValues, type TemplateResult } from 'lit'
import { state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { live } from 'lit/directives/live.js'
import type { UiCheckboxElement } from '../../md/checkbox/ui-checkbox.js'
import { getFormatInfo, getPropertyInputOptions } from '../lib/ui_mapping.js'
import { DomainProperty } from '@api-client/core/modeling/DomainProperty.js'
import { cancelEvent } from '@api-client/core/lib/events/Utils.js'
import { BinaryFormats, type DomainPropertyFormat, NumberFormats } from '@api-client/core/modeling/DataFormat.js'
import {
  PropertySemantic,
  SemanticCategory,
  SemanticScope,
  SemanticType,
  getSemanticsByCategory,
} from '@api-client/core/modeling/Semantics.js'
import { ModelingElement } from './ModelingElement.js'
import type { DomainPropertyInputConfig } from '../types.js'
import { renderHelp } from '../../help/render.js'
import type { TaxonomyDialog } from '../dialogs/TaxonomyDialog.js'
import { PasswordTaxonomy } from '../dialogs/PasswordTaxonomy.js'
import { MarkdownTaxonomy } from '../dialogs/MarkdownTaxonomy.js'
import { HtmlTaxonomy } from '../dialogs/HtmlTaxonomy.js'
import { CurrencyTaxonomy } from '../dialogs/CurrencyTaxonomy.js'
import { StatusTaxonomy } from '../dialogs/StatusTaxonomy.js'
import { SkuTaxonomy } from '../dialogs/SkuTaxonomy.js'
import { EmailTaxonomy } from '../dialogs/EmailTaxonomy.js'
import { PhoneTaxonomy } from '../dialogs/PhoneTaxonomy.js'
import { UrlTaxonomy } from '../dialogs/UrlTaxonomy.js'
import { PublicUniqueNameTaxonomy } from '../dialogs/PublicUniqueNameTaxonomy.js'
import { GeospatialCoordinatesTaxonomy } from '../dialogs/GeospatialCoordinatesTaxonomy.js'
import { CalculatedTaxonomy } from '../dialogs/CalculatedTaxonomy.js'
import { PostalCodeTaxonomy } from '../dialogs/PostalCodeTaxonomy.js'
import { CountryTaxonomy } from '../dialogs/CountryTaxonomy.js'
import type { UiSelectElement } from '../../md/select/ui-select.js'

/**
 * Dispatched when the user deletes a property.
 *
 * @event DomainPropertyEditor#delete
 * @type {CustomEvent}
 * @property {object} detail - The event details.
 * @property {string} detail.key - The key of the parent entity to navigate to.
 * @property {string} detail.kind - The kind of the parent entity. It will always be `DomainEntityKind`.
 */

/**
 * `DomainPropertyEditor` is a LitElement-based web component that provides a user interface for editing
 * properties of a `DomainProperty` object. It allows users to modify various aspects of a data property,
 * including its metadata, attributes, schema, and enum values.
 *
 * ## Features
 *
 * -   **Metadata Editing**: Allows editing of the property's name, display name, and description.
 * -   **Attribute Management**: Provides controls for setting data type, format, and other attributes
 *     like `required`, `multiple`, `primary`, `index`, and `readOnly`.
 * -   **Schema Definition**: Enables defining the schema for the property, including default values
 *     and enum values.
 * -   **Enum Editor**: Supports enabling and disabling an enum editor for predefined values, with
 *     options to add, update, and delete enum values.
 * -   **Default Value Setting**: Allows setting a default value for the property, either as a literal
 *     value or a function.
 * -   **Data Type and Format Selection**: Provides dropdowns for selecting the data type and format
 *     of the property.
 * -   **Validation**: Includes input validation to ensure data integrity.
 * -   **Change Notification**: Dispatches a `change` event whenever a property is modified.
 *
 * ## Usage
 *
 * The `DomainPropertyEditor` is typically used within a larger application that manages data models.
 *
 * ```html
 * <domain-property-editor .root="${this.domainNamespace}" selected="property-key"></domain-property-editor>
 * ```
 *
 * @fires DomainPropertyEditor#delete - A custom event with the `key` and `type` when a property was selected.
 */
export default class DomainPropertyEditor extends ModelingElement {
  /**
   * The `DomainProperty` object being edited.
   * This is updated when the `selected` property changes.
   */
  @state() private accessor domainElement: DomainProperty | undefined
  /**
   * Indicates whether the default value input field is visible.
   * This is set to `true` when the user selects the "Literal" option in the default value select.
   */
  @state() private accessor hasDefaultValueInput = false
  /**
   * Whether the enum options are enabled.
   * When `true` the user can add, update, and delete enum values.
   */
  @state() private accessor enumEnabled = false

  /**
   * A fields that holds in memory enum values after disabling the enum editor.
   * When that happens the values are removed from the model but kept in memory.
   * As long as the property stays the same, the user can reopen the enum editor
   * and restore the previous value.
   */
  protected cachedEnumValues: string[] | undefined
  /**
   * A cached value of the schema-sensitive input configuration.
   * These are the default value and enum value inputs.
   */
  protected cachedSchemaInputConfig?: DomainPropertyInputConfig

  /**
   * A configuration for a schema-sensitive HTML input.
   */
  get schemaInputConfig(): DomainPropertyInputConfig {
    return this.cachedSchemaInputConfig || { type: 'text' }
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('selected') || cp.has('root') || cp.has('namespace')) {
      this.computeProperty()
    }
    super.willUpdate(cp)
  }

  protected async computeProperty(): Promise<void> {
    const { root, selected, namespace } = this
    this.domainElement = undefined
    if (!root || !selected) {
      return
    }
    await this.updateComplete
    const value = root.findProperty(selected, namespace)
    this.domainElement = value
    const schema = value?.ensureSchema()
    if (schema && schema.defaultValue) {
      this.hasDefaultValueInput = schema.defaultValue.type === 'literal'
    } else {
      this.hasDefaultValueInput = false
    }
    if (schema && schema.enum) {
      this.enumEnabled = true
    } else {
      this.enumEnabled = false
    }
    this.cachedEnumValues = undefined
    this.updateSchemaInputConfig()
  }

  /**
   * Toggles the state of the enum editor.
   *
   * If the editor was disabled, it enables it and vice-versa.
   *
   * It ensures the property has the schema and then:
   * - When enabling:
   *   - If `cachedEnumValues` is set it restores them.
   *   - Otherwise it sets the default enum value `['']`
   * - When disabling:
   *   - It sets the `cachedEnumValues` to the current enum value.
   *   - Removes the enum from the model
   * - Requests the model update.
   *
   * @fires change - Dispatched when the enum editor state changes.
   */
  toggleEnum(): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    this.enumEnabled = !this.enumEnabled
    const schema = domainElement.ensureSchema()
    if (!this.enumEnabled) {
      this.cachedEnumValues = schema.enum
      schema.enum = undefined
    } else if (this.cachedEnumValues) {
      schema.enum = this.cachedEnumValues || ['']
      this.cachedEnumValues = undefined
    } else {
      schema.enum = ['']
    }
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * Updates an enum value.
   *
   * @param index The index at which to update the value.
   * @param value The value to set. Empty string is acceptable. The type is determined by the property type.
   * @throws {RangeError} When the index is out of bounds.
   * @throws {TypeError} When the value is not a string.
   * @fires change - Dispatched when the enum value is updated.
   */
  updateEnum(index: number, value: string): void {
    if (index < 0) {
      throw new RangeError(`Enum array index cannot be negative.`)
    }
    if (typeof value !== 'string') {
      throw new TypeError(`Expected a string for the enum value, got ${typeof value}.`)
    }
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const schema = domainElement.ensureSchema()
    if (!schema.enum) {
      schema.enum = [value]
    } else {
      if (schema.enum.length - 1 < index) {
        throw new RangeError(`Invalid enum array index: ${index}.`)
      }
      schema.enum[index] = value
    }
    this.notifyChange()
  }

  /**
   * Adds a new enum value to the property.
   *
   * @param value The value to add. Defaults to an empty string.
   * @throws {TypeError} When the value is not a string.
   * @fires change - Dispatched when a new enum value is added.
   */
  addEnum(value = ''): void {
    if (typeof value !== 'string') {
      throw new TypeError(`Expected a string for the enum value, got ${typeof value}.`)
    }
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const schema = domainElement.ensureSchema()
    if (!schema.enum) {
      schema.enum = [value]
    } else {
      schema.enum.push(value)
    }
    this.requestUpdate()
    // this should be immediately propagated to other clients.
    this.notifyChange()
  }

  /**
   * Deletes an enum value at a position.
   *
   * @param index The index at which to delete an enum property.
   * @throws {RangeError} When the index is out of bounds.
   * @fires change - Dispatched when an enum value is deleted.
   */
  deleteEnum(index: number): void {
    if (index < 0) {
      throw new RangeError(`Enum array index cannot be negative.`)
    }
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const schema = domainElement.ensureSchema()
    if (!schema.enum) {
      return
    }
    if (schema.enum.length - 1 < index) {
      throw new RangeError(`Invalid enum array index: ${index}.`)
    }
    schema.enum.splice(index, 1)
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * Sets a new defaultValue on the property schema.
   *
   * @param value The default value to set. Empty to remove.
   * @param type The type of the value.
   * @param dataType Optional data type to set. Defaults to string. Only used with `type` equal `literal`.
   * @fires change - Dispatched when the default value is updated.
   */
  setDefaultValue(value?: string, type: 'function' | 'literal' = 'function'): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const schema = domainElement.ensureSchema()
    if (value === schema.defaultValue?.value) {
      return
    }
    if (value || (type === 'literal' && value === '')) {
      schema.defaultValue = {
        type,
        value: value,
      }
    } else {
      delete schema.defaultValue
    }
    this.notifyChange()
    this.requestUpdate()
  }

  /**
   * Updates the property's data type.
   *
   * It sets the `format` for `number`, `datetime`, and `binary` data types.
   *
   * @param value The value to set.
   * @throws {TypeError} When the passed value is not a valid data type.
   * @fires change - Dispatched when the data type is updated.
   */
  setDataType(value: string): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    if (!DomainProperty.isValidType(value)) {
      throw new TypeError(`Invalid data type: ${value}.`)
    }
    domainElement.type = value
    const binding = domainElement.getWebBinding()
    if (value === 'number') {
      binding.format = 'int64'
    } else if (value === 'binary') {
      binding.format = 'base64'
    } else {
      binding.format = undefined
    }
    this.notifyChange()
    this.requestUpdate()
    this.updateSchemaInputConfig()
  }

  /**
   * Sets the value of one of the "attribute" fields.
   * These are the checkboxes located in the "Attributes" section.
   *
   * @param name The name of the attribute to set.
   * @param value The boolean option to set as the value.
   * @throws {TypeError} When the passed name is not a valid attribute name.
   * @throws {TypeError} When the passed value is not a boolean value.
   * @fires change - Dispatched when an attribute value is updated.
   */
  setModelAttribute(name: string, value: boolean): void {
    if (!DomainProperty.isValidAttribute(name)) {
      throw new TypeError(`Invalid attribute name: ${name}.`)
    }
    if (typeof value !== 'boolean') {
      throw new TypeError(`Invalid attribute value: ${value}.`)
    }
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    domainElement[name] = value
    this.notifyChange()
    this.requestUpdate()
  }

  /**
   * Sets the `format` value on the model.
   *
   * @param format The format value to set.
   * @throws {TypeError} When the passed format value is not supported.
   * @fires change - Dispatched when the format value is updated.
   */
  setFormat(format?: string): void {
    if (!DomainProperty.isValidFormat(format)) {
      throw new TypeError(`Invalid format value: ${format}.`)
    }
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const binding = domainElement.getWebBinding()
    if (binding.format === format) {
      return
    }
    binding.format = format
    this.requestUpdate()
    this.notifyChange()
    this.updateSchemaInputConfig()
  }

  /**
   * Sets an example value on the schema.
   * @param value The value to set. Removes the examples array if not set.
   */
  setExampleValue(value?: string): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    // @TODO: Eventually, support multiple examples
    const schema = domainElement.ensureSchema()
    if (value) {
      schema.examples = [value]
    } else {
      delete schema.examples
    }
    this.requestUpdate()
    this.notifyChange()
  }

  /**
   * A method to be called when a schema property changes.
   * It caches the schema-sensitive input configuration.
   */
  protected updateSchemaInputConfig(): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    this.cachedSchemaInputConfig = getPropertyInputOptions(domainElement)
  }

  /**
   * A handler for the one of the metadata inputs value change.
   *
   * It supports the following fields:
   *
   * - `name`
   * - `displayName`
   * - `description`
   * - `literalDefaultValue`
   */
  protected handleMetaChange(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const node = e.target as HTMLInputElement
    const { name, value } = node
    if (value && !node.reportValidity()) {
      return
    }
    if (!['name', 'displayName', 'description'].includes(name as string)) {
      return
    }
    if (name === 'name' && !value) {
      return
    }
    domainElement.info[name as 'name' | 'description' | 'displayName'] = value
    this.notifyChange()
  }

  protected handleSchemaChange(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const node = e.target as HTMLInputElement
    const { name, value, checked } = node
    if ((value || checked) && !node.reportValidity()) {
      return
    }
    if (name === 'literalDefaultValue') {
      this.setDefaultValue(value, 'literal')
      return
    }
    if (['minimum', 'maximum', 'multipleOf'].includes(name as string)) {
      const schema = domainElement.ensureSchema()
      schema[name as 'minimum' | 'maximum' | 'multipleOf'] = Number(value) || undefined
    } else if (['exclusiveMinimum', 'exclusiveMaximum'].includes(name as string)) {
      const schema = domainElement.ensureSchema()
      schema[name as 'exclusiveMinimum' | 'exclusiveMaximum'] = checked
    }
    this.updateSchemaInputConfig()
    this.notifyChange()
  }

  /**
   * A handler for the data type select input change
   */
  protected handleDataTypeChange(e: Event): void {
    const select = e.target as UiSelectElement
    const { value, name } = select
    if (name !== 'datatype' || !value) {
      return
    }
    this.setDataType(value)
  }

  /**
   * A handler for any of the checkboxes in the `attributes` section.
   * It calls the `setModelAttribute()` method.
   */
  protected handleAttributeOptionChange(e: Event): void {
    const checkbox = e.target as UiCheckboxElement
    const { name = '', checked } = checkbox
    this.setModelAttribute(name, !!checked)
  }

  /**
   * A handler for the default value select input change.
   * Depending on the selected options it calls the `setDefaultValue()`
   * with appropriate arguments.
   */
  protected handleDefaultValueChange(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const select = e.target as UiSelectElement
    const { value = '' } = select
    if (!value) {
      this.setDefaultValue()
      this.hasDefaultValueInput = false
      return
    }
    if (value.startsWith('function.')) {
      const fName = value.substring(9)
      this.setDefaultValue(fName, 'function')
      this.hasDefaultValueInput = false
      return
    }
    if (value.startsWith('literal.')) {
      const setValue = value.substring(8)
      this.setDefaultValue(setValue, 'literal')
      this.hasDefaultValueInput = false
      return
    }
    if (value === 'literal') {
      this.hasDefaultValueInput = true
      this.setDefaultValue('', 'literal')
    }
  }

  /**
   * A handler for the format select input change.
   * Calls the `setFormat()` with the appropriate value.
   */
  protected handleFormatChange(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const select = e.target as UiSelectElement
    const { value } = select
    this.setFormat(value)
  }

  /**
   * Handles the toggle button for the enum editor.
   * It calls the toggleEnum() whenever activated.
   */
  protected handleEnumOptionChange(): void {
    this.toggleEnum()
  }

  /**
   * A handler for the enum input change.
   * It calls the `updateEnum()` method
   */
  protected handleEnumChange(e: Event): void {
    const input = e.target as HTMLInputElement
    if (!input.reportValidity()) {
      return
    }
    const { value, dataset } = input
    const index = Number(dataset.index)
    if (isNaN(index)) {
      return
    }
    this.updateEnum(index, value)
  }

  /**
   * A handler for the add enum button.
   */
  protected handleAddEnum(): void {
    this.addEnum()
  }

  /**
   * A handler for the delete enum value button click.
   */
  protected handleDeleteEnum(e: Event): void {
    const button = e.currentTarget as HTMLElement
    const { dataset } = button
    const index = Number(dataset.index)
    if (isNaN(index)) {
      return
    }
    this.deleteEnum(index)
  }

  /**
   * Handles a change on the example input.
   */
  protected handleExampleChange(e: Event): void {
    const node = e.target as HTMLInputElement
    const { name, value } = node
    if (value && !node.reportValidity()) {
      return
    }
    if (name !== 'example') {
      return
    }
    this.setExampleValue(value)
  }

  // protected handleTaxonomyChange(e: Event): void {
  //   const node = e.currentTarget as MdOutlinedSelect
  //   const { value } = node
  //   if (!value) {
  //     return
  //   }
  //   node.displayText = ''
  //   this.domainElement?.addSemantic({
  //     id: value as SemanticType,
  //   })
  //   this.requestUpdate()
  // }

  protected handleDeleteTaxonomy(e: Event): void {
    const button = e.currentTarget as HTMLButtonElement
    const { id } = button.dataset
    if (!id) {
      return
    }
    this.domainElement?.removeSemantic(id as SemanticType)
  }

  protected handleTaxonomySelect(e: CustomEvent<{ item: HTMLElement }>): void {
    const { value } = e.detail.item.dataset
    if (value) {
      this.domainElement?.addSemantic({
        id: value as SemanticType,
      })
      this.requestUpdate()
    }
  }

  protected handleConfigureTaxonomy(e: Event): void {
    const button = e.currentTarget as HTMLButtonElement
    const { id } = button.dataset
    if (!id) {
      return
    }
    const element = this.domainElement
    if (!element) {
      return
    }
    let dialog: TaxonomyDialog
    if (id === SemanticType.Password) {
      dialog = new PasswordTaxonomy(element)
    } else if (id === SemanticType.Markdown) {
      dialog = new MarkdownTaxonomy(element)
    } else if (id === SemanticType.HTML) {
      dialog = new HtmlTaxonomy(element)
    } else if (id === SemanticType.Status) {
      dialog = new StatusTaxonomy(element)
    } else if (id === SemanticType.Currency) {
      dialog = new CurrencyTaxonomy(element)
    } else if (id === SemanticType.SKU) {
      dialog = new SkuTaxonomy(element)
    } else if (id === SemanticType.Email) {
      dialog = new EmailTaxonomy(element)
    } else if (id === SemanticType.Phone) {
      dialog = new PhoneTaxonomy(element)
    } else if (id === SemanticType.URL) {
      dialog = new UrlTaxonomy(element)
    } else if (id === SemanticType.PublicUniqueName) {
      dialog = new PublicUniqueNameTaxonomy(element)
    } else if (id === SemanticType.GeospatialCoordinates) {
      dialog = new GeospatialCoordinatesTaxonomy(element)
    } else if (id === SemanticType.Calculated) {
      dialog = new CalculatedTaxonomy(element)
    } else if (id === SemanticType.PostalCode) {
      dialog = new PostalCodeTaxonomy(element)
    } else if (id === SemanticType.Country) {
      dialog = new CountryTaxonomy(element)
    } else {
      throw new TypeError(`Unsupported taxonomy type: ${id}.`)
    }
    dialog.run()
  }

  protected override render(): TemplateResult | typeof nothing {
    const { domainElement } = this
    if (!domainElement) {
      return nothing
    }
    return this.renderForm(domainElement)
  }

  protected renderForm(model: DomainProperty): TemplateResult {
    return html`
      <form name="property" @submit="${cancelEvent}" data-key="${model.key}" class="modeling-form">
        ${this.renderMetaInputs(model)} ${this.renderAttributes(model)} ${this.renderSchema(model)}
        ${this.renderDataGovernanceSection(model)}
      </form>
    `
  }

  protected renderMetaInputs(model: DomainProperty): TemplateResult {
    const disabled = !this.canEdit
    const formatError =
      'Name must start with a letter or underscore and can only contain letters, numbers, and underscores.'
    return html`
      <div class="editor-section">
        <div class="row input-row">
          <ui-outlined-text-field
            class="input"
            name="name"
            .value="${live(model.info.name || '')}"
            label="Name"
            required
            minLength="2"
            maxLength="59"
            pattern="[a-zA-Z_][a-zA-Z0-9_]+"
            .invalidText="${formatError}"
            ?disabled="${disabled}"
            @change="${this.handleMetaChange}"
          ></ui-outlined-text-field>
          ${renderHelp('property', 'name-input')}
        </div>
        <div class="row input-row">
          <ui-outlined-text-field
            class="input"
            name="displayName"
            .value="${live(model.info.displayName || '')}"
            label="Display name (optional)"
            ?disabled="${disabled}"
            @change="${this.handleMetaChange}"
          ></ui-outlined-text-field>
          ${renderHelp('property', 'displayName-input')}
        </div>
        <div class="row input-row">
          <ui-outlined-text-field
            class="input"
            name="description"
            .value="${live(model.info.description || '')}"
            label="Description (optional)"
            ?disabled="${disabled}"
            @change="${this.handleMetaChange}"
          ></ui-outlined-text-field>
          ${renderHelp('property', 'description-input')}
        </div>
      </div>
    `
  }

  protected renderAttributes(model: DomainProperty): TemplateResult {
    return html`<div class="editor-section">
      <div class="section-title title-medium">Attributes</div>
      ${this.renderDataTypeInput(model)} ${this.renderFormat(model)} ${this.renderAttributeOptions(model)}
    </div>`
  }

  protected renderTaxonomy(): TemplateResult {
    return html`<div class="editor-section"></div>`
  }

  protected renderSchema(model: DomainProperty): TemplateResult | typeof nothing {
    if (model.type === 'binary') {
      return nothing
    }
    let content: (TemplateResult | typeof nothing)[] | typeof nothing
    switch (model.type) {
      case 'string':
        content = this.renderStringSchema(model)
        break
      case 'number':
        content = this.renderNumberSchema(model)
        break
      case 'boolean':
        content = this.renderBooleanSchema(model)
        break
      case 'date':
        content = this.renderDateSchema(model)
        break
      case 'time':
        content = this.renderTimeSchema(model)
        break
      case 'datetime':
        content = this.renderDateTimeSchema(model)
        break
      // case 'binary':
      //   content = this.renderBinarySchema(model)
      //   break
      default:
        content = nothing
    }
    return html`<div class="editor-section">
      <div class="section-title title-medium">Schema</div>
      ${content}
    </div>`
  }

  protected renderDataTypeInput(model: DomainProperty): TemplateResult {
    const disabled = !this.canEdit
    const { type } = model
    return html`<div class="row input-row">
      <ui-select
        label="Data Type"
        class="input"
        required
        name="datatype"
        ?disabled="${disabled}"
        @change="${this.handleDataTypeChange}"
      >
        <ui-option ?selected="${type === 'string'}" value="string">
          <div>String</div>
        </ui-option>
        <ui-option ?selected="${type === 'number'}" value="number">
          <div>Number</div>
        </ui-option>
        <ui-option ?selected="${type === 'boolean'}" value="boolean">
          <div>Boolean</div>
        </ui-option>
        <ui-option ?selected="${type === 'date'}" value="date">
          <div>Date</div>
        </ui-option>
        <ui-option ?selected="${type === 'time'}" value="time">
          <div>Time</div>
        </ui-option>
        <ui-option ?selected="${type === 'datetime'}" value="datetime">
          <div>Date and time</div>
        </ui-option>
        <ui-option ?selected="${type === 'binary'}" value="binary">
          <div>Binary</div>
        </ui-option>
      </ui-select>
      ${renderHelp('property', 'datatype-select')}
    </div>`
  }

  protected renderAttributeOptions(model: DomainProperty): TemplateResult {
    const { primary = false, index = false, required = false, multiple = false, readOnly = false } = model
    const disabled = !this.canEdit
    return html`<div class="checkbox-group">
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="att-identifier"
            .checked="${primary}"
            name="primary"
            @change="${this.handleAttributeOptionChange}"
            ?disabled="${disabled}"
          ></ui-checkbox>
          <div class="label">
            <label for="att-identifier" class="body-medium">Identifier</label>
            <span class="body-small meta">The primary key in the data store.</span>
          </div>
        </div>
        ${renderHelp('property', 'primary-checkbox')}
      </div>
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="att-index"
            .checked="${index}"
            name="index"
            ?disabled="${disabled}"
            @change="${this.handleAttributeOptionChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="att-index" class="body-medium">Search field</label>
            <span class="body-small meta">Marks this field as index and enables searching on it.</span>
          </div>
        </div>
        ${renderHelp('property', 'index-checkbox')}
      </div>
      <div class="row">
        <div class="input checkbox">
          <ui-checkbox
            id="att-required"
            .checked="${required}"
            name="required"
            ?disabled="${disabled}"
            @change="${this.handleAttributeOptionChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="att-required" class="body-medium">Required</label>
          </div>
        </div>
        ${renderHelp('property', 'required-checkbox')}
      </div>
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="att-multiple"
            .checked="${multiple}"
            name="multiple"
            ?disabled="${disabled}"
            @change="${this.handleAttributeOptionChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="att-multiple" class="body-medium">Multiple values</label>
            <span class="body-small meta">Creates an array of values.</span>
          </div>
        </div>
        ${renderHelp('property', 'multiple-checkbox')}
      </div>
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="att-readOnly"
            .checked="${readOnly}"
            name="readOnly"
            ?disabled="${disabled}"
            @change="${this.handleAttributeOptionChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="att-readOnly" class="body-medium">Read only</label>
            <span class="body-small meta">The value can only be set when object is created.</span>
          </div>
        </div>
        ${renderHelp('property', 'readOnly-checkbox')}
      </div>
    </div>`
  }

  protected renderStringSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [
      this.renderDefaultValueInput(model),
      this.renderLiteralDefaultValueInput(model),
      this.renderExampleValueInput(model),
      this.renderEnumEditor(model),
      this.renderMinimumValueInput(model),
      this.renderExclusiveMinimumInput(model),
      this.renderMaximumValueInput(model),
      this.renderExclusiveMaximumInput(model),
    ]
  }

  protected renderNumberSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [
      this.renderMultipleOfInput(model),
      this.renderDefaultValueInput(model),
      this.renderLiteralDefaultValueInput(model),
      this.renderExampleValueInput(model),
      this.renderEnumEditor(model),
      this.renderMinimumValueInput(model),
      this.renderExclusiveMinimumInput(model),
      this.renderMaximumValueInput(model),
      this.renderExclusiveMaximumInput(model),
    ]
  }

  protected renderBooleanSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [this.renderDefaultValueInput(model)]
  }

  protected renderDateSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [
      this.renderDefaultValueInput(model),
      this.renderLiteralDefaultValueInput(model),
      this.renderExampleValueInput(model),
      this.renderMinimumValueInput(model),
      this.renderExclusiveMinimumInput(model),
      this.renderMaximumValueInput(model),
      this.renderExclusiveMaximumInput(model),
    ]
  }

  protected renderTimeSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [
      this.renderDefaultValueInput(model),
      this.renderLiteralDefaultValueInput(model),
      this.renderExampleValueInput(model),
      this.renderMinimumValueInput(model),
      this.renderExclusiveMinimumInput(model),
      this.renderMaximumValueInput(model),
      this.renderExclusiveMaximumInput(model),
    ]
  }

  protected renderDateTimeSchema(model: DomainProperty): (TemplateResult | typeof nothing)[] {
    return [
      this.renderDefaultValueInput(model),
      this.renderLiteralDefaultValueInput(model),
      this.renderExampleValueInput(model),
      this.renderMinimumValueInput(model),
      this.renderExclusiveMinimumInput(model),
      this.renderMaximumValueInput(model),
      this.renderExclusiveMaximumInput(model),
    ]
  }

  /**
   * Renders the input for the default value.
   *
   * Note, when defining a new function, add support for it generation in the
   * `src/modeling/value_generator.ts` file.
   */
  protected renderDefaultValueInput(model: DomainProperty): TemplateResult | typeof nothing {
    const { type } = model
    //  || type === 'any' || type === 'nil'
    if (type === 'binary') {
      return nothing
    }
    const schema = model.schema || {}
    const { defaultValue } = schema
    let isNone = true
    let isLiteral = false
    if (defaultValue) {
      if (defaultValue.type === 'literal') {
        isNone = false
        isLiteral = true
      } else if (defaultValue.value) {
        isNone = false
      }
    }

    let allowLiteral = false
    const options: TemplateResult[] = [
      html`<ui-option ?selected="${isNone}" value="">
        <div>None</div>
      </ui-option>`,
      html`<ui-divider></ui-divider>`,
    ]
    if (type === 'string') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'random'}" value="function.random">
          <div>Random string</div>
          <div slot="supporting-text">Generates a random string.</div>
        </ui-option>`
      )
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'uuid-v4'}" value="function.uuid-v4">
          <div>UUID</div>
          <div slot="supporting-text">Random UUID v4 value.</div>
        </ui-option>`
      )
      allowLiteral = true
    } else if (type === 'number') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'incremental'}" value="function.incremental">
          <div>Incremental</div>
          <div slot="supporting-text">Increments the value with each new object.</div>
        </ui-option>`
      )
      allowLiteral = true
    } else if (type === 'boolean') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'false'}" value="literal.false">
          <div>FALSE</div>
        </ui-option>`
      )
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'true'}" value="literal.true">
          <div>TRUE</div>
        </ui-option>`
      )
    } else if (type === 'date') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'now'}" value="function.now">
          <div>Current date</div>
          <div slot="supporting-text">YYYY-MM-DD format</div>
        </ui-option>`
      )
      allowLiteral = true
    } else if (type === 'time') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'now'}" value="function.now">
          <div>Current time</div>
          <div slot="supporting-text">HH:mm:ss format.</div>
        </ui-option>`
      )
      allowLiteral = true
    } else if (type === 'datetime') {
      options.push(
        html`<ui-option ?selected="${defaultValue?.value === 'now'}" value="function.now">
          <div>Current date and time</div>
          <div slot="supporting-text">YYYY-MM-DDTHH:mm:ss format.</div>
        </ui-option>`
      )
      allowLiteral = true
    }

    if (allowLiteral) {
      // options.push(html`<ui-divider></ui-divider>`)
      options.push(
        html`<ui-option ?selected="${isLiteral}" value="literal">
          <div>Literal</div>
          <div slot="supporting-text">Enter custom value.</div>
        </ui-option>`
      )
    }
    const disabled = !this.canEdit
    const supportingText = 'Used when creating the object.'
    return html`<div class="row input-row">
      <ui-select
        label="Default value"
        class="input"
        name="defaultValue"
        .supportingText="${supportingText}"
        ?disabled="${disabled}"
        @change="${this.handleDefaultValueChange}"
      >
        ${options}
      </ui-select>
      ${renderHelp('property', 'defaultValue-select')}
    </div>`
  }

  protected renderLiteralDefaultValueInput(model: DomainProperty): TemplateResult | typeof nothing {
    if (!this.hasDefaultValueInput) {
      return nothing
    }
    const schema = model.schema || {}
    const { defaultValue } = schema
    const currentValue = defaultValue?.value || ''
    const info = this.schemaInputConfig
    const disabled = !this.canEdit
    return html`<div class="row input-row">
      <ui-outlined-text-field
        class="input"
        name="literalDefaultValue"
        .value="${live(currentValue)}"
        label="Literal value"
        required
        type="${info.type}"
        pattern="${ifDefined(info.pattern)}"
        min="${ifDefined(info.min)}"
        max="${ifDefined(info.max)}"
        step="${ifDefined(info.step)}"
        .supportingText="${info.help || ''}"
        @change="${this.handleSchemaChange}"
        ?disabled="${disabled}"
      ></ui-outlined-text-field>
      ${renderHelp('property', 'literalDefaultValue-input')}
    </div>`
  }

  protected renderFormat(model: DomainProperty): TemplateResult | typeof nothing {
    const { type } = model
    if (type === 'number') {
      return this.renderFormatInput(model, NumberFormats)
    }
    if (type === 'binary') {
      return this.renderFormatInput(model, BinaryFormats)
    }
    return nothing
  }

  protected renderFormatInput(model: DomainProperty, formats: DomainPropertyFormat[]): TemplateResult {
    const binding = model.getWebBinding()
    const options = formats.map((format) => {
      const info = getFormatInfo(format)
      return html`<ui-option ?selected="${format === binding.format}" value="${format}">
        <div>${info.label}</div>
        <div slot="supporting-text">${info.help}</div>
      </ui-option>`
    })
    const disabled = !this.canEdit
    return html`<div class="row input-row">
      <ui-select
        label="Format"
        class="input"
        name="format"
        ?disabled="${disabled}"
        @change="${this.handleFormatChange}"
      >
        ${options}
      </ui-select>
      ${renderHelp('property', 'format-select')}
    </div>`
  }

  /**
   * Renders the enum editing editor section.
   */
  protected renderEnumEditor(model: DomainProperty): TemplateResult {
    return html`<div class="row">${this.renderEnumToggle()} ${renderHelp('property', 'enumValues-switch')}</div>
      ${this.renderEnumValues(model)} ${this.renderAddEnumButton()}`
  }

  /**
   * Renders the enum toggle button.
   */
  protected renderEnumToggle(): TemplateResult {
    const disabled = !this.canEdit
    return html`<div class="input enum">
      <ui-switch
        .checked="${this.enumEnabled}"
        id="enumValues"
        ?disabled="${disabled}"
        @change="${this.handleEnumOptionChange}"
      ></ui-switch>
      <label for="enumValues" class="body-medium">Pre-defined values (enum)</label>
    </div>`
  }

  /**
   * Renders the list of enum inputs or nothing when not required.
   */
  protected renderEnumValues(model: DomainProperty): TemplateResult[] | typeof nothing {
    if (!this.enumEnabled) {
      return nothing
    }
    const schema = model.schema || {}
    // We use the default value to render the first input when no enum was set.
    const { enum: enumValues = [''] } = schema
    if (!enumValues.length) {
      return nothing
    }
    return enumValues.map((value, index) => this.renderEnumItem(value, index))
  }

  /**
   * Renders the text field to edit an enum value.
   * @param value The enum value to render in the input
   * @param index The enum's index in the enum array.
   */
  protected renderEnumItem(value: string, index: number): TemplateResult {
    const info = this.schemaInputConfig
    const disabled = !this.canEdit
    return html`<div class="enum-input row">
      <ui-outlined-text-field
        class="input"
        name="enum"
        .value="${live(value)}"
        label="Value"
        data-index="${index}"
        @change="${this.handleEnumChange}"
        type="${info.type}"
        pattern="${ifDefined(info.pattern)}"
        min="${ifDefined(info.min)}"
        max="${ifDefined(info.max)}"
        step="${ifDefined(info.step)}"
        ?disabled="${disabled}"
        .supportingText="${info.help || ''}"
      ></ui-outlined-text-field>
      <ui-icon-button type="button" data-index="${index}" @click="${this.handleDeleteEnum}"
        ><ui-icon icon="deleteOutline"></ui-icon
      ></ui-icon-button>
    </div>`
  }

  /**
   * Renders the "add" value value button.
   */
  protected renderAddEnumButton(): TemplateResult | typeof nothing {
    if (!this.enumEnabled || !this.canEdit) {
      return nothing
    }
    return html`<div class="enum-input row">
      <ui-button color="outlined" @click="${this.handleAddEnum}">
        <ui-icon slot="icon" icon="add"></ui-icon>Add
      </ui-button>
    </div>`
  }

  protected renderExampleValueInput(model: DomainProperty): TemplateResult {
    const schema = model.schema || {}
    const { examples = [] } = schema
    const [example = ''] = examples
    const info = this.schemaInputConfig
    const disabled = !this.canEdit
    return html`<div class="row input-row">
      <ui-outlined-text-field
        class="input"
        name="example"
        .value="${live(example)}"
        label="Example value"
        @change="${this.handleExampleChange}"
        pattern="${ifDefined(info.pattern)}"
        min="${ifDefined(info.min)}"
        max="${ifDefined(info.max)}"
        step="${ifDefined(info.step)}"
        .supportingText="${info.help || 'Used with documentation.'}"
        .type="${info.type}"
        ?disabled="${disabled}"
      ></ui-outlined-text-field>
      ${renderHelp('property', 'example-input')}
    </div>`
  }

  protected renderMinimumValueInput(model: DomainProperty): TemplateResult | typeof nothing {
    const schema = model.schema || {}
    const { minimum } = schema
    let label = 'Minimum length'
    if (model.type === 'number') {
      label = 'Minimum value'
    } else if (model.type === 'date' || model.type === 'datetime') {
      label = 'Minimum date'
    } else if (model.type === 'time') {
      label = 'Minimum time'
    }
    const info = this.schemaInputConfig
    const disabled = !this.canEdit
    return html`<div class="row input-row">
      <ui-outlined-text-field
        class="input"
        name="minimum"
        .value="${live((minimum as unknown as string) ?? '')}"
        label="${label}"
        type="${info.type}"
        pattern="${ifDefined(info.pattern)}"
        min="${ifDefined(info.min)}"
        max="${ifDefined(info.max)}"
        step="${ifDefined(info.step)}"
        .supportingText="${info.help || ''}"
        @change="${this.handleSchemaChange}"
        ?disabled="${disabled}"
      ></ui-outlined-text-field>
      ${renderHelp('property', 'schema:minimum-input')}
    </div>`
  }

  protected renderExclusiveMinimumInput(model: DomainProperty): TemplateResult | typeof nothing {
    const schema = model.schema || {}
    const { exclusiveMinimum = false } = schema
    const disabled = !this.canEdit
    return html`<div class="checkbox row">
      <div class="input checkbox metadata">
        <ui-checkbox
          id="exclusive-minimum"
          .checked="${exclusiveMinimum}"
          name="exclusiveMinimum"
          @change="${this.handleSchemaChange}"
          ?disabled="${disabled}"
        ></ui-checkbox>
        <div class="label">
          <label for="exclusive-minimum" class="body-medium">Exclude minimum value</label>
          <span class="body-small meta">The value must be greater than, but not equal to, the minimum.</span>
        </div>
      </div>
      ${renderHelp('property', 'schema:exclusive-minimum-checkbox')}
    </div>`
  }

  protected renderExclusiveMaximumInput(model: DomainProperty): TemplateResult | typeof nothing {
    const schema = model.schema || {}
    const { exclusiveMaximum } = schema
    const disabled = !this.canEdit
    return html`<div class="checkbox row">
      <div class="input checkbox metadata">
        <ui-checkbox
          id="exclusive-maximum"
          .checked="${exclusiveMaximum || false}"
          name="exclusiveMaximum"
          @change="${this.handleSchemaChange}"
          ?disabled="${disabled}"
        ></ui-checkbox>
        <div class="label">
          <label for="exclusive-maximum" class="body-medium">Exclude maximum value</label>
          <span class="body-small meta">The value must be less than, but not equal to, the maximum.</span>
        </div>
      </div>
      ${renderHelp('property', 'schema:exclusive-maximum-checkbox')}
    </div>`
  }

  protected renderMaximumValueInput(model: DomainProperty): TemplateResult | typeof nothing {
    const schema = model.schema || {}
    const { maximum } = schema
    let label = 'Maximum length'
    if (model.type === 'number') {
      label = 'Maximum value'
    } else if (model.type === 'date' || model.type === 'datetime') {
      label = 'Maximum date'
    } else if (model.type === 'time') {
      label = 'Maximum time'
    }
    const info = this.schemaInputConfig
    const disabled = !this.canEdit
    return html`<div class="row input-row">
      <ui-outlined-text-field
        class="input"
        name="maximum"
        .value="${live((maximum as unknown as string) ?? '')}"
        label="${label}"
        type="${info.type}"
        pattern="${ifDefined(info.pattern)}"
        min="${ifDefined(info.min)}"
        max="${ifDefined(info.max)}"
        step="${ifDefined(info.step)}"
        .supportingText="${info.help || ''}"
        @change="${this.handleSchemaChange}"
        ?disabled="${disabled}"
      ></ui-outlined-text-field>
      ${renderHelp('property', 'schema:maximum-input')}
    </div>`
  }

  protected renderMultipleOfInput(model: DomainProperty): TemplateResult | typeof nothing {
    const schema = model.schema || {}
    const { multipleOf } = schema
    const disabled = !this.canEdit
    const supportingText = 'The number must be divisible by this value. E.g., 2 for even numbers.'
    return html`<div class="row input-row">
      <ui-outlined-text-field
        class="input"
        name="multipleOf"
        .value="${live((multipleOf as unknown as string) || '')}"
        label="Value increment"
        .supportingText="${supportingText}"
        type="number"
        @change="${this.handleSchemaChange}"
        ?disabled="${disabled}"
      ></ui-outlined-text-field>
      ${renderHelp('property', 'schema:multipleOf-input')}
    </div>`
  }

  protected renderDataGovernanceSection(model: DomainProperty): TemplateResult | typeof nothing {
    // TODO: Add classification UI (data tagging)
    const tax = this.renderTaxonomyInput(model)
    if (tax === nothing) {
      return nothing
    }
    return html`<div class="editor-section">
      <div class="section-title title-medium">Data Governance</div>
      ${tax}
    </div>`
  }

  protected renderTaxonomyInput(model: DomainProperty): TemplateResult | typeof nothing {
    const allCategories = getSemanticsByCategory(SemanticScope.Property)
    const selectedSemantics: PropertySemantic[] = []
    const filteredCategories: Record<string, PropertySemantic[]> = {}

    // Filter categories and semantics, excluding already selected ones
    for (const category of Object.keys(allCategories)) {
      const semantics = allCategories[category as SemanticCategory].filter((s) => {
        if (s.applicableDataTypes && !s.applicableDataTypes.includes(model.type)) {
          return false
        }
        // Check if this semantic is already selected
        if (model.semantics.some((ms) => ms.id === s.id)) {
          selectedSemantics.push(s as PropertySemantic)
          return false
        }
        return true
      }) as PropertySemantic[]

      if (semantics.length > 0) {
        filteredCategories[category] = semantics
      }
    }

    if (Object.keys(filteredCategories).length === 0 && selectedSemantics.length === 0) {
      return nothing
    }

    return html`<div class="taxonomy">
        <ui-button
          id="taxonomy-menu-anchor"
          popoverTarget="taxonomy-menu"
          color="outlined"
          aria-label="Add property taxonomy"
          ?disabled="${!this.canEdit}"
        >
          <ui-icon slot="icon">add</ui-icon>Add taxonomy
        </ui-button>
        <ui-menu popover="auto" id="taxonomy-menu" class="taxonomy-menu" @select="${this.handleTaxonomySelect}">
          ${this.renderTaxonomyCategories(filteredCategories)}
        </ui-menu>
        ${renderHelp('property', 'taxonomy-select')}
      </div>
      ${this.renderSelectedSemantics(selectedSemantics)}`
  }

  protected renderSelectedSemantics(selected: PropertySemantic[]): TemplateResult | typeof nothing {
    if (selected.length === 0) {
      return nothing
    }
    const items = selected.map((semantic) => this.renderTaxonomyItem(semantic))
    return html`<div class="enabled-taxonomy">${items}</div>`
  }

  protected renderTaxonomyItem(semantic: PropertySemantic): TemplateResult {
    const canEdit = this.canEdit
    return html`<div class="taxonomy-item">
      <span class="label body-large">${semantic.displayName}</span>
      ${semantic.hasConfig
        ? html`<ui-button
            color="text"
            data-id="${semantic.id}"
            @click="${this.handleConfigureTaxonomy}"
            ?disabled="${!canEdit}"
            >Configure</ui-button
          >`
        : nothing}
      ${canEdit
        ? html`<ui-icon-button
            type="button"
            data-id="${semantic.id}"
            @click="${this.handleDeleteTaxonomy}"
            ?disabled="${!canEdit}"
            ><ui-icon icon="deleteOutline"></ui-icon
          ></ui-icon-button>`
        : nothing}
    </div>`
  }

  protected renderTaxonomyCategories(categories: Record<string, PropertySemantic[]>): TemplateResult[] {
    const items: TemplateResult[] = []
    const entries = Object.entries(categories)
    const cLen = entries.length

    entries.forEach(([categoryName, semantics], categoryIndex) => {
      // Add category title as a non-selectable menu item
      items.push(html` <div class="menu-category-title title-small">${categoryName}</div> `)

      // Add semantics from this category
      for (const semantic of semantics) {
        items.push(html`
          <ui-menu-item data-value="${semantic.id}" lines="two">
            <div>${semantic.displayName}</div>
            <div slot="supporting-text">${semantic.description}</div>
          </ui-menu-item>
        `)
      }

      // Add divider after each category except the last one
      if (categoryIndex < cLen - 1) {
        items.push(html`<ui-divider></ui-divider>`)
      }
    })

    return items
  }
}
