import { html, nothing, type PropertyValues, type TemplateResult } from 'lit'
import { property, state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { classMap } from 'lit/directives/class-map.js'
import { ModelingElement } from './ModelingElement.js'
import type {
  DomainBrowseRecentItem,
  DomainBrowserPickItem,
  DomainBrowserPickReason,
  DomainBrowseSelectedItem,
} from '../types.js'
import {
  DomainEntityKind,
  DomainFileKind,
  DomainModelKind,
  DomainNamespaceKind,
  type DomainEntity,
  type DomainModel,
  type DomainNamespace,
} from '@api-client/core/browser.js'
import { Events } from '../../events/Events.js'
import { getDomainIconType } from '../lib/ui_mapping.js'
import type { TabSelectionDetail } from '../../md/tabs/ui-tabs.js'

interface DomainPartition {
  /**
   * The list of currently rendered items.
   */
  items: (DomainNamespace | DomainModel | DomainEntity)[]
  /**
   * The currently selected item.
   */
  selected?: DomainBrowseSelectedItem
  /**
   * The parent object, if any
   */
  parent?: DomainNamespace | DomainModel
}

interface RecentPartition {
  /**
   * The list of recently picked items.
   */
  currentList: DomainBrowseRecentItem[]
  /**
   * The currently selected item.
   */
  selected?: DomainBrowseSelectedItem
}

/**
 * An element that provides browsing capabilities for a data domain.
 *
 * @fires select - Fired when a domain item is selected or deselected.
 *   The detail contains `DomainBrowseSelectedItem` with the selected item's key, kind,
 *   namespace, and whether it's a parent (can be opened), or `undefined` if deselected.
 */
export default class DomainBrowser extends ModelingElement {
  /**
   * Whether to include recent items in the browsing.
   */
  @property({ type: Boolean }) accessor withRecent = false
  /**
   * The reason to browse the domain.
   */
  @property({ type: String }) accessor reason: DomainBrowserPickReason = 'pick'
  /**
   * Specifies which items are allowed to select in the UI to return the result
   * back to the application. By default all data domain items are allowed.
   */
  @property({ type: Array }) accessor allowed: DomainBrowserPickItem[] | undefined
  /**
   * The currently selected tab.
   * @default 'domain'
   */
  @state() accessor selectedTab: 'recent' | 'domain' = 'domain'
  /**
   * Whether there is an allowed selection.
   */
  @state() accessor hasAllowedSelection = false

  /**
   * The recent browsing partition state.
   */
  recent: RecentPartition = {
    currentList: [],
  }

  /**
   * The current data domain partition state.
   */
  current?: DomainPartition

  /**
   * Returns the active browsing partition based on the selected tab.
   */
  get activePartition(): DomainPartition | RecentPartition {
    if (this.selectedTab === 'recent') {
      return this.recent
    }
    if (!this.current) {
      throw new Error('Browsing domain partition is not set.')
    }
    return this.current
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    super.willUpdate(cp)
    if (cp.has('root')) {
      this.setupCurrentPartition()
      this.setupRecent()
    }
    if (cp.has('withRecent')) {
      this.setupRecent()
    }
  }

  protected setupCurrentPartition(): void {
    const { root } = this
    if (!root) {
      this.current = undefined
      this.hasAllowedSelection = false
      return
    }
    this.current = {
      items: [],
    }
    this.setupAllowedSelection(this.current)
    this.updateBrowsingDomainList()
  }

  /**
   * Sets up whether a selection is allowed based on the current state.
   * @param partition - The partition to check.
   */
  protected setupAllowedSelection(partition: DomainPartition | RecentPartition): void {
    const { allowed = [] } = this
    const { selected } = partition
    if (!Array.isArray(allowed) || allowed.length === 0) {
      if (selected) {
        this.hasAllowedSelection = true
      }
      return
    }
    if (!selected) {
      this.hasAllowedSelection = false
      return
    }
    const { kind } = selected
    if (!kind) {
      this.hasAllowedSelection = false
      return
    }
    this.hasAllowedSelection = allowed.some((item) => {
      if (item === DomainEntityKind) {
        return kind === DomainEntityKind
      }
      if (item === DomainModelKind) {
        return kind === DomainModelKind
      }
      if (item === DomainNamespaceKind) {
        return kind === DomainNamespaceKind || kind === DomainFileKind
      }
      return false
    })
  }

  /**
   * Updates the current list of items in a browsing domain partition.
   * @param partition - The partition to update.
   */
  protected updateBrowsingDomainList(): void {
    const { root, current } = this
    if (!current || !root) {
      return
    }
    current.items = []
    const { parent } = current
    if (!parent) {
      current.items = [...root.listNamespaces(), ...root.listModels()]
      if (root.dependencies.size > 0) {
        for (const foreign of root.dependencies.values()) {
          current.items.push(...root.listForeignNamespaces(foreign.key))
          current.items.push(...root.listForeignModels(foreign.key))
        }
      }
    } else if (parent.kind === DomainModelKind) {
      const model = root.findModel(parent.key, parent.namespace)
      if (model) {
        current.items = [...model.listEntities()]
      }
    } else if (parent.kind === DomainNamespaceKind) {
      const namespace = root.findNamespace(parent.key, parent.namespace)
      if (namespace) {
        current.items = [...namespace.listNamespaces(), ...namespace.listModels()]
      }
    }
    this.requestUpdate()
  }

  /**
   * Requests the recent items from the context provider if `withRecent` is enabled.
   */
  protected async setupRecent(): Promise<void> {
    if (!this.withRecent) {
      this.recent.currentList = []
      if (this.selectedTab === 'recent') {
        this.selectedTab = 'domain'
      }
      return
    }
    const { root } = this
    if (!root) {
      this.recent.currentList = []
      return
    }
    const result = await Events.Domain.Browse.recent(root.key, this.allowed, this)
    this.recent.currentList = result ?? []
  }

  /**
   * Checks if an item is selected.
   * @param key The key of the item to check.
   * @returns True if the item is selected, false otherwise.
   */
  isSelected(key: string): boolean {
    const browsing = this.activePartition
    if (!browsing.selected) {
      return false
    }
    return browsing.selected.key === key
  }

  /**
   * Handles the tab selection event.
   * @param e - The tab selection event.
   */
  protected handleTabSelect(e: CustomEvent<TabSelectionDetail>): void {
    const { item } = e.detail
    const { key } = item.dataset
    if (!key) {
      return
    }
    this.selectedTab = key as 'recent' | 'domain'
    this.requestUpdate()
  }

  /**
   * Handles the click event to go back to the parent.
   */
  protected handleParentBack(): void {
    const source = this.current
    if (!source || !source.parent) {
      return
    }
    if (this.selectedTab === 'domain') {
      const domainElement = source.parent as DomainNamespace | DomainModel
      const ns = domainElement.getParentInstance()
      if (!ns || ns.key === ns.domain.key) {
        source.parent = undefined
      } else {
        this.selectParent(source, ns.key, ns.kind, ns.domain.key)
      }
      this.updateBrowsingDomainList()
    }
  }

  /**
   * Selects a parent item.
   *
   * @param partition - The partition to select the parent in.
   * @param key - The key of the parent item.
   * @param kind - The kind of the parent item.
   * @throws {Error} If the root is not set or the item is not found.
   */
  selectParent(partition: DomainPartition, key: string, kind: string, namespace?: string): void {
    const { root } = this
    if (!root) {
      throw new Error(`Unable to select parent. The root is not set.`)
    }
    if (kind === DomainModelKind) {
      partition.parent = root.findModel(key, namespace)
    } else if (kind === DomainNamespaceKind) {
      partition.parent = root.findNamespace(key, namespace)
    }
    if (!partition.parent) {
      throw new Error(`Unable to select parent. The item with key "${key}" was not found.`)
    }
  }

  /**
   * Handles the click event to open a parent item.
   *
   * @param e - The click event.
   */
  protected handleOpenParent(e: Event): void {
    e.preventDefault()
    e.stopPropagation()
    const button = e.currentTarget as HTMLElement
    const item = button.parentElement as HTMLElement
    const { kind, key, parent, namespace } = item.dataset
    if (!key || !kind || !parent || parent !== 'true' || !this.current) {
      return
    }
    this.selectParent(this.current, key, kind, namespace)
    this.updateBrowsingDomainList()
  }

  /**
   * Handles the selection event on a list.
   * @param e The selection event.
   */
  protected handleItemSelect(e: CustomEvent<{ item: HTMLElement }>): void {
    const { item } = e.detail
    if (!item) {
      return
    }
    const { kind, key, parent, namespace } = item.dataset
    if (!key || !kind || !parent || !namespace) {
      return
    }
    const state = this.activePartition
    if (this.isSelected(key)) {
      state.selected = undefined
    } else {
      state.selected = {
        key,
        kind,
        isParent: parent === 'true',
        namespace,
      }
    }
    this.setupAllowedSelection(state)
    this.requestUpdate()
    if (this.hasAllowedSelection) {
      // We only notify the selection if it is allowed.
      this.notifySelected(state.selected)
    }
  }

  notifySelected(selected: DomainBrowseSelectedItem | undefined): void {
    this.dispatchEvent(
      new CustomEvent<DomainBrowseSelectedItem>('select', { detail: selected, bubbles: false, composed: false })
    )
  }

  override render(): TemplateResult | typeof nothing {
    if (!this.root) {
      return nothing
    }
    const result: (TemplateResult | typeof nothing)[] = []
    result.push(this.renderHeader())
    if (this.selectedTab === 'recent') {
      result.push(this.renderRecent())
    } else if (this.selectedTab === 'domain') {
      result.push(this.renderBrowsingDomain())
    }
    return html`${result}`
  }

  /**
   * Renders the header of the dialog.
   * @private
   */
  protected renderHeader(): TemplateResult | typeof nothing {
    if (this.selectedTab === 'recent') {
      return this.renderTabs()
    }
    const activePartition = this.current
    if (!activePartition) {
      return nothing
    }
    const { parent } = activePartition
    if (parent) {
      return this.renderParentHeader(parent)
    }
    return this.renderTabs()
  }

  /**
   * Renders the tabs.
   * @private
   */
  protected renderTabs(): TemplateResult {
    const { withRecent, selectedTab, root } = this
    return html`<ui-tabs @change="${this.handleTabSelect}" class="tabs">
      ${withRecent
        ? html`<ui-tab aria-controls="recent" data-key="recent" ?selected="${selectedTab === 'recent'}">Recent</ui-tab>`
        : nothing}
      ${root?.key
        ? html`<ui-tab aria-controls="domain" data-key="domain" ?selected="${selectedTab === 'domain'}"
            >Data domain</ui-tab
          >`
        : nothing}
    </ui-tabs>`
  }

  /**
   * Renders the parent header.
   * @private
   * @param parent - The parent item.
   */
  protected renderParentHeader(parent: DomainNamespace | DomainModel): TemplateResult {
    const label = parent.info.displayName || parent.info.name
    return html`<div class="parent-header">
        <ui-icon-button @click="${this.handleParentBack}"><ui-icon>arrow_back</ui-icon></ui-icon-button>
        <h2 class="title-medium">${label}</h2>
      </div>
      <ui-divider></ui-divider>`
  }

  /**
   * Renders the recent items.
   * @private
   */
  protected renderRecent(): TemplateResult {
    const { recent } = this
    return html`<div id="recent">${this.renderRecentItems(recent.currentList)}</div>`
  }

  /**
   * Renders the browsing domain items.
   * @private
   */
  protected renderBrowsingDomain(): TemplateResult | typeof nothing {
    const { current } = this
    if (!current) {
      return nothing
    }
    return html`<div id="domain">${this.renderDomainItems(current.items)}</div>`
  }

  /**
   * Renders the domain items.
   * @private
   * @param items - The items to render.
   */
  protected renderDomainItems(items: (DomainNamespace | DomainModel | DomainEntity)[]): TemplateResult {
    const empty = items.length === 0
    if (empty) {
      return html`<p class="empty-list body-medium">No items found.</p>`
    }
    return html`<ui-list selectActive class="domain-list" @select="${this.handleItemSelect}">
      ${items.map((item) => this.renderDomainItem(item))}
    </ui-list>`
  }

  protected renderRecentItems(items: DomainBrowseRecentItem[]): TemplateResult {
    const empty = items.length === 0
    if (empty) {
      return html`<p class="empty-list body-medium">No items found.</p>`
    }
    return html`<ui-list selectActive class="domain-list" @select="${this.handleItemSelect}">
      ${items.map((item) => this.renderRecentItem(item))}
    </ui-list>`
  }

  protected renderDomainItem(item: DomainNamespace | DomainModel | DomainEntity): TemplateResult {
    const isParent = [DomainModelKind, DomainNamespaceKind].includes(item.kind)
    return this.renderItem(item.key, item.kind, item.info.getLabel(), { isParent, domain: item.namespace })
  }

  protected renderRecentItem(item: DomainBrowseRecentItem): TemplateResult {
    const { key, kind, label } = item
    return this.renderItem(key, kind, label, { domain: this.root?.key })
  }

  /**
   * Renders a generic item.
   * @private
   * @param key - The key of the item.
   * @param kind - The kind of the item.
   * @param label - The label of the item.
   * @param {{ noParent?: boolean }} opts - Additional options.
   */
  protected renderItem(
    key: string,
    kind: string,
    label: string,
    opts: { isParent?: boolean; domain?: string } = {}
  ): TemplateResult {
    const isSelected = this.isSelected(key)
    const parentValue = opts.isParent ? 'true' : 'false'
    const classes = {
      select: isSelected,
    }
    const icon = getDomainIconType(kind)
    return html`<ui-list-item
      data-kind="${kind}"
      data-key="${key}"
      data-parent="${parentValue}"
      data-namespace="${ifDefined(opts.domain)}"
      class="${classMap(classes)}"
    >
      <ui-icon slot="start" class="icon" icon="${icon}"></ui-icon>
      <span>${label}</span>
      ${isSelected && opts.isParent
        ? html`<ui-button color="filled" slot="end" @click="${this.handleOpenParent}">Open</ui-button>`
        : nothing}
    </ui-list-item>`
  }
}
