import React from 'react';

type LookupItem = {
  display: string;
  value: string;
  isManual: boolean;
  source: 'selected' | 'manual';
  listDataId?: string;
};

type LookupOption = {
  display: string;
  value: string;
  listDataId: string;
  raw: any;
};

type WriteTargetInfo = {
  paramName: string;
  raw: any;
  candidates: string[];
  displayValue: string;
};

export class CodeInApplication implements CodeInComp {
  description() {
    return 'Smart Lookup Picker - reusable Yeeflow lookup/search-select control for approval forms and data list forms.';
  }

  requiredFields(params?: CodeInParams) {
    const required: string[] = [];
    const safeParams: any = params || {};
    const outputTargetMap: any = {};
    ['saveToField', 'selectedItemsField', 'newItemsField'].forEach((key) => {
      const candidates = this.collectRequiredFieldCandidates(safeParams[key]).filter((value) => this.isLikelyWritableTargetName(value));
      if (candidates.length > 0) {
        outputTargetMap[key] = candidates[0];
      }
      candidates.forEach((value) => {
        if (value && required.indexOf(value) === -1) {
          required.push(value);
        }
      });
    });
    /*
      Yeeflow variable-selector parameters are evaluated before render(), so an output
      target selected from the variable dropdown can arrive in context.params as the
      variable's current value instead of its writable name. requiredFields(params)
      receives the raw configuration early enough to capture the selected field/temp
      variable key and pass it into the rendered component through this instance.
    */
    (this as any).smartLookupOutputTargetMap = outputTargetMap;
    return required;
  }

  inputParameters(): InputParameter[] {
    return [
      { id: 'dataListId', name: 'Data List ID', type: 'variable', desc: 'Target Yeeflow data list ID used for lookup query.' },
      { id: 'displayField', name: 'Display Field', type: 'variable', desc: 'Field ID/name used for display text and keyword search matching.' },
      { id: 'valueField', name: 'Value Field', type: 'variable', desc: 'Field ID/name used as the saved value for selected matched items.' },
      { id: 'saveToField', name: 'Save To Field', type: 'variable', desc: 'Writable current form field or dashboard temp variable used to save the full combined JSON result. Choose the target variable/field from the selector.' },
      { id: 'selectedItemsField', name: 'Selected Items Field', type: 'variable', desc: 'Writable current form field or dashboard temp variable used to save matched selected values only. Choose the target variable/field from the selector.' },
      { id: 'newItemsField', name: 'New Items Field', type: 'variable', desc: 'Writable current form field or dashboard temp variable used to save manual/free-text items only. Choose the target variable/field from the selector.' },
      { id: 'multiSelect', name: 'Multi Select', type: 'variable', desc: 'Whether multi-select is enabled. Use true or false.' },
      { id: 'allowManualEntry', name: 'Allow Manual Entry', type: 'variable', desc: 'Whether manual/free-text entry is enabled. Use true or false.' },
      { id: 'maxResults', name: 'Max Results', type: 'string', desc: 'Maximum number of matched records shown in the suggestion list.' },
      { id: 'placeholderText', name: 'Placeholder Text', type: 'string', desc: 'Placeholder text shown in the lookup input.' },
      { id: 'labelText', name: 'Label Text', type: 'string', desc: 'Optional label/title shown above the control.' },
      { id: 'noResultText', name: 'No Result Text', type: 'string', desc: 'Text shown when no matched records are found.' },
      { id: 'manualTagText', name: 'Manual Tag Text', type: 'string', desc: 'Optional label shown on manual item chips.' },
      { id: 'minSearchChars', name: 'Minimum Search Characters', type: 'string', desc: 'Optional minimum characters before querying. Default is 1.' },
      { id: 'debounceMs', name: 'Search Debounce Milliseconds', type: 'string', desc: 'Optional search debounce delay. Default is 260.' },
    ];
  }

  render(context: CodeInContext, fieldsValues: any, readonly: boolean) {
    return (
      <SmartLookupPicker
        context={context}
        fieldsValues={fieldsValues}
        readonly={readonly}
        params={(context && context.params) || {}}
        configuredOutputTargets={(this as any).smartLookupOutputTargetMap || {}}
      />
    );
  }

  safeTrim(value: any) {
    return value === undefined || value === null ? '' : String(value).trim();
  }

  normalizeExpressionText(value: any) {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return String(value).trim();
    }
    if (Array.isArray(value)) {
      return value.map((item) => this.normalizeExpressionText(item)).filter(Boolean).join(',');
    }
    if (typeof value === 'object') {
      return this.normalizeExpressionText(
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key :
        value.id !== undefined ? value.id :
        value.Id !== undefined ? value.Id :
        value.fieldId !== undefined ? value.fieldId :
        value.FieldId !== undefined ? value.FieldId :
        value.name !== undefined ? value.name :
        value.Name !== undefined ? value.Name :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label :
        value.title !== undefined ? value.title :
        value.Title !== undefined ? value.Title : ''
      );
    }
    return String(value).trim();
  }

  collectRequiredFieldCandidates(value: any) {
    const candidates: string[] = [];
    const seen: any = {};
    const add = (candidate: any) => {
      const cleaned = this.normalizeExpressionText(candidate);
      if (cleaned && !seen[cleaned]) {
        seen[cleaned] = true;
        candidates.push(cleaned);
      }
    };
    const visit = (current: any, depth: number) => {
      if (current === undefined || current === null || depth > 3) {
        return;
      }
      if (typeof current === 'string' || typeof current === 'number' || typeof current === 'boolean') {
        add(current);
        return;
      }
      if (Array.isArray(current)) {
        current.forEach((item) => visit(item, depth + 1));
        return;
      }
      if (typeof current !== 'object') {
        return;
      }
      [
        'fieldId', 'FieldId', 'fieldName', 'FieldName',
        'variableId', 'VariableId', 'variableName', 'VariableName',
        'tempVariableId', 'TempVariableId', 'tempVariableName', 'TempVariableName',
        'tempVarId', 'TempVarId', 'id', 'Id', 'key', 'Key',
        'name', 'Name', 'code', 'Code', 'label', 'Label', 'title', 'Title',
        'path', 'Path', 'value', 'Value',
      ].forEach((key) => {
        if (current[key] !== undefined && current[key] !== null) {
          add(current[key]);
        }
      });
      ['target', 'Target', 'binding', 'Binding', 'field', 'Field', 'variable', 'Variable', 'tempVariable', 'TempVariable', 'data', 'Data', 'meta', 'Meta', 'metadata', 'Metadata'].forEach((key) => {
        if (current[key] !== undefined && current[key] !== null) {
          visit(current[key], depth + 1);
        }
      });
    };
    visit(value, 0);
    return candidates;
  }

  isLikelyWritableTargetName(candidate: any) {
    const value = this.safeTrim(candidate);
    if (!value || value === '[]' || value === '{}') {
      return false;
    }
    if (value.charAt(0) === '[' || value.charAt(0) === '{') {
      return false;
    }
    if (value.indexOf('":') !== -1 || value.indexOf('","') !== -1) {
      return false;
    }
    return value.length <= 160;
  }
}

class SmartLookupPicker extends React.Component<any, any> {
  searchTimer: any;
  activeRequestId: number;
  wrapperRef: any;

