import { html, nothing, type PropertyValues, type TemplateResult } from 'lit'
import { live } from 'lit-html/directives/live.js'
import { property, state } from 'lit/decorators.js'
import { when } from 'lit/directives/when.js'
import { type ClassInfo, classMap } from 'lit/directives/class-map.js'
import { DomainEntity } from '@api-client/core/modeling/DomainEntity.js'
import { addAutoField, addRecommendedFields } from '@api-client/core/modeling/helpers/Intelisense.js'
import { DomainAssociationKind, DomainEntityKind, DomainPropertyKind } from '@api-client/core/models/kinds.js'
import { cancelEvent } from '@api-client/core/lib/events/Utils.js'
import type { DomainAssociation } from '@api-client/core/modeling/DomainAssociation.js'
import type { DomainProperty } from '@api-client/core/modeling/DomainProperty.js'
import { ModelingElement } from './ModelingElement.js'
import type { UiTabsElement } from '../../md/tabs/ui-tabs.js'
import { getFieldDataTypeLabel, getFieldIcon } from '../lib/ui_mapping.js'
import { Events } from '../../events/Events.js'
import {
  type EntitySemantic,
  getSemanticsByCategory,
  SemanticCategory,
  SemanticScope,
  SemanticType,
} from '@api-client/core/modeling/Semantics.js'
import { renderHelp } from '../../help/render.js'
import type { DomainBrowseInit, DomainSelectionInfo } from '../types.js'
import type { UiListSelection } from '../../md/list/ui-list.js'
import { openAutoFieldsDialog } from '../domain-auto-fields-dialog.js'

/**
 * Dispatched when a field (either a `DomainProperty` or a `DomainAssociation`) within the entity is selected.
 *
 * @event DomainEntityEditor#select
 * @type {CustomEvent}
 * @property {object} detail - The event details.
 * @property {string} detail.key - The key of the selected field.
 * @property {string} detail.kind - The kind of the selected field (e.g., `DomainPropertyKind`,
 * `DomainAssociationKind`).
 */

/**
 * Dispatched when the user wants to navigate to a parent entity.
 *
 * @event DomainEntityEditor#openentity
 * @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`.
 */

/**
 * An element that provides an editor interface for a domain entity.
 * @fires DomainEntityEditor#select - A custom event with the `key` and `type` when a property was selected.
 * @fires DomainEntityEditor#openentity - A custom event with the `key` and `type` when a property was selected.
 * @fires domainbrowserbrowse - Dispatched when the user clicks on the "choose parent" button. The detail object
 *                              has the full intent configuration.
 * @fires domainrename - Dispatched when the user renames the entity.
 */
export default class DomainEntityEditor extends ModelingElement {
  protected domainElement: DomainEntity | undefined

  /**
   * The currently selected tab in the editor.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor contentTab: 'design' | 'preview' = 'design'

  /**
   * The key of the currently selected property.
   * @attribute
   */
  @property({ type: String, reflect: true }) accessor selectedProperty: string | undefined

  /**
   * The mime type of the preview to render.
   * @attribute
   */
  @property({ type: String }) accessor mime = 'application/json'
  /**
   * When set, the preview tab will generate example (mock) data.
   * By default it only render values as defined in the schema.
   * @attribute
   */
  @property({ type: Boolean }) accessor mockValues = false

