/**
 * 「绑定数据」属性控件
 * <p>
 * 声明了 bindable 的字段，属性面板除了能手工输入，还可以从组件 dataSchema 里挑一个数据字段。
 * 挑中后写进属性值的仍是普通文本，渲染引擎按绑定协议解析，
 * 所以绑定与手工输入走的是同一条路径，不存在两套取值逻辑。
 */
import type { Component, Editor, Trait } from 'grapesjs';

import type { QualityFieldSchema } from './types';

import { propAttrName } from './attrs';
import { definitionOf } from './lookup';

/** 绑定控件在 GrapesJS 里注册的类型名 */
export const BINDING_TRAIT_TYPE = 'qc-binding';

/** 不绑定时的空选项 */
const NO_BINDING_LABEL = '不绑定（手工输入）';

const SELECT_CLASS = 'qc-binding-select';
const INPUT_CLASS = 'qc-binding-input';
const HINT_CLASS = 'qc-binding-hint';

/** 绑定值写回属性的形式，与 QualityFieldSchema.bindAs 对应 */
type BindMode = 'path' | 'text';

/** 绑定路径 → 属性值；text 包成 {{path}} 交渲染期解析，path 必须保持裸路径 */
function toBindingValue(path: string, mode: BindMode): string {
  if (!path) {
    return '';
  }
  return mode === 'path' ? path : `{{${path}}}`;
}

/** 属性值 → 绑定路径；识别不出就返回空串，表示这是手工输入而非绑定 */
function fromBindingValue(value: string, mode: BindMode): string {
  const trimmed = value.trim();
  if (mode === 'path') {
    return trimmed;
  }
  const match = /^\{\{\s*([^{}]+?)\s*\}\}$/.exec(trimmed);
  return match?.[1]?.trim() ?? '';
}

interface BindingInputParts {
  input: HTMLInputElement;
  select: HTMLSelectElement;
}

/**
 * 自定义 trait 回调的入参。
 * <p>
 * TraitManager.addType 的泛型不会把参数类型推导到回调上，不显式标注就会是隐式 any。
 * 形状与 TraitView 内部 getClbOpts() 的返回值一致。
 */
interface TraitViewOpts {
  component: Component;
  elInput: HTMLInputElement;
  trait: Trait;
}

/** 找到当前 trait 对应的属性字段，取它的绑定形式与提示 */
function fieldOf(component: Component | undefined, trait: Trait): QualityFieldSchema | undefined {
  const name = trait.getName();
  return definitionOf(component)?.propertySchema.find((field) => propAttrName(field.key) === name);
}

function bindModeOf(component: Component | undefined, trait: Trait): BindMode {
  return fieldOf(component, trait)?.bindAs === 'path' ? 'path' : 'text';
}

/** 在属性面板里建出「下拉选择 + 手工输入 + 提示」三件套 */
function createBindingInput(trait: Trait, component?: Component): HTMLElement {
  const definition = definitionOf(component);
  const wrap = document.createElement('div');
  wrap.className = 'qc-binding';

  const select = document.createElement('select');
  select.className = SELECT_CLASS;
  select.append(new Option(NO_BINDING_LABEL, ''));
  (definition?.dataSchema ?? []).forEach((field) => {
    // 选项值就是绑定路径，标签给用户看懂用途
    select.append(new Option(field.label, field.key));
  });

  const input = document.createElement('input');
  input.type = 'text';
  input.className = INPUT_CLASS;
  input.placeholder = trait.get('placeholder') ?? '{{report.batchNo}}';

  wrap.append(select, input);

  const tip = fieldOf(component, trait)?.tip;
  if (tip) {
    const hint = document.createElement('div');
    hint.className = HINT_CLASS;
    hint.textContent = tip;
    wrap.append(hint);
  }
  return wrap;
}

function partsOf(elInput: HTMLElement): BindingInputParts | undefined {
  const select = elInput.querySelector<HTMLSelectElement>(`.${SELECT_CLASS}`);
  const input = elInput.querySelector<HTMLInputElement>(`.${INPUT_CLASS}`);
  if (!select || !input) {
    return undefined;
  }
  return { input, select };
}

/** 把当前属性值回填到两个控件上：能识别成绑定的勾下拉，否则只填输入框 */
function syncFromTrait(elInput: HTMLElement, trait: Trait, component?: Component): void {
  const parts = partsOf(elInput);
  if (!parts) {
    return;
  }
  const value = String(trait.getValue() ?? '');
  const path = fromBindingValue(value, bindModeOf(component, trait));
  // 设置了 value 却不在候选项里（如组件换过、路径已废弃）时补一个临时选项，避免下拉显示空白
  if (path && ![...parts.select.options].some((option) => option.value === path)) {
    parts.select.append(new Option(path, path));
  }
  parts.select.value = path;
  parts.input.value = value;
}

/** 注册绑定控件类型；重复注册直接覆盖，配合热更新不报错 */
export function registerBindingTraitType(editor: Editor): void {
  editor.TraitManager.addType(BINDING_TRAIT_TYPE, {
    createInput({ trait, component }) {
      return createBindingInput(trait, component);
    },
    onRender({ elInput, trait, component }: TraitViewOpts) {
      syncFromTrait(elInput, trait, component);
    },
    onUpdate({ elInput, trait, component }: TraitViewOpts) {
      syncFromTrait(elInput, trait, component);
    },
    onEvent({ elInput, trait, component, event }: TraitViewOpts & { event: Event }) {
      const target = event.target;
      if (!(target instanceof HTMLElement)) {
        return;
      }
      const parts = partsOf(elInput);
      if (!parts) {
        return;
      }
      if (target.classList.contains(SELECT_CLASS)) {
        const value = toBindingValue(parts.select.value, bindModeOf(component, trait));
        parts.input.value = value;
        trait.setValue(value);
        return;
      }
      if (target.classList.contains(INPUT_CLASS)) {
        trait.setValue(parts.input.value);
      }
    },
  });
}