import type { Edge, Node } from '@xyflow/svelte';

import type { Parameter } from '../types';
import { getTokenRanges } from '../components/utils/paramToken';
import { genShortId } from '../components/utils/IdGen';

export const START_NODE_TYPE = 'startNode';
export const LLM_NODE_TYPE = 'llmNode';
export const KNOWLEDGE_NODE_TYPE = 'knowledgeNode';
export const SYSTEM_START_PARAM_NAME = 'user_input';
export const SYSTEM_START_PARAM_LABEL = '用户问题';
export const FIELD_BINDING_META_KEY = 'fieldBindingMeta';

export type SingleRunFieldDescriptor = {
  key: string;
  label: string;
  value: string;
  placeholder?: string;
  multiline?: boolean;
};

export type SingleRunModel =
  | {
      mode: 'parameters';
      parameters: Parameter[];
      fields: [];
    }
  | {
      mode: 'fields';
      parameters: Parameter[];
      fields: SingleRunFieldDescriptor[];
    };

export type StartFormFieldType =
  | 'text'
  | 'textarea'
  | 'radio'
  | 'checkbox'
  | 'select'
  | 'file';

export type StartFormMeta = {
  title: string;
  description: string;
  submitText: string;
};

export type StartFormFieldSchema = {
  id?: string;
  key: string;
  label: string;
  type: StartFormFieldType;
  required: boolean;
  placeholder?: string;
  description?: string;
  defaultValue?: string | string[];
  options: string[];
  systemReserved?: boolean;
};

const START_FORM_FIELD_TYPE_SET = new Set<StartFormFieldType>([
  'text',
  'textarea',
  'radio',
  'checkbox',
  'select',
  'file',
]);
const OPTION_FIELD_TYPE_SET = new Set<StartFormFieldType>([
  'radio',
  'checkbox',
  'select',
]);
const DEFAULT_START_FORM_TITLE = '开始问答';
const DEFAULT_START_FORM_DESCRIPTION = '请先补充必要信息，再开始执行工作流。';
const DEFAULT_START_FORM_SUBMIT_TEXT = '开始';
const START_FORM_DEFAULT_FIELD_KEY_BY_TYPE: Record<StartFormFieldType, string> = {
  text: 'text_field',
  textarea: 'textarea_field',
  radio: 'radio_field',
  checkbox: 'checkbox_field',
  select: 'select_field',
  file: 'file_field',
};
const START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE: Record<StartFormFieldType, string> = {
  text: '文本字段',
  textarea: '长文本字段',
  radio: '单选字段',
  checkbox: '多选字段',
  select: '下拉字段',
  file: '文件字段',
};

type FieldBindingMeta = Record<
  string,
  {
    autoFilledFrom?: string;
    userModified?: boolean;
  }
>;

function asString(value: unknown) {
  return value == null ? '' : String(value);
}

function trimString(value: unknown) {
  return asString(value).trim();
}

function ensureStringArray(value: unknown) {
  if (!Array.isArray(value)) {
    return [];
  }
  return value.map((item) => asString(item).trim()).filter(Boolean);
}

function isSupportedStartFormFieldType(value: unknown): value is StartFormFieldType {
  return START_FORM_FIELD_TYPE_SET.has(asString(value).trim() as StartFormFieldType);
}

function isOptionFieldType(value: unknown): value is StartFormFieldType {
  return OPTION_FIELD_TYPE_SET.has(asString(value).trim() as StartFormFieldType);
}

function cloneParameter(parameter: Parameter): Parameter {
  return {
    ...parameter,
    children: parameter.children?.map(cloneParameter),
  };
}

function ensureParameterId(parameter: Parameter): Parameter {
  const cloned = cloneParameter(parameter);
  if (!cloned.id) {
    cloned.id = genShortId();
  }
  if (cloned.children?.length) {
    cloned.children = cloned.children.map(ensureParameterId);
  }
  return cloned;
}

function getNodeTitle(node?: Node | null) {
  return asString(node?.data?.title).trim() || '节点';
}

function getParameterLabel(parameter?: Parameter | null) {
  return (
    asString(parameter?.formLabel).trim() ||
    asString(parameter?.displayName).trim() ||
    asString(parameter?.name).trim() ||
    '参数'
  );
}

function getReferenceParameterLabel(parameter?: Parameter | null) {
  if (isSystemStartParameter(parameter)) {
    return getParameterLabel(parameter);
  }
  return asString(parameter?.name).trim() || getParameterLabel(parameter);
}

function isCollectionOutputParameter(parameter?: Parameter | null) {
  const dataType = asString(parameter?.dataType).trim();
  return (
    !!parameter?.children?.length &&
    (
      dataType === 'Array' ||
      dataType.length === 0
    )
  );
}

function buildCollectionAwareLabel(
  referenceLabel: string,
  parentLabel: string,
  parentIsCollection: boolean,
) {
  const normalizedReferenceLabel = asString(referenceLabel).trim();
  if (!normalizedReferenceLabel) {
    return parentLabel;
  }
  if (!parentLabel) {
    return normalizedReferenceLabel;
  }
  return parentIsCollection
    ? `${parentLabel}.[].${normalizedReferenceLabel}`
    : `${parentLabel}.${normalizedReferenceLabel}`;
}

function buildDisconnectedDisplayName(parameter: Parameter) {
  const displayName =
    asString(parameter.displayName).trim() ||
    asString(parameter.formLabel).trim();
  if (displayName) {
    return displayName;
  }

  const name = asString(parameter.name).trim();
  if (!name) {
    return '参数';
  }
  const segments = name.split('.');
  return segments[segments.length - 1] || name;
}

function flattenNodeRefs(
  currentNodeId: string,
  edges: Edge[],
  refNodeIds: string[],
  visited: Set<string>,
) {
  if (visited.has(currentNodeId)) {
    return;
  }
  visited.add(currentNodeId);

  for (const edge of edges) {
    if (edge.target === currentNodeId && edge.source) {
      refNodeIds.push(edge.source);
      flattenNodeRefs(edge.source, edges, refNodeIds, visited);
    }
  }
}