  /**
   * The generated schema preview.
   */
  @state() accessor schemaPreview: string | undefined

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('selected') || cp.has('root') || cp.has('namespace')) {
      this.computeEntity()
    }
    if (cp.has('mime') || cp.has('mockValues')) {
      this.generateExample()
    } else if (cp.has('contentTab')) {
      this.generateExample()
    } else if (this.contentTab === 'preview') {
      this.generateExample()
    }
    super.willUpdate(cp)
  }

  protected computeEntity(): void {
    const { root, selected, namespace } = this
    if (!root || !selected) {
      this.domainElement = undefined
      return
    }
    this.domainElement = root.findEntity(selected, namespace)
  }

  handleMetaChange(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const node = e.target as HTMLInputElement
    if (!node.reportValidity()) {
      return
    }
    const { name, value } = node
    if (!['name', 'displayName', 'description'].includes(name as string)) {
      return
    }
    if (name === 'name' && !value) {
      return
    }
    const preLabel = domainElement.info.getLabel()
    domainElement.info[name as 'name' | 'description' | 'displayName'] = value
    const postLabel = domainElement.info.getLabel()
    this.notifyChange()
    if (preLabel !== postLabel) {
      Events.Domain.rename(domainElement.key, domainElement.domain.key, DomainEntityKind, value, this)
    }
  }

  handleContentTabChange(e: Event): void {
    const tabs = e.target as UiTabsElement
    const tab = tabs.activeTab
    if (!tab) {
      return
    }
    const { key = '' } = tab.dataset
    if (!['design', 'preview'].includes(key)) {
      return
    }
    this.contentTab = key as 'design' | 'preview'
  }

  /**
   * Generates the example data for the preview tab.
   */
  generateExample(): void {
    const { domainElement: model, mime, mockValues = false, contentTab } = this
    if (!model || contentTab !== 'preview') {
      return
    }
    const value = model.toExample(mime, { renderMocked: mockValues, renderExamples: true })
    this.schemaPreview = value as string | undefined
  }

  override notifyChange(): void {
    super.notifyChange()
    this.generateExample()
  }

  /**
   * A handler for the mime type change event.
   */
  handleMimeChange(e: Event): void {
    const node = e.currentTarget as HTMLSelectElement
    const { value } = node
    if (!value) {
      return
    }
    this.mime = value
  }

  /**
   * A handler for the field item click to select a field.
   * @param e The click event.
   */
  handleFieldListSelect(e: CustomEvent<UiListSelection>): void {
    const item = e.detail.item
    const { key, kind } = item.dataset
    if (!key || !kind) {
      return
    }
    this.select(key, kind)
  }

  /**
   * Selects a field in the editor and dispatches a custom event with the key and kind of the field.
   * @param key The key of the field to select.
   * @param kind The kind of the field to select.
   */
  select(key: string, kind: string): void {
    const detail: DomainSelectionInfo = {
      key,
      kind,
      domain: this.root?.key || '',
    }
    this.selectedProperty = key
    this.dispatchEvent(
      new CustomEvent('select', {
        detail,
      })
    )
  }

  addProperty(name?: string): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const prop = domainElement.addProperty({ info: { name: name || 'new_field' } })
    this.select(prop.key, DomainPropertyKind)
    this.notifyChange()
  }

  addAssociation(name?: string): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const prop = domainElement.addAssociation({}, { info: { name: name || 'new_reference' } })
    this.select(prop.key, DomainAssociationKind)
    this.notifyChange()
  }

  handleFieldMenuSelect(e: CustomEvent<{ item: HTMLElement }>): void {
    if (!this.domainElement) {
      return
    }
    const { item } = e.detail
    const { action = '' } = item.dataset
    if (action === 'property') {
      this.addProperty()
    } else if (action === 'association') {
      this.addAssociation()
    } else if (action === 'all-recommended') {
      addRecommendedFields(this.domainElement)
      this.notifyChange()
    } else if (action === 'auto-fields-dialog') {
      this.openAutoFieldsDialog()
    }
  }

  /**
   * Opens the auto fields selection dialog
   */
  openAutoFieldsDialog(): void {
    if (!this.domainElement) {
      return
    }

    openAutoFieldsDialog((selectedFields: string[]) => {
      if (!this.domainElement) {
        return
      }

      let lastField: { key: string; kind: string } | undefined

      // Add all selected fields
      for (const fieldId of selectedFields) {
        const field = addAutoField(this.domainElement, fieldId)
        lastField = { key: field.key, kind: field.kind }
      }

      // Select the last added field if any
      if (lastField) {
        this.select(lastField.key, lastField.kind)
      }

      this.notifyChange()
    })
  }

  protected async handleChooseParent(): Promise<void> {
    if (!this.domainElement) {
      return
    }
    const data: DomainBrowseInit = {
      reason: 'pick',
      domain: this.domainElement.domain,
      allowed: [DomainEntityKind],
      withRecent: true,
    }
    const result = await Events.Domain.Browse.browse(data, this)
    if (!result) {
      return
    }
    if (result.result === 'canceled') {
      return
    }
    this.addParentEntity(result.selected, result.domain)
  }

  protected handleDeleteParent(e: Event): void {
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    const item = e.currentTarget as HTMLElement
    const { key, domain } = item.dataset
    if (!key || !domain) {
      return
    }
    domainElement.removeParent(key, domain)
    this.notifyChange()
    this.requestUpdate()
  }

  protected handleNavigateParent(e: Event): void {
    const item = e.currentTarget as HTMLElement
    const { key } = item.dataset
    if (!key) {
      return
    }
    this.openEntity(key)
  }

  addParentEntity(key: string, domain?: string): void {
    const { domainElement } = this
    if (!domainElement || !key) {
      return
    }
    try {
      const entity = domainElement.domain.findEntity(key, domain)
      if (!entity) {
        throw new Error(`Entity with key ${key} not found in domain ${domainElement.domain.key}.`)
      }
      domainElement.addParent(key, domain)
      this.notifyChange()
      this.requestUpdate()
    } catch (error) {
      this.reportError(error as Error)
    }
  }

  protected handleInheritanceDragEnter(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt) {
      return
    }
    const hasJsonData = dt.types.includes('application/json')
    if (!hasJsonData) {
      return
    }
    const zone = e.currentTarget as HTMLElement
    zone.classList.add('drop-zone')
    dt.dropEffect = 'link'
    dt.effectAllowed = 'link'
  }

  protected handleInheritanceDragOver(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt) {
      return
    }
    const hasJsonData = dt.types.includes('application/json')
    if (!hasJsonData) {
      return
    }
    e.preventDefault()
  }

  protected handleInheritanceDragLeave(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt) {
      return
    }
    const zone = e.currentTarget as HTMLElement
    zone.classList.remove('drop-zone')
  }

  protected handleInheritanceDrop(e: DragEvent): void {
    const dt = e.dataTransfer
    if (!dt) {
      return
    }
    const zone = e.currentTarget as HTMLElement
    zone.classList.remove('drop-zone')
    const raw = dt.getData('application/json')
    const data = JSON.parse(raw) as { items: DomainSelectionInfo[] }
    const entities = data.items.filter((item) => item.kind === DomainEntityKind)
    if (entities.length === 0 || !this.domainElement) {
      return
    }
    for (const entity of entities) {
      this.domainElement.addParent(entity.key, entity.domain)
    }
    this.notifyChange()
    this.requestUpdate()
  }

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

  protected handleTaxonomyClear(e: Event): void {
    e.preventDefault()
    e.stopPropagation()
    const { domainElement } = this
    if (!domainElement) {
      return
    }
    domainElement.semantics = []
    this.requestUpdate()
  }

  protected handleFieldContextMenuSelect(e: CustomEvent<{ item: HTMLElement }>): void {
    const menu = e.currentTarget as HTMLElement
    const { key, kind } = menu.dataset
    if (!key || !kind) {
      return
    }
    const { item } = e.detail
    const { action = '' } = item.dataset
    if (action === 'edit') {
      this.select(key, kind)
    } else if (action === 'delete') {
      this.deleteField(key, kind)
    } else if (action === 'convert') {
      if (kind === DomainPropertyKind) {
        this.convertToAssociation(key)
      } else if (kind === DomainAssociationKind) {
        this.convertToProperty(key)
      }
    } else if (action === 'duplicate') {
      this.duplicateField(key, kind)
    }
  }

  /**
   * Duplicates a field (property or association).
   * Creates a copy with the same properties but a new unique name.
   * @param key The key of the field to duplicate.
   * @param kind The kind of the field to duplicate.
   */
  duplicateField(key: string, kind: string): void {
    const { root, domainElement } = this
    if (!root || !domainElement) {
      return
    }

    if (kind === DomainPropertyKind) {
      this.duplicateProperty(key)
    } else if (kind === DomainAssociationKind) {
      this.duplicateAssociation(key)
    }
  }

  /**
   * Duplicates a property field.
   * @param key The key of the property to duplicate.
   */
  protected duplicateProperty(key: string): void {
    const { root, domainElement } = this
    if (!root || !domainElement) {
      return
    }

    // Find the property to duplicate
    const property = root.findProperty(key, this.namespace)
    if (!property) {
      return
    }

    const originalIndex = domainElement.fields.findIndex((f) => f.key === key)
    if (originalIndex === -1) {
      return
    }

    // Generate a unique name for the duplicate
    const baseName = property.info.name || 'field'
    const newName = this.generateUniqueName(baseName)

    // Get the web binding for format information
    const webBinding = property.getWebBinding()

    // Create the duplicate with all the same properties
    const duplicateProperty = domainElement.addProperty({
      type: property.type,
      info: {
        name: newName,
        displayName: property.info.displayName,
        description: property.info.description,
      },
      required: property.required,
      multiple: property.multiple,
      primary: false, // Don't duplicate primary key
      index: property.index,
      readOnly: property.readOnly,
    })

    // Copy the web binding format if it exists
    if (webBinding.format) {
      const duplicateBinding = duplicateProperty.getWebBinding()
      duplicateBinding.format = webBinding.format
    }

    // Copy the schema if it exists
    if (property.schema) {
      const duplicateSchema = duplicateProperty.ensureSchema()
      const originalSchema = property.schema

      if (originalSchema.defaultValue) {
        duplicateSchema.defaultValue = { ...originalSchema.defaultValue }
      }
      if (originalSchema.enum) {
        duplicateSchema.enum = [...originalSchema.enum]
      }
      if (originalSchema.examples) {
        duplicateSchema.examples = [...originalSchema.examples]
      }
      if (originalSchema.minimum !== undefined) {
        duplicateSchema.minimum = originalSchema.minimum
      }
      if (originalSchema.maximum !== undefined) {
        duplicateSchema.maximum = originalSchema.maximum
      }
      if (originalSchema.exclusiveMinimum !== undefined) {
        duplicateSchema.exclusiveMinimum = originalSchema.exclusiveMinimum
      }
      if (originalSchema.exclusiveMaximum !== undefined) {
        duplicateSchema.exclusiveMaximum = originalSchema.exclusiveMaximum
      }
      if (originalSchema.multipleOf !== undefined) {
        duplicateSchema.multipleOf = originalSchema.multipleOf
      }
    }

    // Move the duplicate to be right after the original
    const fromIndex = domainElement.fields.length - 1
    const duplicateField = domainElement.fields[fromIndex]
    domainElement.fields.splice(fromIndex, 1)
    domainElement.fields.splice(originalIndex + 1, 0, duplicateField)

    // Select the new duplicate
    this.select(duplicateProperty.key, DomainPropertyKind)
    this.notifyChange()
  }

  /**
   * Duplicates an association field.
   * @param key The key of the association to duplicate.
   */
  private duplicateAssociation(key: string): void {
    const { root, domainElement } = this
    if (!root || !domainElement) {
      return
    }

    // Find the association to duplicate
    const association = root.findAssociation(key, this.namespace)
    if (!association) {
      return
    }

    const originalIndex = domainElement.fields.findIndex((f) => f.key === key)
    if (originalIndex === -1) {
      return
    }

    // Generate a unique name for the duplicate
    const baseName = association.info.name || 'reference'
    const newName = this.generateUniqueName(baseName)

    // Create the duplicate with all the same properties
    const duplicateAssociation = domainElement.addAssociation(
      {},
      {
        info: {
          name: newName,
          displayName: association.info.displayName,
          description: association.info.description,
        },
        required: association.required,
        multiple: association.multiple,
      }
    )

    // Copy the schema if it exists
    if (association.schema) {
      const duplicateSchema = duplicateAssociation.ensureSchema()
      const originalSchema = association.schema

      if (originalSchema.linked !== undefined) {
        duplicateSchema.linked = originalSchema.linked
      }
      if (originalSchema.unionType) {
        duplicateSchema.unionType = originalSchema.unionType
      }
    }

    // Copy the target entities
    for (const target of association.listTargets()) {
      duplicateAssociation.addTarget(target.key, target.domain.key)
    }

    // Move the duplicate to be right after the original
    const fromIndex = domainElement.fields.length - 1
    const duplicateField = domainElement.fields[fromIndex]
    domainElement.fields.splice(fromIndex, 1)
    domainElement.fields.splice(originalIndex + 1, 0, duplicateField)

    // Select the new duplicate
    this.select(duplicateAssociation.key, DomainAssociationKind)
    this.notifyChange()
  }

  /**
   * Generates a unique name by appending a number to the base name.
   * @param baseName The base name to make unique.
   * @returns A unique name within the current entity.
   */
  protected generateUniqueName(baseName: string): string {
    const { domainElement } = this
    if (!domainElement) {
      return baseName
    }

    const existingNames = new Set<string>()
    for (const field of domainElement.listFields()) {
      if (field.info.name) {
        existingNames.add(field.info.name)
      }
    }

    let counter = 2
    let newName = `${baseName}_copy`
    while (existingNames.has(newName)) {
      newName = `${baseName}_copy_${counter}`
      counter++
    }

    return newName
  }

  deleteField(key: string, kind: string): void {
    const { root } = this
    if (!root) {
      return
    }
    if (kind === DomainPropertyKind) {
      root.removeProperty(key)
    } else if (kind === DomainAssociationKind) {
      root.removeAssociation(key)
    } else {
      return
    }
    this.dispatchEvent(new CustomEvent('delete', { detail: { key, kind } }))
  }

  openEntity(key: string): void {
    const detail: DomainSelectionInfo = {
      key,
      kind: DomainEntityKind,
      domain: this.root?.key || '',
    }
    this.dispatchEvent(new CustomEvent('openentity', { detail }))
  }

  /**
   * Converts a property to an association.
   * Keeps as many properties as possible.
   * @param key The key of the property to convert.
   */
  convertToAssociation(key: string): void {
    const { root, domainElement } = this
    if (!root || !domainElement) {
      return
    }

    // Find the property to convert
    const property = root.findProperty(key, this.namespace)
    if (!property) {
      return
    }
    const toIndex = domainElement.fields.findIndex((f) => f.key === key)
    if (toIndex === -1) {
      return
    }

    // Store the property information before removal
    const info = {
      name: property.info.name,
      displayName: property.info.displayName,
      description: property.info.description,
    }
    const required = property.required
    const multiple = property.multiple

    // Remove the original property
    root.removeProperty(key)

    // Create a new association with preserved information
    const association = domainElement.addAssociation({}, { info, required, multiple })
    const fromIndex = domainElement.fields.length - 1
    const value = domainElement.fields[fromIndex]
    // Move the association to the same position as the original property
    domainElement.fields.splice(fromIndex, 1)
    domainElement.fields.splice(toIndex, 0, value)

    // Select the new association
    this.select(association.key, DomainAssociationKind)
    this.notifyChange()
  }

  /**
   * Converts an association to a property.
   * Keeps as many properties as possible.
   * @param key The key of the association to convert.
   */
  convertToProperty(key: string): void {
    const { root, domainElement } = this
    if (!root || !domainElement) {
      return
    }

    // Find the association to convert
    const association = root.findAssociation(key, this.namespace)
    if (!association) {
      return
    }

    const toIndex = domainElement.fields.findIndex((f) => f.key === key)
    if (toIndex === -1) {
      return
    }

    // Store the association information before removal
    const info = {
      name: association.info.name,
      displayName: association.info.displayName,
      description: association.info.description,
    }
    const required = association.required
    const multiple = association.multiple

    // Remove the original association
    root.removeAssociation(key)

    // Create a new property with preserved information
    // Default to string type since we can't preserve the target reference as a data type
    const property = domainElement.addProperty({
      type: 'string',
      info,
      required,
      multiple,
    })
    const fromIndex = domainElement.fields.length - 1
    const value = domainElement.fields[fromIndex]
    // Move the property to the same position as the original association
    domainElement.fields.splice(fromIndex, 1)
    domainElement.fields.splice(toIndex, 0, value)

    // Select the new property
    this.select(property.key, DomainPropertyKind)
    this.notifyChange()
  }

  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 handleDeleteTaxonomy(e: Event): void {
    const button = e.currentTarget as HTMLButtonElement
    const { id } = button.dataset
    if (!id) {
      return
    }
    this.domainElement?.removeSemantic(id as SemanticType)
  }

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

  protected renderForm(model: DomainEntity): TemplateResult {
    return html`
      <form name="entity" @submit="${cancelEvent}" data-key="${model.key}" class="modeling-form">
        ${this.renderMetaInputs(model)} ${this.renderParentEntity(model)} ${this.renderContentTabs()}
        ${this.renderTabsContent(model)}
      </form>
    `
  }

  protected renderMetaInputs(model: DomainEntity): 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="31"
            pattern="[a-zA-Z_][a-zA-Z0-9_]+"
            .invalidText="${formatError}"
            ?disabled="${disabled}"
            @change="${this.handleMetaChange}"
          ></ui-outlined-text-field>
          ${renderHelp('entity', '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('entity', '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('entity', 'description-input')}
        </div>
      </div>
    `
  }

  protected renderParentEntity(model: DomainEntity): TemplateResult {
    const disabled = !this.canEdit
    return html`
      <div
        class="inheritance-section"
        @dragenter="${this.handleInheritanceDragEnter}"
        @dragover="${this.handleInheritanceDragOver}"
        @dragleave="${this.handleInheritanceDragLeave}"
        @drop="${this.handleInheritanceDrop}"
      >
        <div class="section-header">
          <span class="label-large">Inherits from</span>
          <ui-button
            color="tonal"
            type="button"
            class="select"
            @click="${this.handleChooseParent}"
            ?disabled="${disabled}"
            >Choose parent entity</ui-button
          >
          ${renderHelp('entity', 'inherits-from-selector')}
        </div>
        ${this.renderParentItems(model)}
      </div>
    `
  }

  protected renderParentItems(model: DomainEntity): TemplateResult | typeof nothing {
    if (!model.hasParents()) {
      return nothing
    }
    const contents: TemplateResult[] = []
    const disabled = !this.canEdit
    for (const item of model.listParents()) {
      const label = item.info.getLabel()
      contents.push(
        html`<ui-chip
          class="parent-chip"
          data-key="${item.key}"
          data-domain="${item.domain.key}"
          removable
          type="input"
          title="Open the parent entity"
          aria-label="Open the parent entity ${label}"
          ?disabled="${disabled}"
          @remove="${this.handleDeleteParent}"
          @click="${this.handleNavigateParent}"
          >${label}</ui-chip
        >`
      )
    }
    return html`<ui-chip-set>${contents}</ui-chip-set>`
  }

  protected renderContentTabs(): TemplateResult {
    return html`
      <ui-tabs priority="secondary" @change="${this.handleContentTabChange}">
        <ui-tab data-key="design" selected>Design</ui-tab>
        <ui-tab data-key="preview">Preview</ui-tab>
      </ui-tabs>
    `
  }

  protected renderTabsContent(model: DomainEntity): TemplateResult {
    const { contentTab } = this
    switch (contentTab) {
      case 'design':
        return this.renderDesignTabContent(model)
      case 'preview':
        return this.renderPreviewTabContent()
      default:
        return html`<p>Invalid selection.</p>`
    }
  }

  protected renderDesignTabContent(model: DomainEntity): TemplateResult {
    // TODO: Add taxonomy related UI.
    return html`${this.renderFieldsSection(model)} ${this.renderDataGovernanceSection(model)}`
  }

  protected renderPreviewTabContent(): TemplateResult {
    return html`
      <div class="preview-section">
        ${this.renderPreviewMimeSelector()}
        <div class="schema-preview"><pre class="title-small">${this.schemaPreview}</pre></div>
      </div>
    `
  }

  protected renderPreviewMimeSelector(): TemplateResult {
    const { mime } = this
    const mimes = ['application/json', 'application/xml']
    return html`<div class="row">
      <ui-select label="Media Type" class="input" name="mime" .value="${mime}" @change="${this.handleMimeChange}">
        ${mimes.map(
          (item) =>
            html`<ui-option value="${item}">
              <div>${item}</div>
            </ui-option>`
        )}
      </ui-select>
    </div>`
  }

  protected renderFieldsSection(model: DomainEntity): TemplateResult {
    return html`
      <div class="fields-section">
        <div class="fields-header">
          <span class="label-large">Fields</span>
          ${this.renderFieldAddButton()}
        </div>
        ${model.hasFields() ? this.renderFieldsList(model) : this.renderFieldsEmptyState()}
      </div>
    `
  }

  protected renderFieldAddButton(): TemplateResult {
    const disabled = !this.canEdit
    return html`<div class="field-add-button">
      <ui-button
        id="field-new-anchor"
        trailingIcon
        type="button"
        color="tonal"
        ?disabled="${disabled}"
        popoverTarget="new-field-menu"
      >
        Add field
        <ui-icon slot="icon">arrow_drop_down</ui-icon>
      </ui-button>
      <ui-menu id="new-field-menu" popover="auto" @select="${this.handleFieldMenuSelect}">
        <ui-menu-item data-action="property">
          <div>Property</div>
          <div slot="supporting-text">Data property in the schema</div>
        </ui-menu-item>
        <ui-menu-item data-action="association">
          <div>Reference</div>
          <div slot="supporting-text">Connect to another entity object</div>
        </ui-menu-item>
        <ui-menu-item data-action="auto-fields-dialog">
          <div>Auto fields...</div>
          <div slot="supporting-text">Choose from common field types</div>
        </ui-menu-item>
        <ui-menu-item data-action="all-recommended">
          <div>All Recommended Fields</div>
          <div slot="supporting-text">Adds all commonly recommended fields (ID, timestamps, audit trail).</div>
        </ui-menu-item>
      </ui-menu>
    </div>`
  }

  protected renderFieldsEmptyState(): TemplateResult {
    return html`<div class="empty-fields">
      <div class="icon-wrapper">
        <ui-icon class="icon" icon="textFields"></ui-icon>
      </div>
      <p class="empty-message body-large">Find your data entities here</p>
      <p class="empty-message body-large">Add a data entity to describe your schema.</p>
    </div>`
  }

  protected renderFieldsList(items: DomainEntity): TemplateResult {
    const contents: TemplateResult[] = []
    for (const item of items.listFields()) {
      contents.push(this.renderField(item))
    }
    return html`<ui-list class="fields-list" @select="${this.handleFieldListSelect}">${contents}</ui-list>`
  }

  protected renderField(item: DomainAssociation | DomainProperty): TemplateResult {
    const selected = this.selectedProperty === item.key
    const classes: ClassInfo = {
      'field-item': true,
      'selected': selected,
    }
    const label = item.info.getLabel()
    const icon = selected ? 'check' : getFieldIcon(item)
    return html`<ui-list-item class="${classMap(classes)}" data-key="${item.key}" data-kind="${item.kind}">
      <span slot="start"><ui-icon class="icon" icon="${icon}"></ui-icon></span>
      <span>${label}</span>
      <span slot="end-text">${getFieldDataTypeLabel(item)}</span>
      ${this.createFieldContextualMenu(item)}
    </ui-list-item>`
  }

  protected createFieldContextualMenu(item: DomainAssociation | DomainProperty): TemplateResult {
    const disabled = !this.canEdit
    return html`
      <div
        class="field-menu"
        slot="end"
        @click="${cancelEvent}"
        @keydown="${cancelEvent}"
        @keyup="${cancelEvent}"
        @mousedown="${cancelEvent}"
        @mouseup="${cancelEvent}"
        @pointerdown="${cancelEvent}"
        @pointerup="${cancelEvent}"
        @contextmenu="${cancelEvent}"
        @touchstart="${cancelEvent}"
        @touchend="${cancelEvent}"
        @touchcancel="${cancelEvent}"
      >
        <ui-icon-button
          id="field-menu-anchor-${item.key}-menu"
          popoverTarget="field-${item.key}-menu"
          color="outlined"
          aria-label="Field options"
          ?disabled="${!this.canEdit}"
        >
          <ui-icon>more_vert</ui-icon>
        </ui-icon-button>
        <ui-menu
          data-key="${item.key}"
          data-kind="${item.kind}"
          id="field-${item.key}-menu"
          popover="auto"
          @select="${this.handleFieldContextMenuSelect}"
        >
          <ui-menu-item data-action="edit" ?disabled="${disabled}" lines="two">
            <div>Edit</div>
            <div slot="supporting-text">Edit the field properties</div>
          </ui-menu-item>
          <ui-menu-item data-action="duplicate" ?disabled="${disabled}" lines="two">
            <div>Duplicate</div>
            <div slot="supporting-text">Create a copy of this field</div>
          </ui-menu-item>
          ${when(
            item.kind === DomainPropertyKind,
            () =>
              html`<ui-menu-item data-action="convert" ?disabled="${disabled}" lines="two">
                <div>Convert to association</div>
                <div slot="supporting-text">Replaces this property with an association</div>
              </ui-menu-item>`,
            () =>
              html`<ui-menu-item data-action="convert" ?disabled="${disabled}" lines="two">
                <div>Convert to property</div>
                <div slot="supporting-text">Replaces this association with a property</div>
              </ui-menu-item>`
          )}
          <ui-divider></ui-divider>
          <ui-menu-item data-action="delete" ?disabled="${disabled}" lines="two">
            <div>Delete</div>
            <div slot="supporting-text">Remove the field from the schema</div>
          </ui-menu-item>
        </ui-menu>
      </div>
    `
  }

  protected renderDataGovernanceSection(model: DomainEntity): TemplateResult {
    // TODO: Add classification UI (data tagging)
    return html`<div class="data-gov-section">
      <div class="data-gov-header">
        <span class="label-large">Data Governance</span>
      </div>
      ${this.renderTaxonomyInput(model)}
    </div>`
  }

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

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

      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 entity 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('entity', 'taxonomy-select')}
      </div>
      ${this.renderSelectedSemantics(selectedSemantics)}`

    // const options: TemplateResult[] = []
    // let hasSelection = false
    // for (const semantic of Object.values(DataSemantics)) {
    //   if (semantic.scope !== SemanticScope.Entity) {
    //     continue
    //   }
    //   const has = model.semantics.some((s) => s.id === semantic.id)
    //   if (has) {
    //     hasSelection = true
    //   }
    //   options.push(
    //     html`<ui-option value="${semantic.id}" ?selected="${has}">
    //       <div>${semantic.displayName}</div>
    //       <div slot="supporting-text">${semantic.description}</div>
    //     </ui-option>`
    //   )
    // }
    // const support = 'Helps recognize what the entity represents'
    // return html`<div class="row input-row">
    //   <ui-select
    //     label="Entity taxonomy"
    //     class="input"
    //     name="taxonomy"
    //     ?disabled="${!this.canEdit}"
    //     .supportingText="${support}"
    //     @change="${this.handleTaxonomyChange}"
    //   >
    //     ${options}
    //     ${hasSelection
    //       ? html`<ui-icon slot="trailing-icon" icon="cancel" @click="${this.handleTaxonomyClear}"></ui-icon>`
    //       : nothing}
    //   </ui-select>
    //   ${renderHelp('entity', 'taxonomy-select')}
    // </div>`
  }

  protected renderSelectedSemantics(selected: EntitySemantic[]): 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: EntitySemantic): 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}" ?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, EntitySemantic[]>): 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
  }
}
