import { TaxonomyDialog } from './TaxonomyDialog.js'
import type { DomainProperty } from '@api-client/core/modeling/DomainProperty.js'
import { SemanticType } from '@api-client/core/modeling/Semantics.js'
import { bound } from '../../decorators/bound.js'
import { html, nothing, type TemplateResult } from 'lit'
import { renderHelp } from '../../help/render.js'
import CodeEditorElement, { FunctionSchema, type Suggestion } from '../../elements/code-editor/code-editor.js'
import { type CalculatedConfig, DEFAULT_CALCULATED_CONFIG } from '@api-client/core/modeling/definitions/Calculated.js'
import type { DomainPropertyType } from '@api-client/core/modeling/DataFormat.js'
import { FormulaParser, FormulaTestRunResult } from '../lib/formula_parser.js'
import type { ValidationResult } from '@pawel-up/jexl'
import { stringLibrarySchema } from '@pawel-up/jexl/schemas/string.schema.js'
import { mathLibrarySchema } from '@pawel-up/jexl/schemas/math.schema.js'
import { dateLibrarySchema } from '@pawel-up/jexl/schemas/date.schema.js'
import { arrayLibrarySchema } from '@pawel-up/jexl/schemas/array.schema.js'
import type { HelpRequestDetail } from '../../help/help_coordinator.js'

import '../../elements/code-editor/code-editor.js'

const supportedTypes: DomainPropertyType[] = ['string', 'number', 'boolean', 'date']

/**
 * Dialog for configuring a calculated field semantic on a domain property.
 * It is a formula field that computes its value based on other properties.
 */
export class CalculatedTaxonomy extends TaxonomyDialog<CalculatedConfig> {
  static override title = `Configure Calculated Field`

  /**
   * The list of properties available for mentions.
   */
  suggestions: Suggestion[] = []

  /**
   * The list of properties that can be used in the formula.
   */
  properties: DomainProperty[] = []

  /**
   * The data type of the property.
   * Depending on the data type, the formula field offers different capabilities.
   */
  dataType: DomainPropertyType = 'string'

  /**
   * The latest validation result of the formula.
   */
  validation?: ValidationResult

  /**
   * The result of the formula test run.
   * This is used to display the result of the formula when testing it.
   */
  renderValue: FormulaTestRunResult | null = null

  functions: FunctionSchema[] = []

  constructor(public domainElement: DomainProperty) {
    let config: CalculatedConfig
    const semantic = domainElement.semantics.find((s) => s.id === SemanticType.Calculated)
    if (semantic && semantic.config) {
      config = structuredClone(semantic.config) as CalculatedConfig
    } else {
      config = {} as CalculatedConfig
    }
    super(config)
    this.dataType = domainElement.type
    const entity = domainElement.getParentInstance()
    if (entity) {
      const props = this.listComputedProperties(entity, {
        exclude: [this.domainElement.key],
      })
      this.properties = props
      for (const prop of props) {
        const suggestion: Suggestion = {
          id: prop.key,
          label: prop.info.getLabel(),
          description: prop.info.description,
          suffix: prop.type,
        }
        this.suggestions.push(suggestion)
      }
    }
    this.functions = this.createFunctionSchema()
  }

  /**
   * Creates a schema for the code-editor functions.
   * This method is responsible for defining the functions that can be used in the formula,
   * given the current data type.
   */
  createFunctionSchema(): FunctionSchema[] {
    const result: FunctionSchema[] = []

    for (const func of Object.values(stringLibrarySchema.functions)) {
      result.push(func)
    }
    for (const func of Object.values(mathLibrarySchema.functions)) {
      result.push(func)
    }
    for (const func of Object.values(dateLibrarySchema.functions)) {
      result.push(func)
    }
    for (const func of Object.values(arrayLibrarySchema.functions)) {
      result.push(func)
    }
    return result
  }

  protected override formCallback(event: Event): Promise<void> {
    // Extract dependencies from the formula using the parser
    const parser = new FormulaParser(this.config.formula || '', this.domainElement, this.properties)
    const dependencies = parser.extractDependencies()

    if (dependencies && dependencies.length > 0) {
      this.config.dependencies = dependencies
    } else {
      delete this.config.dependencies
    }

    const semantic = this.domainElement.semantics.find((s) => s.id === SemanticType.Calculated)
    if (semantic) {
      semantic.config = this.config
    } else {
      this.domainElement.semantics.push({
        id: SemanticType.Calculated,
        config: this.config,
      })
    }
    this.domainElement.domain.notifyChange()
    return super.formCallback(event)
  }

  @bound
  protected handleFormulaChange(e: Event): void {
    const input = e.target as CodeEditorElement
    this.config.formula = input.value
    this.validate(input.value)
  }