function flattenOutputDefs(
  node: Node,
  parameters: Parameter[],
  parentPath = '',
  parentLabel = '',
  parentIsCollection = false,
): Parameter[] {
  if (!parameters.length) {
    return [];
  }

  return parameters.flatMap((parameter) => {
    const rawName = asString(parameter.name).trim();
    if (!rawName) {
      return [];
    }

    const path = parentPath ? `${parentPath}.${rawName}` : rawName;
    const referenceLabel = getReferenceParameterLabel(parameter);
    const label = buildCollectionAwareLabel(referenceLabel, parentLabel, parentIsCollection);
    const displayName = `${getNodeTitle(node)} > ${label}`;
    const isCollection = isCollectionOutputParameter(parameter);
    const fullRef = `${node.id}.${path}`;
    const baseCandidate: Parameter = ensureParameterId({
      name: fullRef,
      ref: fullRef,
      refType: 'ref',
      dataType: parameter.dataType || 'String',
      displayName,
      formLabel: displayName,
      pathLabel: label,
      itemTypeLabel: parentIsCollection ? '数组项字段' : undefined,
      isCollection,
      nameDisabled: true,
      dataTypeDisabled: true,
      deleteDisabled: true,
      autoManaged: true,
    });

    const children = flattenOutputDefs(
      node,
      parameter.children || [],
      path,
      label,
      isCollection,
    );

    return [baseCandidate, ...children];
  });
}

function getNodeReferenceParameters(node: Node): Parameter[] {
  if (node.type === START_NODE_TYPE) {
    const parameters = Array.isArray(node.data?.parameters)
      ? (node.data.parameters as Parameter[])
      : [];
    return parameters
      .filter((parameter) => asString(parameter.name).trim())
      .map((parameter) =>
        ensureParameterId({
          ...cloneParameter(parameter),
          name: `${node.id}.${asString(parameter.name).trim()}`,
          ref: `${node.id}.${asString(parameter.name).trim()}`,
          refType: 'ref',
          displayName: `${getNodeTitle(node)} > ${getReferenceParameterLabel(parameter)}`,
          formLabel: `${getNodeTitle(node)} > ${getReferenceParameterLabel(parameter)}`,
          nameDisabled: true,
          dataTypeDisabled: true,
          deleteDisabled: true,
          autoManaged: true,
        }),
      );
  }

  const outputDefs = Array.isArray(node.data?.outputDefs)
    ? (node.data.outputDefs as Parameter[])
    : [];
  return flattenOutputDefs(node, outputDefs);
}

function uniqueParameters(parameters: Parameter[]) {
  const result: Parameter[] = [];
  const indexMap = new Map<string, number>();

  for (const parameter of parameters) {
    const key = asString(parameter.name).trim();
    if (!key) {
      continue;
    }
    const existingIndex = indexMap.get(key);
    if (existingIndex == null) {
      indexMap.set(key, result.length);
      result.push(ensureParameterId(parameter));
      continue;
    }

    const existingParameter = result[existingIndex]!;
    result[existingIndex] = ensureParameterId({
      ...existingParameter,
      ...parameter,
      children:
        parameter.children?.length || existingParameter.children?.length
          ? parameter.children || existingParameter.children
          : undefined,
    });
  }

  return result;
}

function findParameterByName(parameters: Parameter[], name: string) {
  return parameters.find((parameter) => asString(parameter.name).trim() === name);
}

function toManagedRefParameter(refPath: string, candidate?: Parameter): Parameter {
  return ensureParameterId({
    name: refPath,
    ref: candidate?.ref || refPath,
    refType: 'ref',
    dataType: candidate?.dataType || 'String',
    displayName: candidate?.displayName || candidate?.formLabel || refPath,
    formLabel: candidate?.formLabel || candidate?.displayName || refPath,
    description: candidate?.description,
    nameDisabled: true,
    dataTypeDisabled: true,
    deleteDisabled: true,
    autoManaged: true,
  });
}

function normalizeSystemStartFormType(value: unknown): 'input' | 'textarea' {
  return trimString(value) === 'input' ? 'input' : 'textarea';
}

function normalizeStartFormFieldUiType(
  value: unknown,
  fallback: StartFormFieldType = 'text',
): StartFormFieldType {
  const normalized = trimString(value);
  if (normalized === 'input') {
    return 'text';
  }
  if (isSupportedStartFormFieldType(normalized)) {
    return normalized;
  }
  return fallback;
}

function getDefaultStartFormFieldKeyBase(type: StartFormFieldType) {
  return START_FORM_DEFAULT_FIELD_KEY_BY_TYPE[type];
}

function getDefaultStartFormFieldLabel(type: StartFormFieldType) {
  return START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE[type];
}

function buildUniqueStartFormFieldKey(
  type: StartFormFieldType,
  existingKeys: string[],
) {
  const normalizedExistingKeys = new Set(
    existingKeys.map((item) => trimString(item)).filter(Boolean),
  );
  const baseKey = getDefaultStartFormFieldKeyBase(type);
  if (!normalizedExistingKeys.has(baseKey)) {
    return baseKey;
  }
  let index = 2;
  while (normalizedExistingKeys.has(`${baseKey}_${index}`)) {
    index += 1;
  }
  return `${baseKey}_${index}`;
}

function isAutoGeneratedStartFormFieldKey(key: unknown) {
  const normalizedKey = trimString(key);
  if (!normalizedKey) {
    return false;
  }
  if (/^field_[A-Za-z0-9]+$/.test(normalizedKey)) {
    return true;
  }
  return Object.values(START_FORM_DEFAULT_FIELD_KEY_BY_TYPE).some((baseKey) => {
    return normalizedKey === baseKey || new RegExp(`^${baseKey}_[0-9]+$`).test(normalizedKey);
  });
}

