type VariantRecord = {
  selectedVariantIndex?: number | string;
  variantIndex?: number | string;
};

interface ChatVariantSwitchControllerOptions<TRecord extends VariantRecord, TItem> {
  mapRecords: (records: TRecord[]) => TItem[];
  onError?: (error: unknown) => void;
  onStateChange?: () => void;
  replaceRound: (items: TItem[], roundId: string, nextItems: TItem[]) => void;
}

interface EnsureVariantsOptions<TRecord extends VariantRecord> {
  fetchVariants: () => Promise<TRecord[]>;
  roundId: number | string;
  sessionId: number | string;
}

interface SwitchVariantOptions<TRecord extends VariantRecord, TItem>
  extends EnsureVariantsOptions<TRecord> {
  items: TItem[];
  onLocalSwitch?: (record: TRecord) => void;
  persistVariant: () => Promise<TRecord | void>;
  targetVariantIndex: number;
}

function variantCacheKey(sessionId: number | string, roundId: number | string) {
  return `${String(sessionId)}:${String(roundId)}`;
}

function normalizeVariantIndex(value: unknown) {
  const parsed = Number.parseInt(String(value || ''), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}

function markVariantSelected<TRecord extends VariantRecord>(
  record: TRecord,
  selectedVariantIndex: number,
): TRecord {
  return {
    ...record,
    selectedVariantIndex,
  };
}

function syncCachedSelection<TRecord extends VariantRecord>(
  records: TRecord[],
  selectedVariantIndex: number,
  selectedRecord?: TRecord,
) {
  return records.map((record) => {
    const isSelected =
      selectedRecord &&
      normalizeVariantIndex(record.variantIndex) ===
        normalizeVariantIndex(selectedRecord.variantIndex);
    return markVariantSelected(
      isSelected ? { ...record, ...selectedRecord } : record,
      selectedVariantIndex,
    );
  });
}

export function createChatVariantSwitchController<
  TRecord extends VariantRecord,
  TItem,
>(options: ChatVariantSwitchControllerOptions<TRecord, TItem>) {
  const cache = new Map<string, TRecord[]>();
  const fetchTasks = new Map<string, Promise<TRecord[]>>();
  const switchingKeys = new Set<string>();

  function notifyStateChange() {
    options.onStateChange?.();
  }

  async function ensureVariants(params: EnsureVariantsOptions<TRecord>) {
    const key = variantCacheKey(params.sessionId, params.roundId);
    const cached = cache.get(key);
    if (cached) {
      return cached;
    }
    const existingTask = fetchTasks.get(key);
    if (existingTask) {
      return existingTask;
    }
    const task = params
      .fetchVariants()
      .then((records) => {
        cache.set(key, records);
        return records;
      })
      .finally(() => {
        fetchTasks.delete(key);
      });
    fetchTasks.set(key, task);
    return task;
  }

  function prefetchVariants(params: EnsureVariantsOptions<TRecord>) {
    void ensureVariants(params).catch(() => {
      // 预取失败不打断当前页面，用户点击时仍会再次拉取。
    });
  }

  function hasCachedVariant(
    sessionId: number | string,
    roundId: number | string,
    variantIndex: number,
  ) {
    const records = cache.get(variantCacheKey(sessionId, roundId));
    return Boolean(
      records?.some(
        (record) => normalizeVariantIndex(record.variantIndex) === variantIndex,
      ),
    );
  }

  function isSwitching(sessionId?: number | string, roundId?: number | string) {
    if (!sessionId || !roundId) {
      return false;
    }
    return switchingKeys.has(variantCacheKey(sessionId, roundId));
  }

  async function switchVariant(params: SwitchVariantOptions<TRecord, TItem>) {
    const key = variantCacheKey(params.sessionId, params.roundId);
    if (switchingKeys.has(key)) {
      return null;
    }
    switchingKeys.add(key);
    notifyStateChange();
    const snapshot = [...params.items];
    try {
      const records = await ensureVariants(params);
      const target = records.find(
        (record) =>
          normalizeVariantIndex(record.variantIndex) === params.targetVariantIndex,
      );
      if (!target) {
        throw new Error('目标答案版本不存在');
      }
      const localTarget = markVariantSelected(target, params.targetVariantIndex);
      const nextItems = options.mapRecords([localTarget]);
      if (nextItems.length === 0) {
        throw new Error('目标答案版本渲染失败');
      }
      options.replaceRound(
        params.items,
        String(params.roundId),
        nextItems,
      );
      params.onLocalSwitch?.(localTarget);
      const persistedRecord = await params.persistVariant();
      const selectedRecord = markVariantSelected(
        persistedRecord || localTarget,
        params.targetVariantIndex,
      );
      cache.set(
        key,
        syncCachedSelection(records, params.targetVariantIndex, selectedRecord),
      );
      return selectedRecord;
    } catch (error) {
      params.items.splice(0, params.items.length, ...snapshot);
      options.onError?.(error);
      return null;
    } finally {
      switchingKeys.delete(key);
      notifyStateChange();
    }
  }

  function cacheVariants(
    sessionId: number | string,
    roundId: number | string,
    records: TRecord[],
  ) {
    cache.set(variantCacheKey(sessionId, roundId), records);
  }

  return {
    cacheVariants,
    hasCachedVariant,
    isSwitching,
    prefetchVariants,
    switchVariant,
  };
}