  @bound
  protected handleShowDocumentation(e: CustomEvent<{ functionSchema: FunctionSchema }>): void {
    const schema = e.detail.functionSchema
    const id = schema.category + ':' + schema.name

    const detail: HelpRequestDetail = {
      category: 'function',
      id,
      target: e.target as HTMLElement,
    }
    document.dispatchEvent(
      new CustomEvent('request-help', {
        detail,
        bubbles: true,
        composed: true,
      })
    )
  }

  validate(input: string): void {
    const parser = new FormulaParser(input, this.domainElement, this.properties)
    this.validation = parser.validate()
    this.requestUpdate()
  }

  @bound
  handleTestClick(): void {
    this.testFormula()
  }

  async testFormula(): Promise<void> {
    const parser = new FormulaParser(this.config.formula ?? '', this.domainElement, this.properties)
    const dependencies = parser.extractDependencies()
    this.renderValue = await parser.test(dependencies)
    this.requestUpdate()
  }

  @bound
  render(): TemplateResult {
    const { dataType } = this
    if (!supportedTypes.includes(dataType)) {
      return html`<p class="body-medium">Calculated fields are not supported for ${dataType} data type.</p>`
    }
    return this.renderEditor()
  }

  protected renderEditor(): TemplateResult {
    return html`
      ${this.renderIntro()}
      <div class="input-group">${this.renderFormulaInput()}</div>
      ${this.renderPreview()} ${this.renderValidationMessage()}
      <div class="checkbox-group">${this.renderRecalculateOnUpdate()} ${this.renderAllowManualOverride()}</div>
    `
  }

  protected renderIntro(): TemplateResult {
    return html`<span class="body-medium">
      Define a formula to compute the value of this property. You can use mathematical operators, built-in functions,
      and reference other properties from this entity by typing <code>{{field_name}}</code>.
    </span>`
  }

  protected renderFormulaInput(): TemplateResult {
    const { formula = '' } = this.config
    let invalid: boolean
    if (!formula || formula.trim() === '') {
      invalid = false
    } else if (this.validation && !this.validation.valid) {
      invalid = true
    } else {
      invalid = false
    }
    return html`
      <div class="row code-editor">
        <code-editor
          class="input"
          label="Formula"
          .suggestions="${this.suggestions}"
          .functionSchemas="${this.functions}"
          .value="${formula}"
          @change="${this.handleFormulaChange}"
          .invalid="${invalid}"
          showDocumentation
          @docsrequest="${this.handleShowDocumentation}"
        ></code-editor>
        ${renderHelp('taxonomy', 'calculated:formula-input')}
      </div>
    `
  }

  protected renderPreview(): TemplateResult | typeof nothing {
    if (this.validation && !this.validation.valid) return nothing
    if (this.validation?.errors.length) return nothing
    return html`<div class="row">
      <ui-button color="outlined" @click="${this.handleTestClick}">Test</ui-button>
      ${this.renderValue
        ? html`
            <div class="test-result body-medium">
              <strong>Test Result:</strong>
              ${this.renderValue.error
                ? html`<span class="error-message">Error: ${this.renderValue.message}</span>`
                : html`<span class="generated-value">${this.renderValue.value}</span>`}
            </div>
          `
        : nothing}
    </div>`
  }

  protected renderValidationMessage(): TemplateResult | typeof nothing {
    if (!this.validation || this.validation.valid) return nothing
    const errors = this.validation.errors || []
    if (errors.length === 0) return nothing
    return html`
      <div class="validation-messages">
        ${errors.map((error) => html`<p class="body-small error-message">${error.message}</p>`)}
        <p class="body-small">
          You can use mathematical operators, built-in functions, and reference other properties from this entity by
          typing <code>{{field_name}}</code>.
        </p>
      </div>
    `
  }

  protected renderRecalculateOnUpdate(): TemplateResult {
    const enabled = this.config.recalculateOnUpdate ?? DEFAULT_CALCULATED_CONFIG.recalculateOnUpdate
    return html`
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="recalculate-on-update"
            name="recalculateOnUpdate"
            .checked="${enabled}"
            @change="${this.handleBooleanChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="recalculate-on-update" class="body-medium">Recalculate on Update</label>
            <span class="body-small meta">Automatically update this field when its dependencies change.</span>
          </div>
        </div>
        ${renderHelp('taxonomy', 'calculated:recalculateOnUpdate')}
      </div>
    `
  }

  protected renderAllowManualOverride(): TemplateResult {
    const enabled = this.config.allowManualOverride ?? DEFAULT_CALCULATED_CONFIG.allowManualOverride
    return html`
      <div class="row">
        <div class="input checkbox metadata">
          <ui-checkbox
            id="allow-manual-override"
            name="allowManualOverride"
            .checked="${enabled}"
            @change="${this.handleBooleanChange}"
          ></ui-checkbox>
          <div class="label">
            <label for="allow-manual-override" class="body-medium">Allow Manual Override</label>
            <span class="body-small meta">Permit users to manually set a value, overriding the formula.</span>
          </div>
        </div>
        ${renderHelp('taxonomy', 'calculated:allowManualOverride')}
      </div>
    `
  }
}