function isDefaultStartFormFieldLabel(label: unknown) {
  const normalizedLabel = trimString(label);
  if (!normalizedLabel) {
    return false;
  }
  if (normalizedLabel === '新字段') {
    return true;
  }
  return Object.values(START_FORM_DEFAULT_FIELD_LABEL_BY_TYPE).includes(normalizedLabel);
}

function ensureStartFormFieldId(
  fieldId: unknown,
  existingParameter?: Parameter | null,
) {
  return trimString(fieldId) || trimString(existingParameter?.id) || genShortId();
}

function createDefaultStartFormMeta(): StartFormMeta {
  return {
    title: DEFAULT_START_FORM_TITLE,
    description: DEFAULT_START_FORM_DESCRIPTION,
    submitText: DEFAULT_START_FORM_SUBMIT_TEXT,
  };
}

export function normalizeStartFormMeta(meta?: Partial<StartFormMeta> | null): StartFormMeta {
  const fallback = createDefaultStartFormMeta();
  return {
    // Missing values use defaults; an explicit empty string means the user cleared the field.
    title: meta?.title == null ? fallback.title : trimString(meta.title),
    description: meta?.description == null ? fallback.description : trimString(meta.description),
    submitText: trimString(meta?.submitText) || fallback.submitText,
  };
}

function normalizeSystemStartParameter(parameter?: Parameter | null): Parameter {
  const fallbackLabel = SYSTEM_START_PARAM_LABEL;
  const formLabel = trimString(parameter?.formLabel) || fallbackLabel;
  const formType = normalizeSystemStartFormType(parameter?.formType);
  return ensureParameterId({
    ...cloneParameter(parameter || {}),
    name: SYSTEM_START_PARAM_NAME,
    dataType: 'String',
    refType: 'input',
    required: true,
    contentType: 'text',
    formType,
    formLabel,
    formDescription: asString(parameter?.formDescription),
    formPlaceholder: trimString(parameter?.formPlaceholder) || '请输入用户问题',
    defaultValue: asString(parameter?.defaultValue),
    displayName: `流程开始 > ${formLabel}`,
    nameDisabled: true,
    dataTypeDisabled: true,
    deleteDisabled: true,
    requiredDisabled: true,
    systemReserved: true,
  });
}

function parameterTypeToStartFormFieldType(parameter?: Parameter | null): StartFormFieldType {
  if (
    trimString(parameter?.contentType) === 'file' ||
    trimString(parameter?.dataType).toLowerCase() === 'file'
  ) {
    return 'file';
  }
  const formType = trimString(parameter?.formType);
  if (formType === 'textarea') {
    return 'textarea';
  }
  if (formType === 'select') {
    return 'select';
  }
  if (formType === 'radio') {
    return 'radio';
  }
  if (formType === 'checkbox') {
    return 'checkbox';
  }
  return 'text';
}

function fieldTypeToDataType(type: StartFormFieldType) {
  if (type === 'checkbox') {
    return 'Array';
  }
  if (type === 'file') {
    return 'File';
  }
  return 'String';
}

function fieldTypeToContentType(type: StartFormFieldType) {
  return type === 'file' ? 'file' : 'text';
}

function fieldTypeToFormType(type: StartFormFieldType) {
  if (type === 'text') {
    return 'input';
  }
  if (type === 'file') {
    return 'input';
  }
  return type;
}

function normalizeStartFormField(
  field: Partial<StartFormFieldSchema> | null | undefined,
  existingParameter?: Parameter,
): StartFormFieldSchema | null {
  const key = trimString(field?.key) || trimString(existingParameter?.name);
  if (!key) {
    return null;
  }

  const fallbackType = parameterTypeToStartFormFieldType(existingParameter);
  const requestedType = isSupportedStartFormFieldType(field?.type)
    ? field?.type
    : fallbackType;
  const isSystemField =
    field?.systemReserved === true || key === SYSTEM_START_PARAM_NAME;
  const type = isSystemField
    ? requestedType === 'text'
      ? 'text'
      : 'textarea'
    : requestedType;
  const options = isOptionFieldType(type)
    ? ensureStringArray(field?.options || existingParameter?.enums)
    : [];

  const defaultValue = Array.isArray(field?.defaultValue)
    ? ensureStringArray(field?.defaultValue)
    : asString(field?.defaultValue ?? existingParameter?.defaultValue);

  return {
    id: ensureStartFormFieldId(field?.id, existingParameter),
    key,
    label:
      trimString(field?.label) ||
      trimString(existingParameter?.formLabel) ||
      trimString(existingParameter?.displayName) ||
      key,
    type,
    required: isSystemField ? true : Boolean(field?.required ?? existingParameter?.required),
    placeholder:
      trimString(field?.placeholder) || trimString(existingParameter?.formPlaceholder),
    description:
      trimString(field?.description) || trimString(existingParameter?.formDescription),
    defaultValue,
    options,
    systemReserved: isSystemField,
  };
}

function parameterToStartFormField(parameter?: Parameter | null) {
  return normalizeStartFormField(
    {
      id: trimString(parameter?.id),
      key: trimString(parameter?.name),
      label: trimString(parameter?.formLabel),
      type: parameterTypeToStartFormFieldType(parameter),
      required: Boolean(parameter?.required),
      placeholder: trimString(parameter?.formPlaceholder),
      description: trimString(parameter?.formDescription),
      defaultValue: parameter?.defaultValue,
      options: parameter?.enums || [],
      systemReserved: isSystemStartParameter(parameter),
    },
    parameter || undefined,
  );
}