  constructor(props: any) {
    super(props);
    this.searchTimer = null;
    this.activeRequestId = 0;
    this.wrapperRef = null;
    this.state = {
      keyword: '',
      selectedItems: this.getInitialSelectedItems(props),
      options: [],
      loading: false,
      dropdownOpen: false,
      errorText: '',
      lastLoadedRawValue: this.readRawCombinedValue(props),
    };
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleDocumentMouseDown);
    this.logSaveDebug('mounted raw output params', {
      saveToField: this.props.params && this.props.params.saveToField,
      selectedItemsField: this.props.params && this.props.params.selectedItemsField,
      newItemsField: this.props.params && this.props.params.newItemsField,
      contextMethodSummary: this.getContextMethodSummary(this.props.context),
    });
    this.persistSelectedItems(this.state.selectedItems);
  }

  componentDidUpdate(prevProps: any) {
    const previousRawValue = this.readRawCombinedValue(prevProps);
    const nextRawValue = this.readRawCombinedValue(this.props);
    if (previousRawValue !== nextRawValue && nextRawValue !== this.state.lastLoadedRawValue) {
      this.setState({
        selectedItems: this.getInitialSelectedItems(this.props),
        lastLoadedRawValue: nextRawValue,
      });
    }
  }

  componentWillUnmount() {
    if (this.searchTimer) {
      clearTimeout(this.searchTimer);
    }
    document.removeEventListener('mousedown', this.handleDocumentMouseDown);
  }

  getConfig() {
    const params = this.props.params || {};
    const saveToTarget = this.resolveWriteTarget('saveToField', params.saveToField);
    const selectedItemsTarget = this.resolveWriteTarget('selectedItemsField', params.selectedItemsField);
    const newItemsTarget = this.resolveWriteTarget('newItemsField', params.newItemsField);
    return {
      dataListId: this.toCleanString(params.dataListId),
      displayField: this.toCleanString(params.displayField),
      valueField: this.toCleanString(params.valueField),
      saveToField: saveToTarget.displayValue,
      selectedItemsField: selectedItemsTarget.displayValue,
      newItemsField: newItemsTarget.displayValue,
      saveToTarget: saveToTarget,
      selectedItemsTarget: selectedItemsTarget,
      newItemsTarget: newItemsTarget,
      multiSelect: this.toBoolean(params.multiSelect, false),
      allowManualEntry: this.toBoolean(params.allowManualEntry, false),
      maxResults: this.toPositiveInt(params.maxResults, 8),
      placeholderText: this.toCleanString(params.placeholderText) || 'Search and select',
      labelText: this.toCleanString(params.labelText),
      noResultText: this.toCleanString(params.noResultText) || 'No matching records found',
      manualTagText: this.toCleanString(params.manualTagText) || 'Manual',
      minSearchChars: this.toPositiveInt(params.minSearchChars, 1),
      debounceMs: this.toPositiveInt(params.debounceMs, 260),
    };
  }

  getInitialSelectedItems(props: any) {
    const config = this.getConfigFromProps(props);
    const combinedRaw = this.readCurrentFieldValue(props, config.saveToField);
    const combinedItems = this.parseCombinedItems(combinedRaw);
    if (combinedItems.length > 0) {
      return this.dedupeItems(combinedItems);
    }

    const matchedValues = this.parseArrayValue(this.readCurrentFieldValue(props, config.selectedItemsField));
    const manualValues = this.parseArrayValue(this.readCurrentFieldValue(props, config.newItemsField));
    const rebuilt = matchedValues.map((value: any) => ({
      display: this.toCleanString(value),
      value: this.toCleanString(value),
      isManual: false,
      source: 'selected' as 'selected',
    })).concat(manualValues.map((value: any) => ({
      display: this.toCleanString(value),
      value: this.toCleanString(value),
      isManual: true,
      source: 'manual' as 'manual',
    })));
    return this.dedupeItems(rebuilt);
  }

  getConfigFromProps(props: any) {
    const params = (props && props.params) || {};
    const saveToTarget = this.resolveWriteTargetFromProps(props, 'saveToField', params.saveToField);
    const selectedItemsTarget = this.resolveWriteTargetFromProps(props, 'selectedItemsField', params.selectedItemsField);
    const newItemsTarget = this.resolveWriteTargetFromProps(props, 'newItemsField', params.newItemsField);
    return {
      saveToField: saveToTarget.displayValue,
      selectedItemsField: selectedItemsTarget.displayValue,
      newItemsField: newItemsTarget.displayValue,
      saveToTarget: saveToTarget,
      selectedItemsTarget: selectedItemsTarget,
      newItemsTarget: newItemsTarget,
    };
  }

  readRawCombinedValue(props: any) {
    const config = this.getConfigFromProps(props);
    return this.toCleanString(this.readCurrentFieldValue(props, config.saveToField));
  }

  readCurrentFieldValue(props: any, fieldName: string) {
    if (!fieldName) {
      return '';
    }
    const fieldsValues = (props && props.fieldsValues) || {};
    if (fieldsValues && fieldsValues[fieldName] !== undefined && fieldsValues[fieldName] !== null) {
      return fieldsValues[fieldName];
    }
    const context = props && props.context;
    if (context && typeof context.getFieldValue === 'function') {
      try {
        const value = context.getFieldValue(fieldName);
        if (value !== undefined && value !== null) {
          return value;
        }
      } catch (error) {
        return '';
      }
    }
    return '';
  }

  parseCombinedItems(rawValue: any) {
    const parsed = this.parseJsonValue(rawValue);
    if (!Array.isArray(parsed)) {
      return [];
    }
    return parsed
      .map((item: any) => {
        if (!item) {
          return null;
        }
        const isManual = item.isManual === true || item.source === 'manual';
        const display = this.toCleanString(item.display || item.value);
        const value = this.toCleanString(item.value || item.display);
        if (!display && !value) {
          return null;
        }
        return {
          display: display || value,
          value: value || display,
          isManual: isManual,
          source: isManual ? 'manual' : 'selected',
          listDataId: this.toCleanString(item.listDataId || item.ListDataID),
        };
      })
      .filter(Boolean);
  }

  parseArrayValue(rawValue: any) {
    const parsed = this.parseJsonValue(rawValue);
    if (Array.isArray(parsed)) {
      return parsed.map((value) => this.toCleanString(value)).filter(Boolean);
    }
    const cleaned = this.toCleanString(rawValue);
    if (!cleaned) {
      return [];
    }
    return cleaned.split(',').map((value) => this.toCleanString(value)).filter(Boolean);
  }

  parseJsonValue(rawValue: any) {
    if (Array.isArray(rawValue) || (rawValue && typeof rawValue === 'object')) {
      return rawValue;
    }
    const cleaned = this.toCleanString(rawValue);
    if (!cleaned) {
      return null;
    }
    try {
      return JSON.parse(cleaned);
    } catch (error) {
      return null;
    }
  }

  handleDocumentMouseDown = (event: any) => {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      this.setState({ dropdownOpen: false });
    }
  };

  handleInputChange = (event: any) => {
    const keyword = event.target.value || '';
    this.setState({ keyword: keyword, dropdownOpen: true, errorText: '' });
    this.scheduleSearch(keyword);
  };

  handleInputFocus = () => {
    if (this.state.options.length > 0 || this.state.keyword) {
      this.setState({ dropdownOpen: true });
    }
  };

  handleInputKeyDown = (event: any) => {
    const config = this.getConfig();
    if (event.key === 'Enter') {
      event.preventDefault();
      const keyword = this.toCleanString(this.state.keyword);
      if (config.allowManualEntry && keyword) {
        this.addManualItem(keyword);
      } else if (!config.multiSelect && this.state.options.length > 0) {
        this.addMatchedItem(this.state.options[0]);
      }
    }
    if (event.key === 'Escape') {
      this.setState({ dropdownOpen: false });
    }
  };

  scheduleSearch(keyword: string) {
    const config = this.getConfig();
    if (this.searchTimer) {
      clearTimeout(this.searchTimer);
    }
    const cleaned = this.toCleanString(keyword);
    if (!cleaned || cleaned.length < config.minSearchChars) {
      this.setState({ options: [], loading: false, dropdownOpen: Boolean(cleaned) });
      return;
    }
    this.searchTimer = setTimeout(() => {
      this.runSearch(cleaned);
    }, config.debounceMs);
  }

  async runSearch(keyword: string) {
    const config = this.getConfig();
    const client = this.props.context && this.props.context.modules && this.props.context.modules.yeeSDKClient;
    const canQuery = client && client.lists && typeof client.lists.queryItems === 'function';
    const requestId = this.activeRequestId + 1;
    this.activeRequestId = requestId;

    if (!canQuery || !config.dataListId || !config.displayField || !config.valueField) {
      this.setState({
        options: [],
        loading: false,
        dropdownOpen: true,
        errorText: canQuery ? '' : 'Lookup service is not available',
      });
      return;
    }

    this.setState({ loading: true, dropdownOpen: true, errorText: '' });

    try {
      /*
        Search strategy:
        Different Yeeflow placements/tenants accept different queryItems filter
        payloads. To keep the reusable template reliable, we first try several
        common contains-style payloads, then run a limited broad query and apply
        local contains matching against displayField. This avoids the "no result"
        symptom when the API accepts the request but ignores/rejects a specific
        filter shape.
      */
      let response: any = null;
      let options: LookupOption[] = [];
      const searchPayloads = this.buildSearchQueryPayloads(config, keyword);

      for (let payloadIndex = 0; payloadIndex < searchPayloads.length; payloadIndex += 1) {
        try {
          response = await this.callQueryItems(client, config.dataListId, searchPayloads[payloadIndex]);
          options = this.extractOptions(response, config, keyword);
          this.logSaveDebug('search payload result', {
            payloadIndex: payloadIndex,
            rows: this.extractRows(response).length,
            options: options.length,
          });
          if (options.length > 0) {
            break;
          }
        } catch (searchError) {
          this.logSaveDebug('search payload failed', {
            payloadIndex: payloadIndex,
            error: this.stringifyForLog(searchError),
          });
        }
      }

      if (options.length === 0) {
        try {
          const fallbackResponse = await this.callQueryItems(client, config.dataListId, this.buildFallbackQueryPayload(config));
          const fallbackOptions = this.extractOptions(fallbackResponse, config, keyword);
          this.logSaveDebug('search broad fallback', {
            keyword: keyword,
            filteredRows: this.extractRows(response).length,
            fallbackRows: this.extractRows(fallbackResponse).length,
            fallbackOptions: fallbackOptions.length,
          });
          if (fallbackOptions.length > 0) {
            options = fallbackOptions;
          }
        } catch (fallbackError) {
          /* Keep the empty filtered result when the broad fallback is not supported. */
        }
      }

      if (requestId !== this.activeRequestId) {
        return;
      }

      this.setState({
        options: options.slice(0, config.maxResults),
        loading: false,
        dropdownOpen: true,
        errorText: '',
      });
    } catch (error) {
      if (requestId !== this.activeRequestId) {
        return;
      }
      this.setState({
        options: [],
        loading: false,
        dropdownOpen: true,
        errorText: 'Unable to load matching records',
      });
    }
  }

  buildFilteredQueryPayload(config: any, keyword: string) {
    return this.buildSearchQueryPayloads(config, keyword)[0];
  }

  async callQueryItems(client: any, dataListId: string, payload: any) {
    /*
      Runtime compatibility note:
      Yeeflow examples commonly show queryItems(payload), while some SDK builds expose
      queryItems(dataListId, payload). Try both safely because an unsupported signature
      may return an empty page rather than throwing.
    */
    const payloadWithoutListId = Object.assign({}, payload);
    delete payloadWithoutListId.listId;
    delete payloadWithoutListId.dataListId;
    const attempts = [
      () => client.lists.queryItems(payload),
      () => client.lists.queryItems(dataListId, payloadWithoutListId),
      () => client.lists.queryItems(dataListId, payload),
      () => client.lists.queryItems(dataListId, payloadWithoutListId, {}),
      () => client.lists.queryItems(dataListId, payload, {}),
    ];
    let lastResponse: any = null;
    let lastError: any = null;
    for (let index = 0; index < attempts.length; index += 1) {
      try {
        const response = await attempts[index]();
        const rows = this.extractRows(response);
        this.logSaveDebug('queryItems attempt', {
          attempt: index,
          rows: rows.length,
          hasResponse: !!response,
        });
        if (rows.length > 0) {
          return response;
        }
        if (lastResponse === null) {
          lastResponse = response;
        }
      } catch (error) {
        lastError = error;
        this.logSaveDebug('queryItems attempt failed', {
          attempt: index,
          error: this.stringifyForLog(error),
        });
      }
    }
    if (lastResponse !== null) {
      return lastResponse;
    }
    throw lastError;
  }

  buildSearchQueryPayloads(config: any, keyword: string) {
    const fields = this.uniqueArray(['ListDataID', config.displayField, config.valueField]);

    /*
      Query assumption:
      Yeeflow list search is configured with field IDs/names passed in displayField and valueField.
      This payload uses a contains-style filter on displayField. If a tenant/runtime uses a
      different operator shape, runSearch falls back to loading a limited page and matching
      displayField locally so the control remains safe instead of crashing.
    */
    const basePayload: any = {
      listId: config.dataListId,
      dataListId: config.dataListId,
      fields: fields,
      fieldIds: fields,
      selectedFields: fields,
      pageIndex: 1,
      pageNo: 1,
      pageSize: Math.max(config.maxResults, 20),
    };

    return [
      Object.assign({}, basePayload, {
        filters: [
          {
            fieldId: config.displayField,
            field: config.displayField,
            operator: 'contains',
            compare: 'contains',
            value: keyword,
          },
        ],
      }),
      Object.assign({}, basePayload, {
        filters: [
          {
            fieldId: config.displayField,
            field: config.displayField,
            op: 'contains',
            method: 'contains',
            values: [keyword],
            value: keyword,
          },
        ],
      }),
      Object.assign({}, basePayload, {
        filter: {
          fieldId: config.displayField,
          field: config.displayField,
          operator: 'contains',
          compare: 'contains',
          value: keyword,
        },
      }),
      Object.assign({}, basePayload, {
        query: {
          fieldId: config.displayField,
          field: config.displayField,
          operator: 'contains',
          value: keyword,
        },
      }),
      Object.assign({}, basePayload, {
        where: [
          {
            fieldId: config.displayField,
            field: config.displayField,
            operator: 'contains',
            value: keyword,
          },
        ],
      }),
      Object.assign({}, basePayload, {
        condition: {
          fieldId: config.displayField,
          field: config.displayField,
          operator: 'contains',
          value: keyword,
        },
      }),
      Object.assign({}, basePayload, {
        conditions: [
          {
            fieldId: config.displayField,
            field: config.displayField,
            operator: 'contains',
            value: keyword,
          },
        ],
      }),
      Object.assign({}, basePayload, {
        filterItems: [
          {
            fieldId: config.displayField,
            field: config.displayField,
            operator: 'contains',
            value: keyword,
          },
        ],
      }),
      Object.assign({}, basePayload, {
        query: keyword,
        keyword: keyword,
        search: keyword,
        searchField: config.displayField,
        searchFieldId: config.displayField,
        searchKey: keyword,
      }),
    ];
  }

  buildFallbackQueryPayload(config: any) {
    return {
      listId: config.dataListId,
      dataListId: config.dataListId,
      fields: this.uniqueArray(['ListDataID', config.displayField, config.valueField]),
      fieldIds: this.uniqueArray(['ListDataID', config.displayField, config.valueField]),
      selectedFields: this.uniqueArray(['ListDataID', config.displayField, config.valueField]),
      pageIndex: 1,
      pageNo: 1,
      pageSize: Math.max(config.maxResults, 50),
    };
  }

  extractOptions(response: any, config: any, keyword: string) {
    const rows = this.extractRows(response);
    const normalizedKeyword = this.normalizeText(keyword);
    return rows
      .map((row: any) => {
        const display = this.toCleanString(this.readItemValue(row, config.displayField));
        const value = this.toCleanString(this.readItemValue(row, config.valueField) || this.readItemValue(row, 'ListDataID') || this.readItemValue(row, 'ListDataId'));
        const listDataId = this.toCleanString(this.readItemValue(row, 'ListDataID') || this.readItemValue(row, 'ListDataId') || this.readItemValue(row, 'id'));
        if (!display || !value) {
          return null;
        }
        return {
          display: display,
          value: value,
          listDataId: listDataId,
          raw: row,
        };
      })
      .filter(Boolean)
      .filter((option: LookupOption) => {
        if (!normalizedKeyword) {
          return true;
        }
        return this.normalizeText(option.display).indexOf(normalizedKeyword) !== -1;
      })
      .filter((option: LookupOption) => !this.isSelected(option.value, false));
  }

  extractRows(response: any) {
    if (!response) {
      return [];
    }
    if (Array.isArray(response)) {
      return response;
    }
    const candidates = [
      response.items,
      response.Items,
      response.rows,
      response.Rows,
      response.records,
      response.Records,
      response.listData,
      response.ListData,
      response.data,
      response.Data,
      response.result,
      response.Result,
      response.data && response.data.items,
      response.data && response.data.Items,
      response.data && response.data.data,
      response.data && response.data.Data,
      response.data && response.data.rows,
      response.data && response.data.Rows,
      response.data && response.data.list,
      response.data && response.data.List,
      response.data && response.data.listData,
      response.data && response.data.ListData,
      response.data && response.data.records,
      response.data && response.data.Records,
      response.result && response.result.items,
      response.result && response.result.Items,
      response.result && response.result.data,
      response.result && response.result.Data,
      response.result && response.result.rows,
      response.result && response.result.Rows,
      response.result && response.result.listData,
      response.result && response.result.ListData,
      response.result && response.result.records,
      response.result && response.result.Records,
      response.Data && response.Data.items,
      response.Data && response.Data.Items,
      response.Data && response.Data.data,
      response.Data && response.Data.Data,
      response.Data && response.Data.rows,
      response.Data && response.Data.Rows,
      response.Result && response.Result.items,
      response.Result && response.Result.Items,
      response.Result && response.Result.data,
      response.Result && response.Result.Data,
      response.Result && response.Result.rows,
      response.Result && response.Result.Rows,
    ];
    for (let index = 0; index < candidates.length; index += 1) {
      if (Array.isArray(candidates[index])) {
        return candidates[index];
      }
    }
    return this.findFirstRowArray(response, 0);
  }

  findFirstRowArray(value: any, depth: number): any[] {
    if (!value || depth > 5) {
      return [];
    }
    if (Array.isArray(value)) {
      if (value.length === 0) {
        return value;
      }
      const first = value[0];
      if (first && typeof first === 'object') {
        return value;
      }
      return [];
    }
    if (typeof value !== 'object') {
      return [];
    }
    const preferredKeys = [
      'items', 'Items', 'rows', 'Rows', 'records', 'Records',
      'listData', 'ListData', 'data', 'Data', 'result', 'Result',
      'list', 'List',
    ];
    for (let keyIndex = 0; keyIndex < preferredKeys.length; keyIndex += 1) {
      const child = value[preferredKeys[keyIndex]];
      const found = this.findFirstRowArray(child, depth + 1);
      if (found.length > 0) {
        return found;
      }
    }
    const keys = Object.keys(value);
    for (let index = 0; index < keys.length; index += 1) {
      const found = this.findFirstRowArray(value[keys[index]], depth + 1);
      if (found.length > 0) {
        return found;
      }
    }
    return [];
  }

  readItemValue(row: any, fieldName: string) {
    if (!row || !fieldName) {
      return '';
    }
    const directValue = this.readObjectValueByKey(row, fieldName);
    if (directValue !== undefined && directValue !== null) {
      return this.normalizeCellValue(directValue);
    }
    const fieldMaps = [row.values, row.Values, row.fields, row.Fields, row.data, row.Data, row.item, row.Item];
    for (let mapIndex = 0; mapIndex < fieldMaps.length; mapIndex += 1) {
      const map = fieldMaps[mapIndex];
      if (map && !Array.isArray(map)) {
        const mappedValue = this.readObjectValueByKey(map, fieldName);
        if (mappedValue !== undefined && mappedValue !== null) {
          return this.normalizeCellValue(mappedValue);
        }
      }
    }
    const fieldArrays = [
      row.fieldValues,
      row.FieldValues,
      row.fields,
      row.Fields,
      row.values,
      row.Values,
      row.data,
      row.Data,
      row.cells,
      row.Cells,
      row.columns,
      row.Columns,
    ];
    for (let groupIndex = 0; groupIndex < fieldArrays.length; groupIndex += 1) {
      const group = fieldArrays[groupIndex];
      if (!Array.isArray(group)) {
        continue;
      }
      for (let itemIndex = 0; itemIndex < group.length; itemIndex += 1) {
        const field = group[itemIndex] || {};
        const fieldId = this.toCleanString(
          field.fieldId !== undefined ? field.fieldId :
          field.FieldId !== undefined ? field.FieldId :
          field.FieldID !== undefined ? field.FieldID :
          field.id !== undefined ? field.id :
          field.Id !== undefined ? field.Id :
          field.ID !== undefined ? field.ID :
          field.name !== undefined ? field.name :
          field.Name !== undefined ? field.Name :
          field.fieldName !== undefined ? field.fieldName :
          field.FieldName !== undefined ? field.FieldName :
          field.fieldCode !== undefined ? field.fieldCode :
          field.FieldCode !== undefined ? field.FieldCode :
          field.code !== undefined ? field.code :
          field.Code !== undefined ? field.Code :
          field.key !== undefined ? field.key :
          field.Key !== undefined ? field.Key : ''
        );
        if (this.sameFieldName(fieldId, fieldName)) {
          return this.normalizeCellValue(
            field.value !== undefined ? field.value :
            field.Value !== undefined ? field.Value :
            field.fieldValue !== undefined ? field.fieldValue :
            field.FieldValue !== undefined ? field.FieldValue :
            field.dataValue !== undefined ? field.dataValue :
            field.DataValue !== undefined ? field.DataValue :
            field.valueText !== undefined ? field.valueText :
            field.ValueText !== undefined ? field.ValueText :
            field.text !== undefined ? field.text :
            field.Text !== undefined ? field.Text :
            field.display !== undefined ? field.display :
            field.Display !== undefined ? field.Display :
            field.label !== undefined ? field.label :
            field.Label !== undefined ? field.Label :
            field.name !== undefined ? field.name :
            field.Name !== undefined ? field.Name : ''
          );
        }
      }
    }
    return '';
  }

  readObjectValueByKey(source: any, wantedKey: string) {
    if (!source || typeof source !== 'object' || Array.isArray(source) || !wantedKey) {
      return undefined;
    }
    if (source[wantedKey] !== undefined) {
      return source[wantedKey];
    }
    const keys = Object.keys(source);
    for (let index = 0; index < keys.length; index += 1) {
      if (this.sameFieldName(keys[index], wantedKey)) {
        return source[keys[index]];
      }
    }
    return undefined;
  }

  sameFieldName(left: any, right: any) {
    const leftText = this.toCleanString(left);
    const rightText = this.toCleanString(right);
    if (!leftText || !rightText) {
      return false;
    }
    return leftText === rightText || leftText.toLowerCase() === rightText.toLowerCase();
  }

  normalizeCellValue(value: any) {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return value;
    }
    if (Array.isArray(value)) {
      return value.map((item) => this.normalizeCellValue(item)).filter(Boolean).join(', ');
    }
    if (typeof value === 'object') {
      return (
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.fieldValue !== undefined ? value.fieldValue :
        value.FieldValue !== undefined ? value.FieldValue :
        value.dataValue !== undefined ? value.dataValue :
        value.DataValue !== undefined ? value.DataValue :
        value.valueText !== undefined ? value.valueText :
        value.ValueText !== undefined ? value.ValueText :
        value.text !== undefined ? value.text :
        value.Text !== undefined ? value.Text :
        value.display !== undefined ? value.display :
        value.Display !== undefined ? value.Display :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label :
        value.name !== undefined ? value.name :
        value.Name !== undefined ? value.Name :
        value.title !== undefined ? value.title :
        value.Title !== undefined ? value.Title : ''
      );
    }
    return value;
  }

  addMatchedItem(option: LookupOption) {
    const config = this.getConfig();
    if (this.props.readonly || !option) {
      return;
    }
    const item = {
      display: option.display,
      value: option.value,
      isManual: false,
      source: 'selected' as 'selected',
      listDataId: option.listDataId,
    };
    const nextItems = config.multiSelect ? this.dedupeItems(this.state.selectedItems.concat([item])) : [item];
    this.setState({
      selectedItems: nextItems,
      keyword: '',
      options: [],
      dropdownOpen: false,
      errorText: '',
    }, () => this.persistSelectedItems(nextItems));
  }

  addManualItem(text: string) {
    const config = this.getConfig();
    const cleaned = this.toCleanString(text);
    if (this.props.readonly || !config.allowManualEntry || !cleaned) {
      return;
    }
    const item = {
      display: cleaned,
      value: cleaned,
      isManual: true,
      source: 'manual' as 'manual',
    };
    const nextItems = config.multiSelect ? this.dedupeItems(this.state.selectedItems.concat([item])) : [item];
    this.setState({
      selectedItems: nextItems,
      keyword: '',
      options: [],
      dropdownOpen: false,
      errorText: '',
    }, () => this.persistSelectedItems(nextItems));
  }

  removeItem(indexToRemove: number) {
    if (this.props.readonly) {
      return;
    }
    const nextItems = this.state.selectedItems.filter((item: LookupItem, index: number) => index !== indexToRemove);
    this.setState({ selectedItems: nextItems }, () => this.persistSelectedItems(nextItems));
  }

  persistSelectedItems(items: LookupItem[]) {
    const config = this.getConfig();
    const normalizedItems = this.dedupeItems(items).map((item: LookupItem) => ({
      display: item.display,
      value: item.value,
      isManual: item.isManual === true,
      source: item.isManual === true ? 'manual' : 'selected',
    }));
    const matchedValues = normalizedItems
      .filter((item: LookupItem) => item.isManual !== true)
      .map((item: LookupItem) => item.value);
    const manualValues = normalizedItems
      .filter((item: LookupItem) => item.isManual === true)
      .map((item: LookupItem) => item.value);

    this.logSaveDebug('persist payload', {
      rawTargets: {
        saveToField: config.saveToTarget.raw,
        selectedItemsField: config.selectedItemsTarget.raw,
        newItemsField: config.newItemsTarget.raw,
      },
      resolvedTargets: {
        saveToField: config.saveToTarget.candidates,
        selectedItemsField: config.selectedItemsTarget.candidates,
        newItemsField: config.newItemsTarget.candidates,
      },
      normalizedItems: normalizedItems,
      matchedValues: matchedValues,
      manualValues: manualValues,
    });

    if (config.saveToTarget.candidates.length > 0) {
      this.writeTargetValue(config.saveToTarget, JSON.stringify(normalizedItems));
    }
    if (config.selectedItemsTarget.candidates.length > 0) {
      this.writeTargetValue(config.selectedItemsTarget, JSON.stringify(matchedValues));
    }
    if (config.newItemsTarget.candidates.length > 0) {
      this.writeTargetValue(config.newItemsTarget, JSON.stringify(manualValues));
    }
  }

  writeTargetValue(target: WriteTargetInfo, value: any) {
    const candidates = target && target.candidates ? target.candidates : [];
    let saved = false;
    for (let index = 0; index < candidates.length; index += 1) {
      const candidate = candidates[index];
      if (this.writeFieldValue(candidate, value, target.paramName)) {
        saved = true;
        break;
      }
    }
    this.logSaveDebug('write result', {
      paramName: target && target.paramName,
      candidates: candidates,
      succeeded: saved,
      value: value,
    });
    return saved;
  }

  writeFieldValue(fieldName: string, value: any, paramName?: string) {
    const context = this.props.context || {};
    const fieldsValues = this.props.fieldsValues || {};

    if (!fieldName) {
      return false;
    }

    const writerNames = [
      'setFieldValue',
      'setFormFieldValue',
      'setFieldData',
      'setValue',
      'setVariableValue',
      'setVariable',
      'setTempVariableValue',
      'setTempVariable',
      'setTemporaryVariableValue',
      'setTemporaryVariable',
      'setDashboardVariableValue',
      'setPageVariableValue',
      'setFilterVariableValue',
      'setFilterValue',
      'updateFieldValue',
      'updateVariableValue',
    ];
    const setterHosts = this.getSetterHosts(context);

    for (let hostIndex = 0; hostIndex < setterHosts.length; hostIndex += 1) {
      const hostInfo = setterHosts[hostIndex];
      const host = hostInfo.host;
      for (let writerIndex = 0; writerIndex < writerNames.length; writerIndex += 1) {
        const writerName = writerNames[writerIndex];
        const writer = host && host[writerName];
        if (typeof writer === 'function') {
          try {
            const result = writer.call(host, fieldName, value);
            this.logSaveDebug('setter succeeded', {
              paramName: paramName,
              target: fieldName,
              host: hostInfo.name,
              writer: writerName,
              result: result,
              value: value,
            });
            if (result === false) {
              continue;
            }
            return true;
          } catch (error) {
            this.logSaveDebug('setter failed', {
              paramName: paramName,
              target: fieldName,
              host: hostInfo.name,
              writer: writerName,
              error: this.stringifyForLog(error),
            });
          }
        }
      }
    }

    const objectSetterNames = ['setFieldsValue', 'setFieldValues', 'setValues', 'setVariables', 'setTempVariables'];
    for (let hostIndex = 0; hostIndex < setterHosts.length; hostIndex += 1) {
      const hostInfo = setterHosts[hostIndex];
      const host = hostInfo.host;
      for (let writerIndex = 0; writerIndex < objectSetterNames.length; writerIndex += 1) {
        const writerName = objectSetterNames[writerIndex];
        const writer = host && host[writerName];
        if (typeof writer === 'function') {
          try {
            const payload: any = {};
            payload[fieldName] = value;
            const result = writer.call(host, payload);
            this.logSaveDebug('object setter succeeded', {
              paramName: paramName,
              target: fieldName,
              host: hostInfo.name,
              writer: writerName,
              result: result,
              value: value,
            });
            if (result === false) {
              continue;
            }
            return true;
          } catch (error) {
            this.logSaveDebug('object setter failed', {
              paramName: paramName,
              target: fieldName,
              host: hostInfo.name,
              writer: writerName,
              error: this.stringifyForLog(error),
            });
          }
        }
      }
    }

    fieldsValues[fieldName] = value;
    this.logSaveDebug('fieldsValues fallback used', {
      paramName: paramName,
      target: fieldName,
      value: value,
    });
    return true;
  }

  getSetterHosts(context: any) {
    const hosts: any[] = [];
    const addHost = (name: string, host: any) => {
      if (host && hosts.filter((item) => item.host === host).length === 0) {
        hosts.push({ name: name, host: host });
      }
    };
    addHost('context', context);
    addHost('context.formContext', context && context.formContext);
    addHost('context.pageContext', context && context.pageContext);
    addHost('context.dashboardContext', context && context.dashboardContext);
    addHost('context.variableContext', context && context.variableContext);
    addHost('context.variables', context && context.variables);
    addHost('context.tempVariables', context && context.tempVariables);
    addHost('context.runtimeContext', context && context.runtimeContext);
    addHost('context.dataContext', context && context.dataContext);
    return hosts;
  }

  dedupeItems(items: LookupItem[]) {
    const seen: any = {};
    const result: LookupItem[] = [];
    (items || []).forEach((item: LookupItem) => {
      if (!item) {
        return;
      }
      const display = this.toCleanString(item.display || item.value);
      const value = this.toCleanString(item.value || item.display);
      if (!display && !value) {
        return;
      }
      const isManual = item.isManual === true || item.source === 'manual';
      const key = (isManual ? 'manual|' : 'selected|') + this.normalizeText(value || display);
      if (seen[key]) {
        return;
      }
      seen[key] = true;
      result.push({
        display: display || value,
        value: value || display,
        isManual: isManual,
        source: isManual ? 'manual' : 'selected',
        listDataId: this.toCleanString(item.listDataId),
      });
    });
    return result;
  }

  isSelected(value: string, isManual: boolean) {
    const key = (isManual ? 'manual|' : 'selected|') + this.normalizeText(value);
    return this.state.selectedItems.some((item: LookupItem) => {
      const itemKey = (item.isManual ? 'manual|' : 'selected|') + this.normalizeText(item.value);
      return itemKey === key;
    });
  }

  resolveWriteTarget(paramName: string, rawValue: any): WriteTargetInfo {
    return this.resolveWriteTargetFromProps(this.props, paramName, rawValue);
  }

  resolveWriteTargetFromProps(props: any, paramName: string, rawValue: any): WriteTargetInfo {
    const configuredTargets = (props && props.configuredOutputTargets) || {};
    const requiredFieldFallbackTarget = this.getRequiredFieldFallbackTarget(props, paramName);
    const candidates = this.mergeTargetCandidates(
      this.collectWritableTargetCandidates(configuredTargets[paramName]),
      this.mergeTargetCandidates(
        this.collectWritableTargetCandidates(rawValue),
        this.collectWritableTargetCandidates(requiredFieldFallbackTarget)
      )
    ).filter((candidate) => this.isLikelyWritableTargetName(candidate));
    const displayValue = candidates.length > 0 ? candidates[0] : this.toCleanString(rawValue);
    const target = {
      paramName: paramName,
      raw: rawValue,
      candidates: candidates,
      displayValue: displayValue,
    };

    /*
      DEBUG - Smart Lookup Picker target resolution.
      Keep temporarily while validating Yeeflow expression-editor variable bindings.
      This logs raw output-target parameters and the writable target candidates extracted
      from plain strings, variable-picker objects, expression wrapper objects, and temp
      variable metadata where available.
    */
    this.logSaveDebug('resolve target', {
      paramName: paramName,
      raw: rawValue,
      requiredFieldFallbackTarget: requiredFieldFallbackTarget,
      candidates: candidates,
      contextMethodSummary: this.getContextMethodSummary(props && props.context),
    });

    return target;
  }

  getRequiredFieldFallbackTarget(props: any, paramName: string) {
    /*
      Fallback for Yeeflow dashboard/page variable selectors:
      requiredFields(params) returns the three configured output targets in order. In
      some runtime placements the CodeInApplication instance used for requiredFields()
      is not the same instance used for render(), so the stored target map may not
      survive. fieldsValues still carries the required field/temp variable keys, so
      we can safely use the ordered keys as a write-target fallback.
    */
    const fieldsValues = (props && props.fieldsValues) || {};
    const keys = Object.keys(fieldsValues).filter((key) => this.isLikelyWritableTargetName(key));
    const indexMap: any = {
      saveToField: 0,
      selectedItemsField: 1,
      newItemsField: 2,
    };
    const index = indexMap[paramName];
    return index !== undefined && keys[index] ? keys[index] : '';
  }

  isLikelyWritableTargetName(candidate: any) {
    const value = this.toCleanString(candidate);
    if (!value) {
      return false;
    }
    if (value === '[]' || value === '{}') {
      return false;
    }
    if (value.charAt(0) === '[' || value.charAt(0) === '{') {
      return false;
    }
    if (value.indexOf('":') !== -1 || value.indexOf('","') !== -1) {
      return false;
    }
    return value.length <= 160;
  }

  mergeTargetCandidates(primary: string[], secondary: string[]) {
    const candidates: string[] = [];
    const seen: any = {};
    (primary || []).concat(secondary || []).forEach((candidate) => {
      const cleaned = this.toCleanString(candidate);
      if (cleaned && !seen[cleaned]) {
        seen[cleaned] = true;
        candidates.push(cleaned);
      }
    });
    return candidates;
  }

  collectWritableTargetCandidates(rawValue: any) {
    const candidates: string[] = [];
    const seen: any = {};
    const addCandidate = (value: any) => {
      const cleaned = this.toCleanString(value);
      if (!cleaned || seen[cleaned]) {
        return;
      }
      seen[cleaned] = true;
      candidates.push(cleaned);
    };

    const visit = (value: any, depth: number) => {
      if (value === undefined || value === null || depth > 4) {
        return;
      }
      if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
        addCandidate(value);
        this.extractExpressionTokens(value).forEach(addCandidate);
        return;
      }
      if (Array.isArray(value)) {
        value.forEach((item) => visit(item, depth + 1));
        return;
      }
      if (typeof value !== 'object') {
        return;
      }

      /*
        For write targets selected from Yeeflow's variable dropdown, `value` may be
        the current variable value (often blank), while key/name/label/id metadata
        identifies the variable to update. Therefore target-like keys are collected
        before value-like keys.
      */
      [
        'fieldId',
        'FieldId',
        'fieldName',
        'FieldName',
        'variableId',
        'VariableId',
        'variableName',
        'VariableName',
        'tempVariableId',
        'TempVariableId',
        'tempVariableName',
        'TempVariableName',
        'tempVarId',
        'TempVarId',
        'id',
        'Id',
        'key',
        'Key',
        'name',
        'Name',
        'code',
        'Code',
        'label',
        'Label',
        'title',
        'Title',
        'path',
        'Path',
        'value',
        'Value',
      ].forEach((key) => {
        if (value[key] !== undefined && value[key] !== null) {
          addCandidate(value[key]);
          this.extractExpressionTokens(value[key]).forEach(addCandidate);
        }
      });

      [
        'target',
        'Target',
        'binding',
        'Binding',
        'bind',
        'Bind',
        'field',
        'Field',
        'variable',
        'Variable',
        'tempVariable',
        'TempVariable',
        'expression',
        'Expression',
        'data',
        'Data',
        'meta',
        'Meta',
        'metadata',
        'Metadata',
        'props',
        'Props',
      ].forEach((key) => {
        if (value[key] !== undefined && value[key] !== null) {
          visit(value[key], depth + 1);
        }
      });
    };

    visit(rawValue, 0);
    return candidates;
  }

  extractExpressionTokens(value: any) {
    const text = this.toCleanString(value);
    if (!text) {
      return [];
    }
    const tokens: string[] = [];
    const patterns = [
      /\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g,
      /@\{\s*([A-Za-z0-9_.-]+)\s*\}/g,
      /\$\{\s*([A-Za-z0-9_.-]+)\s*\}/g,
      /\b(?:TempVariables|tempVariables|Variables|variables|Fields|fields)\.([A-Za-z0-9_.-]+)\b/g,
    ];
    patterns.forEach((pattern) => {
      let match = pattern.exec(text);
      while (match) {
        if (match[1]) {
          tokens.push(match[1]);
          const parts = match[1].split('.');
          if (parts.length > 1) {
            tokens.push(parts[parts.length - 1]);
          }
        }
        match = pattern.exec(text);
      }
    });
    return tokens;
  }

  toCleanString(value: any) {
    return this.normalizeExpressionValue(value);
  }

  normalizeText(value: any) {
    return this.toCleanString(value).toLowerCase();
  }

  toBoolean(value: any, fallback: boolean) {
    if (value === true || value === false) {
      return value;
    }
    if (Array.isArray(value) && value.length > 0) {
      return this.toBoolean(value[0], fallback);
    }
    if (value && typeof value === 'object') {
      const objectValue =
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key :
        value.checked !== undefined ? value.checked :
        value.Checked !== undefined ? value.Checked :
        value.selected !== undefined ? value.selected :
        value.Selected !== undefined ? value.Selected :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label : undefined;
      if (objectValue !== undefined) {
        return this.toBoolean(objectValue, fallback);
      }
    }
    const cleaned = this.normalizeText(value);
    if (['true', '1', 'yes', 'y', 'on'].indexOf(cleaned) !== -1) {
      return true;
    }
    if (['false', '0', 'no', 'n', 'off'].indexOf(cleaned) !== -1) {
      return false;
    }
    return fallback;
  }

  normalizeExpressionValue(value: any) {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return String(value).trim();
    }
    if (Array.isArray(value)) {
      if (value.length === 0) {
        return '';
      }
      return value.map((item) => this.normalizeExpressionValue(item)).filter(Boolean).join(',');
    }
    if (typeof value === 'object') {
      const resolved =
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key :
        value.id !== undefined ? value.id :
        value.Id !== undefined ? value.Id :
        value.fieldId !== undefined ? value.fieldId :
        value.FieldId !== undefined ? value.FieldId :
        value.name !== undefined ? value.name :
        value.Name !== undefined ? value.Name :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label :
        value.title !== undefined ? value.title :
        value.Title !== undefined ? value.Title : undefined;
      if (resolved !== undefined) {
        return this.normalizeExpressionValue(resolved);
      }
      try {
        return JSON.stringify(value);
      } catch (error) {
        return '';
      }
    }
    return String(value).trim();
  }

  toPositiveInt(value: any, fallback: number) {
    const parsed = parseInt(String(value || ''), 10);
    return parsed > 0 ? parsed : fallback;
  }

  uniqueArray(values: string[]) {
    const seen: any = {};
    return values.filter((value) => {
      const cleaned = this.toCleanString(value);
      if (!cleaned || seen[cleaned]) {
        return false;
      }
      seen[cleaned] = true;
      return true;
    });
  }

  logSaveDebug(message: string, payload?: any) {
    /*
      DEBUG - Smart Lookup Picker save diagnostics.
      Remove or guard this method after validating output-target behavior across:
      - approval form fields
      - data list form fields
      - dashboard temp variables
      - expression-editor variable picker bindings
    */
    try {
      if (typeof window !== 'undefined') {
        const debugWindow: any = window as any;
        debugWindow.__SmartLookupPickerDebug = debugWindow.__SmartLookupPickerDebug || [];
        debugWindow.__SmartLookupPickerDebug.push({
          message: message,
          payload: payload,
          time: new Date().toISOString(),
        });
        if (debugWindow.__SmartLookupPickerDebug.length > 80) {
          debugWindow.__SmartLookupPickerDebug.shift();
        }
      }
      if (typeof console !== 'undefined' && console && typeof console.log === 'function') {
        console.log('[SmartLookupPicker debug] ' + message, payload);
      }
    } catch (error) {
      /* Debug logging must never break the control. */
    }
  }

  stringifyForLog(value: any) {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string') {
      return value;
    }
    if (value && value.message) {
      return value.message;
    }
    try {
      return JSON.stringify(value);
    } catch (error) {
      return String(value);
    }
  }

  getContextMethodSummary(context: any) {
    const summary: any = {};
    const hosts = this.getSetterHosts(context || {});
    hosts.forEach((hostInfo) => {
      const host = hostInfo.host;
      const methods: string[] = [];
      if (host) {
        [
          'setFieldValue',
          'setFormFieldValue',
          'setValue',
          'setVariableValue',
          'setTempVariableValue',
          'setTempVariable',
          'setDashboardVariableValue',
          'setPageVariableValue',
          'setFilterVariableValue',
          'setFieldsValue',
          'setVariables',
          'setTempVariables',
        ].forEach((name) => {
          if (typeof host[name] === 'function') {
            methods.push(name);
          }
        });
      }
      summary[hostInfo.name] = methods;
    });
    return summary;
  }

  renderSelectedChips(config: any) {
    if (!this.state.selectedItems.length) {
      return null;
    }
    return (
      <div className="slp-chip-row">
        {this.state.selectedItems.map((item: LookupItem, index: number) => (
          <span className={item.isManual ? 'slp-chip slp-chip-manual' : 'slp-chip'} key={(item.isManual ? 'm-' : 's-') + item.value + '-' + index}>
            <span className="slp-chip-text">{item.display}</span>
            {item.isManual ? <span className="slp-chip-badge">{config.manualTagText}</span> : null}
            {!this.props.readonly ? (
              <button type="button" className="slp-chip-remove" onClick={() => this.removeItem(index)} aria-label={'Remove ' + item.display}>
                x
              </button>
            ) : null}
          </span>
        ))}
      </div>
    );
  }

  renderDropdown(config: any) {
    if (!this.state.dropdownOpen || this.props.readonly) {
      return null;
    }
    const hasKeyword = Boolean(this.toCleanString(this.state.keyword));
    return (
      <div className="slp-dropdown">
        {this.state.loading ? <div className="slp-status">Searching...</div> : null}
        {!this.state.loading && this.state.errorText ? <div className="slp-status slp-status-error">{this.state.errorText}</div> : null}
        {!this.state.loading && !this.state.errorText && hasKeyword && this.state.options.length === 0 ? (
          <div className="slp-status">{config.noResultText}</div>
        ) : null}
        {!this.state.loading && !this.state.errorText && this.state.options.map((option: LookupOption) => (
          <button type="button" className="slp-option" key={option.value + '-' + option.listDataId} onMouseDown={(event) => event.preventDefault()} onClick={() => this.addMatchedItem(option)}>
            {config.multiSelect ? <span className="slp-checkbox" aria-hidden="true" /> : null}
            <span className="slp-option-main">
              <span className="slp-option-title">{option.display}</span>
              <span className="slp-option-subtitle">{option.value}</span>
            </span>
          </button>
        ))}
        {!this.state.loading && !this.state.errorText && config.allowManualEntry && hasKeyword ? (
          <button type="button" className="slp-manual-row" onMouseDown={(event) => event.preventDefault()} onClick={() => this.addManualItem(this.state.keyword)}>
            Add "{this.toCleanString(this.state.keyword)}" as manual entry
          </button>
        ) : null}
      </div>
    );
  }

  render() {
    const config = this.getConfig();
    const readonly = this.props.readonly === true;
    const isConfigMissing = !config.dataListId || !config.displayField || !config.valueField;
    return (
      <div className={readonly ? 'slp-root slp-readonly' : 'slp-root'} ref={(node) => { this.wrapperRef = node; }}>
        <style dangerouslySetInnerHTML={{ __html: smartLookupPickerStyles }} />
        {config.labelText ? <div className="slp-label">{config.labelText}</div> : null}
        <div className={isConfigMissing ? 'slp-shell slp-shell-warning' : 'slp-shell'}>
          {this.renderSelectedChips(config)}
          {!readonly ? (
            <input
              className="slp-input"
              value={this.state.keyword}
              placeholder={config.placeholderText}
              onChange={this.handleInputChange}
              onFocus={this.handleInputFocus}
              onKeyDown={this.handleInputKeyDown}
              disabled={readonly}
            />
          ) : null}
          {readonly && !this.state.selectedItems.length ? <div className="slp-readonly-empty">No lookup item selected</div> : null}
        </div>
        {isConfigMissing && !readonly ? (
          <div className="slp-help">Configure dataListId, displayField, and valueField to enable lookup search.</div>
        ) : null}
        {this.renderDropdown(config)}
        {/*
          Testing checklist:
          - single select and multi select
          - manual entry with Enter
          - remove selected item
          - save combined output JSON
          - save matched-only output values
          - save manual-only output text
          - clear input after selection
          - no duplicate first item bug
          - readonly mode
          - no result state
          - invalid saved JSON state
        */}
      </div>
    );
  }
}