function startFormFieldToParameter(
  field: StartFormFieldSchema,
  existingParameter?: Parameter,
): Parameter {
  if (field.systemReserved || field.key === SYSTEM_START_PARAM_NAME) {
    return normalizeSystemStartParameter({
      ...existingParameter,
      id: trimString(existingParameter?.id) || trimString(field.id),
      formLabel: field.label,
      formDescription: field.description,
      formPlaceholder: field.placeholder,
      formType: field.type === 'text' ? 'input' : 'textarea',
      defaultValue: Array.isArray(field.defaultValue)
        ? field.defaultValue.join('\n')
        : asString(field.defaultValue),
    });
  }

  const formLabel = trimString(field.label) || field.key;
  return ensureParameterId({
    ...cloneParameter(existingParameter || {}),
    id: trimString(existingParameter?.id) || trimString(field.id),
    name: field.key,
    dataType: fieldTypeToDataType(field.type),
    refType: 'input',
    required: Boolean(field.required),
    contentType: fieldTypeToContentType(field.type),
    formType: fieldTypeToFormType(field.type),
    formLabel,
    formDescription: asString(field.description),
    formPlaceholder: asString(field.placeholder),
    defaultValue: Array.isArray(field.defaultValue)
      ? field.defaultValue.join('\n')
      : asString(field.defaultValue),
    enums: isOptionFieldType(field.type) ? ensureStringArray(field.options) : [],
    displayName: `流程开始 > ${formLabel}`,
    systemReserved: false,
  });
}

export function normalizeStartFormSchema(
  schema?: Partial<StartFormFieldSchema>[] | null,
  parameters?: Parameter[] | null,
) {
  const parameterCandidates = Array.isArray(parameters)
    ? parameters.map(ensureParameterId)
    : [];
  const parameterMap = new Map(
    parameterCandidates
      .map((parameter) => [trimString(parameter.name), parameter] as const)
      .filter(([key]) => key.length > 0),
  );
  const parameterIdMap = new Map(
    parameterCandidates
      .map((parameter) => [trimString(parameter.id), parameter] as const)
      .filter(([key]) => key.length > 0),
  );

  const source = Array.isArray(schema) && schema.length > 0
    ? schema
    : parameterCandidates
        .map((parameter) => parameterToStartFormField(parameter))
        .filter(Boolean);

  const result: StartFormFieldSchema[] = [];
  const seen = new Set<string>();

  for (const item of source) {
    const fieldId = trimString((item as Partial<StartFormFieldSchema>)?.id);
    const key =
      trimString((item as Partial<StartFormFieldSchema>)?.key) ||
      trimString((item as any)?.name);
    if (!key || seen.has(key)) {
      continue;
    }
    const normalized = normalizeStartFormField(
      item as Partial<StartFormFieldSchema>,
      (fieldId ? parameterIdMap.get(fieldId) : undefined) || parameterMap.get(key),
    );
    if (!normalized) {
      continue;
    }
    seen.add(normalized.key);
    if (normalized.key === SYSTEM_START_PARAM_NAME) {
      result.unshift(normalized);
    } else {
      result.push(normalized);
    }
  }

  if (!seen.has(SYSTEM_START_PARAM_NAME)) {
    const systemParameter = parameterCandidates.find((parameter) =>
      isSystemStartParameter(parameter),
    );
    result.unshift(
      normalizeStartFormField(
        {
          id: trimString(systemParameter?.id),
          key: SYSTEM_START_PARAM_NAME,
          label: trimString(systemParameter?.formLabel) || SYSTEM_START_PARAM_LABEL,
          type:
            normalizeSystemStartFormType(systemParameter?.formType) === 'input'
              ? 'text'
              : 'textarea',
          required: true,
          placeholder: trimString(systemParameter?.formPlaceholder),
          description: trimString(systemParameter?.formDescription),
          defaultValue: asString(systemParameter?.defaultValue),
          options: [],
          systemReserved: true,
        },
        systemParameter,
      )!,
    );
  }

  return result;
}

export function createCustomStartFormField(
  field?: Partial<StartFormFieldSchema> | null,
  existingKeys: string[] = [],
): StartFormFieldSchema {
  const type = normalizeStartFormFieldUiType(field?.type, 'text');
  const key = trimString(field?.key) || buildUniqueStartFormFieldKey(type, existingKeys);
  return {
    id: ensureStartFormFieldId(field?.id),
    key,
    label: trimString(field?.label) || getDefaultStartFormFieldLabel(type),
    type,
    required: Boolean(field?.required),
    placeholder:
      trimString(field?.placeholder) ||
      (type === 'file' ? '请选择文件' : '请输入内容'),
    description: trimString(field?.description),
    defaultValue: Array.isArray(field?.defaultValue)
      ? ensureStringArray(field?.defaultValue)
      : asString(field?.defaultValue),
    options: isOptionFieldType(type) ? ensureStringArray(field?.options) : [],
    systemReserved: false,
  };
}

export function appendStartFormField(
  data?: Record<string, any> | null,
  field?: Partial<StartFormFieldSchema> | null,
) {
  const currentData = (data || {}) as Record<string, any>;
  const currentParameters = Array.isArray(currentData.parameters)
    ? (currentData.parameters as Parameter[])
    : [];
  const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
  const nextField = createCustomStartFormField(
    field,
    schema.map((item) => item.key),
  );
  return normalizeStartNodeData({
    ...currentData,
    startFormSchema: [...schema, nextField],
  });
}

export function updateStartFormField(
  data: Record<string, any> | null | undefined,
  currentKey: string,
  patch: Partial<StartFormFieldSchema>,
) {
  const currentData = (data || {}) as Record<string, any>;
  const currentParameters = Array.isArray(currentData.parameters)
    ? (currentData.parameters as Parameter[])
    : [];
  const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
  const nextSchema = schema.map((field) => {
    if (field.key !== currentKey) {
      return field;
    }
    const nextType = normalizeStartFormFieldUiType(patch.type, field.type);
    const shouldAutoRenameKey =
      !trimString(patch.key) &&
      patch.type != null &&
      isAutoGeneratedStartFormFieldKey(field.key);
    const shouldAutoRenameLabel =
      !trimString(patch.label) &&
      patch.type != null &&
      isDefaultStartFormFieldLabel(field.label);
    return {
      ...field,
      ...patch,
      key: trimString(patch.key) || (
        shouldAutoRenameKey
          ? buildUniqueStartFormFieldKey(
              nextType,
              schema
                .filter((item) => item.key !== field.key)
                .map((item) => item.key),
            )
          : field.key
      ),
      label: trimString(patch.label) || (
        shouldAutoRenameLabel ? getDefaultStartFormFieldLabel(nextType) : field.label
      ),
      type: nextType,
    };
  });
  return normalizeStartNodeData({
    ...currentData,
    startFormSchema: nextSchema,
  });
}