const smartLookupPickerStyles = [
  '.slp-root{position:relative;width:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;color:#172033;}',
  '.slp-label{margin-bottom:8px;color:#172033;font-size:13px;font-weight:600;line-height:1.35;}',
  '.slp-shell{min-height:42px;display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:7px 9px;background:#ffffff;border:1px solid #d8e1ee;border-radius:8px;transition:border-color .16s ease,box-shadow .16s ease,background .16s ease;}',
  '.slp-shell:focus-within{border-color:#146ff6;box-shadow:0 0 0 3px rgba(20,111,246,.1);}',
  '.slp-shell-warning{border-color:#f2c36b;background:#fffaf0;}',
  '.slp-input{flex:1 1 180px;min-width:120px;height:28px;border:0;outline:none;background:transparent;color:#172033;font-size:14px;line-height:28px;}',
  '.slp-input::placeholder{color:#8a97aa;}',
  '.slp-chip-row{display:flex;flex-wrap:wrap;gap:6px;}',
  '.slp-chip{display:inline-flex;max-width:100%;align-items:center;gap:6px;min-height:28px;padding:3px 6px 3px 10px;border:1px solid #cfe0ff;border-radius:999px;background:#edf4ff;color:#0d4fb3;font-size:12px;font-weight:500;line-height:18px;}',
  '.slp-chip-manual{border-color:#d9dee7;background:#f5f7fa;color:#4f5d73;}',
  '.slp-chip-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
  '.slp-chip-badge{padding:1px 5px;border-radius:999px;background:#ffffff;color:#6b778c;font-size:10px;font-weight:600;}',
  '.slp-chip-remove{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer;font-size:13px;line-height:1;}',
  '.slp-chip-remove:hover{background:rgba(20,111,246,.12);}',
  '.slp-dropdown{position:absolute;z-index:30;left:0;right:0;top:calc(100% + 6px);max-height:280px;overflow:auto;padding:6px;border:1px solid #d8e1ee;border-radius:8px;background:#ffffff;box-shadow:0 10px 24px rgba(21,33,51,.12);}',
  '.slp-option,.slp-manual-row{width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;border:0;border-radius:6px;background:transparent;color:#172033;text-align:left;cursor:pointer;}',
  '.slp-option:hover,.slp-manual-row:hover{background:#f3f7ff;}',
  '.slp-checkbox{width:15px;height:15px;flex:0 0 15px;border:1px solid #aebbd0;border-radius:4px;background:#ffffff;}',
  '.slp-option-main{min-width:0;display:flex;flex-direction:column;gap:2px;}',
  '.slp-option-title{overflow:hidden;color:#172033;font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap;}',
  '.slp-option-subtitle{overflow:hidden;color:#7a879a;font-size:12px;line-height:16px;text-overflow:ellipsis;white-space:nowrap;}',
  '.slp-manual-row{margin-top:4px;border-top:1px solid #eef2f7;border-radius:0 0 6px 6px;color:#4f5d73;font-size:13px;}',
  '.slp-status{padding:11px 10px;color:#6b778c;font-size:13px;line-height:18px;}',
  '.slp-status-error{color:#b42318;}',
  '.slp-help{margin-top:6px;color:#8a6200;font-size:12px;line-height:16px;}',
  '.slp-readonly .slp-shell{background:#f8fafc;}',
  '.slp-readonly-empty{color:#8a97aa;font-size:13px;line-height:24px;}',
  '@media (max-width:520px){.slp-shell{padding:7px;}.slp-input{flex-basis:100%;}.slp-dropdown{max-height:240px;}}',
].join('');