export function removeStartFormField(
  data?: Record<string, any> | null,
  key?: string | null,
) {
  const currentKey = trimString(key);
  if (!currentKey || currentKey === SYSTEM_START_PARAM_NAME) {
    return normalizeStartNodeData(data || {});
  }
  const currentData = (data || {}) as Record<string, any>;
  const currentParameters = Array.isArray(currentData.parameters)
    ? (currentData.parameters as Parameter[])
    : [];
  const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
  return normalizeStartNodeData({
    ...currentData,
    startFormSchema: schema.filter((field) => field.key !== currentKey),
  });
}

export function normalizeStartNodeData(
  data?: Record<string, any> | null,
  options?: {
    allowLegacyParametersOnly?: boolean;
  },
) {
  const currentData = (data || {}) as Record<string, any>;
  const currentParameters = Array.isArray(currentData.parameters)
    ? (currentData.parameters as Parameter[])
    : [];
  const hasSchema = Array.isArray(currentData.startFormSchema);
  const hasSystemParameter = hasSystemStartParameter(currentParameters);

  if (!hasSchema && !hasSystemParameter && options?.allowLegacyParametersOnly) {
    return {
      parameters: currentParameters.map(ensureParameterId),
      startFormMeta: currentData.startFormMeta,
      startFormSchema: currentData.startFormSchema,
    };
  }

  const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
  const parameterMap = new Map(
    currentParameters
      .map((parameter) => [trimString(parameter.name), parameter] as const)
      .filter(([key]) => key.length > 0),
  );
  const parameterIdMap = new Map(
    currentParameters
      .map((parameter) => [trimString(parameter.id), parameter] as const)
      .filter(([key]) => key.length > 0),
  );
  const parameters = schema.map((field) =>
    startFormFieldToParameter(
      field,
      (trimString(field.id) ? parameterIdMap.get(trimString(field.id)) : undefined) ||
        parameterMap.get(field.key),
    ),
  );

  return {
    parameters,
    startFormMeta: normalizeStartFormMeta(currentData.startFormMeta),
    startFormSchema: schema,
  };
}

export function createSystemStartParameter(): Parameter {
  return normalizeSystemStartParameter();
}

export function isSystemStartParameter(parameter?: Parameter | null) {
  if (!parameter) {
    return false;
  }
  return (
    parameter.systemReserved === true ||
    (asString(parameter.name).trim() === SYSTEM_START_PARAM_NAME &&
      asString(parameter.refType).trim() === 'input')
  );
}

export function hasSystemStartParameter(parameters?: Parameter[] | null) {
  return Array.isArray(parameters)
    ? parameters.some((parameter) => isSystemStartParameter(parameter))
    : false;
}

export function ensureStartNodeParameters(parameters?: Parameter[]) {
  const source = Array.isArray(parameters)
    ? parameters.map(cloneParameter)
    : [];
  const index = source.findIndex((parameter) => isSystemStartParameter(parameter));

  if (index >= 0) {
    source[index] = normalizeSystemStartParameter(source[index]!);
  } else {
    source.unshift(createSystemStartParameter());
  }

  const customParameters = source
    .filter((parameter) => !isSystemStartParameter(parameter))
    .map((parameter) => ensureParameterId(parameter));

  return [source.find((parameter) => isSystemStartParameter(parameter))!, ...customParameters];
}

export function createInitialWorkflowData() {
  const startData = normalizeStartNodeData(
    {
      parameters: ensureStartNodeParameters(),
    },
    {
      allowLegacyParametersOnly: false,
    },
  );
  return {
    nodes: [
      {
        id: `node_${genShortId()}`,
        type: START_NODE_TYPE,
        position: { x: 80, y: 180 },
        data: {
          title: '开始节点',
          ...startData,
        },
      } satisfies Node,
    ],
    edges: [],
    viewport: {
      x: 0,
      y: 0,
      zoom: 1,
    },
  };
}

export function normalizeWorkflowStartNodes<T extends Record<string, any>>(data: T): T {
  if (!data || typeof data !== 'object' || !Array.isArray(data.nodes)) {
    return data;
  }

  let changed = false;
  const nextNodes = data.nodes.map((node) => {
    if (node?.type !== START_NODE_TYPE) {
      return node;
    }
    const currentData = (node.data || {}) as Record<string, any>;
    const currentParameters = Array.isArray(currentData.parameters)
      ? (currentData.parameters as Parameter[])
      : [];
    const shouldNormalize =
      Array.isArray(currentData.startFormSchema) ||
      hasSystemStartParameter(currentParameters);
    if (!shouldNormalize) {
      return node;
    }
    const normalizedData = normalizeStartNodeData(currentData);
    const nextData = {
      ...currentData,
      ...normalizedData,
    };
    if (JSON.stringify(currentData) === JSON.stringify(nextData)) {
      return node;
    }
    changed = true;
    return {
      ...node,
      data: {
        ...nextData,
      },
    };
  });

  if (!changed) {
    return data;
  }
  return {
    ...data,
    nodes: nextNodes,
  };
}

export function isWorkflowDataEmpty(data: any) {
  if (!data || typeof data !== 'object') {
    return true;
  }
  const nodes = Array.isArray(data.nodes) ? data.nodes : [];
  const edges = Array.isArray(data.edges) ? data.edges : [];
  return nodes.length === 0 && edges.length === 0;
}

export function buildEditorReferenceParameters(
  currentNodeId: string,
  nodes: Node[],
  edges: Edge[],
  existingParameters?: Parameter[],
) {
  const refNodeIds: string[] = [];
  flattenNodeRefs(currentNodeId, edges, refNodeIds, new Set<string>());

  const upstreamParameters = nodes
    .filter((node) => refNodeIds.includes(node.id))
    .flatMap((node) => getNodeReferenceParameters(node));

  const upstreamNameSet = new Set(
    upstreamParameters.map((parameter) => asString(parameter.name).trim()),
  );

  const disconnectedParameters = (existingParameters || [])
    .map((parameter) => ensureParameterId(parameter))
    .filter((parameter) => {
      const name = asString(parameter.name).trim();
      if (!name || upstreamNameSet.has(name)) {
        return false;
      }
      return parameter.autoManaged === true;
    })
    .map((parameter) =>
      ensureParameterId({
        ...parameter,
        resolved: false,
        disconnected: true,
        displayName: buildDisconnectedDisplayName(parameter),
        formLabel: buildDisconnectedDisplayName(parameter),
      } as Parameter & { resolved: boolean; disconnected: boolean }),
    );

  return uniqueParameters([...disconnectedParameters, ...upstreamParameters]);
}

export function syncManagedParametersForFields(
  existingParameters: Parameter[] | undefined,
  candidateParameters: Parameter[],
  fieldValues: Record<string, string | undefined>,
) {
  const currentParameters = Array.isArray(existingParameters)
    ? existingParameters.map(ensureParameterId)
    : [];
  const tokenKeys = Array.from(
    new Set(
      Object.values(fieldValues)
        .flatMap((value) => getTokenRanges(asString(value)).map((token) => token.key))
        .filter((value) => value.trim().length > 0),
    ),
  );
  const usedTokenKeySet = new Set(tokenKeys);
  const candidateKeySet = new Set(
    candidateParameters
      .map((parameter) => asString(parameter.name).trim())
      .filter((value) => value.length > 0),
  );
  const nextParameters = currentParameters
    .filter((parameter) => {
      if (parameter.autoManaged !== true) {
        return true;
      }
      const parameterName = asString(parameter.name).trim();
      return usedTokenKeySet.has(parameterName) && candidateKeySet.has(parameterName);
    })
    .map((parameter) => {
      if (parameter.autoManaged !== true) {
        return parameter;
      }
      const candidate = findParameterByName(
        candidateParameters,
        asString(parameter.name).trim(),
      ) as (Parameter & { disconnected?: boolean }) | undefined;
      if (!candidate) {
        return parameter;
      }
      return {
        ...parameter,
        displayName: candidate.displayName || parameter.displayName,
        formLabel: candidate.formLabel || parameter.formLabel,
        disconnected: candidate.disconnected === true ? true : undefined,
      } as Parameter;
    });

  for (const tokenKey of tokenKeys) {
    if (!candidateKeySet.has(tokenKey)) {
      continue;
    }
    if (findParameterByName(nextParameters, tokenKey)) {
      continue;
    }
    const candidate = findParameterByName(candidateParameters, tokenKey);
    nextParameters.push(toManagedRefParameter(tokenKey, candidate));
  }

  return uniqueParameters(nextParameters);
}

export function toToken(refPath: string) {
  return `{{${refPath}}}`;
}

function getFieldBindingMeta(data: Record<string, any>) {
  return ((data?.[FIELD_BINDING_META_KEY] || {}) as FieldBindingMeta) || {};
}

export function updateFieldBindingMeta(
  currentData: Record<string, any>,
  fieldName: string,
  nextValue: string,
) {
  const nextMeta: FieldBindingMeta = {
    ...getFieldBindingMeta(currentData),
  };
  const currentFieldMeta = nextMeta[fieldName];
  if (!currentFieldMeta?.autoFilledFrom) {
    return nextMeta;
  }

  nextMeta[fieldName] = {
    ...currentFieldMeta,
    userModified: nextValue !== toToken(currentFieldMeta.autoFilledFrom),
  };
  return nextMeta;
}

function resolveNearestStartNodeRef(
  targetNodeId: string,
  nodes: Node[],
  edges: Edge[],
) {
  const queue = [targetNodeId];
  const visited = new Set<string>();

  while (queue.length > 0) {
    const currentId = queue.shift()!;
    if (visited.has(currentId)) {
      continue;
    }
    visited.add(currentId);

    for (const edge of edges) {
      if (edge.target !== currentId || !edge.source) {
        continue;
      }
      const sourceNode = nodes.find((node) => node.id === edge.source);
      if (!sourceNode) {
        continue;
      }
      if (sourceNode.type === START_NODE_TYPE) {
        const parameters = Array.isArray(sourceNode.data?.parameters)
          ? ((sourceNode.data?.parameters as Parameter[]) || [])
          : [];
        const systemParam = parameters.find((parameter) =>
          isSystemStartParameter(parameter),
        );
        if (!systemParam) {
          continue;
        }
        return {
          refPath: `${sourceNode.id}.${SYSTEM_START_PARAM_NAME}`,
          displayName: `${getNodeTitle(sourceNode)} > ${getParameterLabel(systemParam)}`,
        };
      }
      queue.push(sourceNode.id);
    }
  }

  return null;
}

function canApplyAutoBinding(
  fieldValue: string | undefined,
  fieldMeta: FieldBindingMeta[string] | undefined,
  expectedRefPath: string,
) {
  if (fieldMeta?.userModified) {
    return false;
  }
  const currentValue = asString(fieldValue).trim();
  if (!currentValue) {
    return true;
  }
  return currentValue === toToken(expectedRefPath);
}

function getSupportedFieldKeys(nodeType: string | undefined) {
  if (nodeType === LLM_NODE_TYPE) {
    return ['systemPrompt', 'userPrompt'];
  }
  if (nodeType === KNOWLEDGE_NODE_TYPE) {
    return ['keyword', 'limit'];
  }
  return [];
}

function replaceStartFieldReferenceValue(
  value: unknown,
  oldRefPath: string,
  newRefPath: string,
): unknown {
  if (typeof value === 'string') {
    const oldToken = toToken(oldRefPath);
    const newToken = toToken(newRefPath);
    if (value === oldRefPath) {
      return newRefPath;
    }
    if (!value.includes(oldToken)) {
      return value;
    }
    return value.split(oldToken).join(newToken);
  }

  if (Array.isArray(value)) {
    let changed = false;
    const nextValue = value.map((item) => {
      const nextItem = replaceStartFieldReferenceValue(item, oldRefPath, newRefPath);
      if (nextItem !== item) {
        changed = true;
      }
      return nextItem;
    });
    return changed ? nextValue : value;
  }

  if (value && typeof value === 'object') {
    let changed = false;
    const nextValue = Object.fromEntries(
      Object.entries(value).map(([key, itemValue]) => {
        const nextItemValue = replaceStartFieldReferenceValue(
          itemValue,
          oldRefPath,
          newRefPath,
        );
        if (nextItemValue !== itemValue) {
          changed = true;
        }
        return [key, nextItemValue];
      }),
    );
    return changed ? nextValue : value;
  }

  return value;
}

export function collectDownstreamNodeIds(
  rootNodeId: string,
  edges: Edge[],
) {
  const nodeIds = new Set<string>();

  const visit = (nodeId: string) => {
    if (!nodeId || nodeIds.has(nodeId)) {
      return;
    }
    nodeIds.add(nodeId);
    edges
      .filter((edge) => edge.source === nodeId && edge.sourceHandle !== 'loop_handle')
      .forEach((edge) => {
        if (edge.target) {
          visit(edge.target);
        }
      });
  };

  visit(rootNodeId);
  nodeIds.delete(rootNodeId);
  return Array.from(nodeIds);
}

export function renameStartFieldReferencesInNodes(
  nodes: Node[],
  edges: Edge[],
  startNodeId: string,
  currentKey: string,
  nextKey: string,
): Node[] {
  const normalizedStartNodeId = trimString(startNodeId);
  const normalizedCurrentKey = trimString(currentKey);
  const normalizedNextKey = trimString(nextKey);
  if (
    !normalizedStartNodeId ||
    !normalizedCurrentKey ||
    !normalizedNextKey ||
    normalizedCurrentKey === normalizedNextKey
  ) {
    return nodes;
  }

  const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`;
  const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`;

  const nextNodes: Node[] = nodes.map((node) => {
    if (node.id === normalizedStartNodeId) {
      return node;
    }
    const nextData = replaceStartFieldReferenceValue(
      node.data,
      oldRefPath,
      newRefPath,
    ) as Record<string, any> | undefined;
    if (nextData === node.data) {
      return node;
    }
    return {
      ...node,
      data: nextData ?? {},
    };
  });

  const affectedNodeIds = collectDownstreamNodeIds(normalizedStartNodeId, edges);
  if (affectedNodeIds.length === 0) {
    return nextNodes;
  }

  const patches = buildSequentialFieldBindingPatches(
    affectedNodeIds,
    nextNodes,
    edges,
  );
  if (patches.length === 0) {
    return nextNodes;
  }

  const patchMap = new Map(patches.map((item) => [item.nodeId, item.patch] as const));
  return nextNodes.map((node) => {
    const patch = patchMap.get(node.id);
    if (!patch) {
      return node;
    }
    return {
      ...node,
      data: {
        ...((node.data || {}) as Record<string, any>),
        ...patch,
      },
    };
  });
}

function getSingleRunFieldDescriptors(
  node: Pick<Node, 'type' | 'data'>,
): SingleRunFieldDescriptor[] {
  if (!node?.data) {
    return [];
  }

  if (node.type === LLM_NODE_TYPE) {
    return [
      {
        key: 'systemPrompt',
        label: '系统提示词',
        value: asString((node.data as Record<string, any>).systemPrompt),
        placeholder: '未设置',
        multiline: true,
      },
      {
        key: 'userPrompt',
        label: '用户提示词',
        value: asString((node.data as Record<string, any>).userPrompt),
        placeholder: '未设置',
        multiline: true,
      },
    ];
  }

  if (node.type === KNOWLEDGE_NODE_TYPE) {
    return [
      {
        key: 'keyword',
        label: '关键词',
        value: asString((node.data as Record<string, any>).keyword),
        placeholder: '未设置',
      },
      {
        key: 'limit',
        label: '获取数据量',
        value: asString((node.data as Record<string, any>).limit),
        placeholder: '未设置',
      },
    ];
  }

  return [];
}

function getAutoBindingFieldKeys(nodeType: string | undefined) {
  if (nodeType === LLM_NODE_TYPE) {
    return ['userPrompt'];
  }
  if (nodeType === KNOWLEDGE_NODE_TYPE) {
    return ['keyword'];
  }
  return [];
}

export function buildFieldBindingPatch(
  node: Node,
  nodes: Node[],
  edges: Edge[],
) {
  if (!node?.id || !node?.data) {
    return null;
  }

  const fieldKeys = getSupportedFieldKeys(node.type);
  if (fieldKeys.length === 0) {
    return null;
  }

  const data = (node.data || {}) as Record<string, any>;
  const startRef = resolveNearestStartNodeRef(node.id, nodes, edges);
  const patch: Record<string, any> = {};
  const nextFieldValues: Record<string, string | undefined> = Object.fromEntries(
    fieldKeys.map((fieldKey) => [fieldKey, asString(data[fieldKey])]),
  );
  const nextMeta: FieldBindingMeta = {
    ...getFieldBindingMeta(data),
  };

  const applyField = (fieldName: string) => {
    if (!startRef) {
      return;
    }
    const fieldMeta = nextMeta[fieldName];
    if (!canApplyAutoBinding(nextFieldValues[fieldName], fieldMeta, startRef.refPath)) {
      return;
    }
    const tokenValue = toToken(startRef.refPath);
    if (asString(nextFieldValues[fieldName]).trim() !== tokenValue) {
      patch[fieldName] = tokenValue;
    }
    nextFieldValues[fieldName] = tokenValue;
    nextMeta[fieldName] = {
      autoFilledFrom: startRef.refPath,
      userModified: false,
    };
  };

  const clearField = (fieldName: string, fieldMeta: NonNullable<FieldBindingMeta[string]>) => {
    const currentValue = asString(nextFieldValues[fieldName]).trim();
    const autoTokenValue = toToken(fieldMeta.autoFilledFrom || '');
    if (currentValue && currentValue !== autoTokenValue) {
      return;
    }
    if (currentValue) {
      patch[fieldName] = '';
    }
    nextFieldValues[fieldName] = '';
    delete nextMeta[fieldName];
  };

  for (const fieldName of getAutoBindingFieldKeys(node.type)) {
    const fieldMeta = nextMeta[fieldName];
    if (!fieldMeta?.autoFilledFrom || fieldMeta.userModified) {
      continue;
    }
    if (startRef?.refPath === fieldMeta.autoFilledFrom) {
      continue;
    }
    clearField(fieldName, fieldMeta);
  }

  for (const fieldName of getAutoBindingFieldKeys(node.type)) {
    applyField(fieldName);
  }

  const editorParameters = buildEditorReferenceParameters(
    node.id,
    nodes,
    edges,
    (data.parameters as Parameter[]) || [],
  );
  patch.parameters = syncManagedParametersForFields(
    (data.parameters as Parameter[]) || [],
    editorParameters,
    nextFieldValues,
  );
  patch[FIELD_BINDING_META_KEY] = nextMeta;

  const currentParameters = JSON.stringify((data.parameters as Parameter[]) || []);
  if (JSON.stringify(patch.parameters) === currentParameters) {
    delete patch.parameters;
  }

  if (
    JSON.stringify(nextMeta) === JSON.stringify(getFieldBindingMeta(data))
  ) {
    delete patch[FIELD_BINDING_META_KEY];
  }

  if (Object.keys(patch).length === 0) {
    return null;
  }

  return patch;
}

export function buildAutoBindingPatch(
  node: Node,
  nodes: Node[],
  edges: Edge[],
) {
  return buildFieldBindingPatch(node, nodes, edges);
}

export function buildSequentialFieldBindingPatches(
  nodeIds: string[],
  nodes: Node[],
  edges: Edge[],
) {
  const uniqueNodeIds = Array.from(
    new Set(nodeIds.map((nodeId) => asString(nodeId).trim()).filter(Boolean)),
  );
  if (uniqueNodeIds.length === 0) {
    return [];
  }

  const workingNodes = nodes.map((node) => ({
    ...node,
    data: {
      ...((node.data || {}) as Record<string, any>),
    },
  }));
  const patches: Array<{ nodeId: string; patch: Record<string, any> }> = [];

  for (const nodeId of uniqueNodeIds) {
    const nodeIndex = workingNodes.findIndex((item) => item.id === nodeId);
    if (nodeIndex < 0) {
      continue;
    }
    const currentNode = workingNodes[nodeIndex]!;
    const patch = buildFieldBindingPatch(currentNode, workingNodes, edges);
    if (!patch) {
      continue;
    }
    patches.push({ nodeId, patch });
    workingNodes[nodeIndex] = {
      ...currentNode,
      data: {
        ...((currentNode.data || {}) as Record<string, any>),
        ...patch,
      },
    };
  }

  return patches;
}

export function buildSingleRunParameters(node: Pick<Node, 'type' | 'data'> | null | undefined) {
  if (!node?.data) {
    return [];
  }

  if (node.type === START_NODE_TYPE) {
    const normalizedData = normalizeStartNodeData(
      (node.data || {}) as Record<string, any>,
      {
        allowLegacyParametersOnly: true,
      },
    );
    return Array.isArray(normalizedData.parameters)
      ? (normalizedData.parameters as Parameter[])
      : [];
  }

  const parameters = Array.isArray(node.data.parameters)
    ? (node.data.parameters as Parameter[]).map(ensureParameterId)
    : [];

  const fieldKeys =
    node.type === LLM_NODE_TYPE
      ? ['systemPrompt', 'userPrompt']
      : node.type === KNOWLEDGE_NODE_TYPE
        ? ['keyword', 'limit']
        : [];

  if (fieldKeys.length === 0) {
    return parameters;
  }

  const usedTokenKeys = Array.from(
    new Set(
      fieldKeys.flatMap((fieldKey) =>
        getTokenRanges(asString((node.data as Record<string, any>)[fieldKey])).map(
          (token) => token.key,
        ),
      ),
    ),
  );

  return parameters.filter((parameter) =>
    usedTokenKeys.includes(asString(parameter.name).trim()),
  ).map((parameter) =>
    ensureParameterId({
      ...parameter,
      required: parameter.required ?? true,
    }),
  );
}

export function buildSingleRunModel(
  node: Pick<Node, 'type' | 'data'> | null | undefined,
): SingleRunModel {
  const parameters = buildSingleRunParameters(node);
  if (!node?.data) {
    return {
      mode: 'parameters',
      parameters,
      fields: [],
    };
  }

  if (node.type === START_NODE_TYPE) {
    return {
      mode: 'parameters',
      parameters,
      fields: [],
    };
  }

  const fields = getSingleRunFieldDescriptors(node);
  if (fields.length === 0) {
    return {
      mode: 'parameters',
      parameters,
      fields: [],
    };
  }

  return {
    mode: 'fields',
    parameters,
    fields,
  };
}
