import isEqual from 'fast-deep-equal';
import type { PartialDeep } from 'type-fest';

import { MESSAGE_CANCEL_FLAT } from '@/const/message';
import { shareService } from '@/services/share';
import { userService } from '@/services/user';
import type { StoreSetter } from '@/store/types';
import type { UserStore } from '@/store/user';
import type { LobeAgentSettings } from '@/types/session';
import type {
  SystemAgentItem,
  UserGeneralConfig,
  UserKeyVaults,
  UserServiceModelConfigKey,
  UserSettings,
  UserSystemAgentConfigKey,
} from '@/types/user/settings';
import { difference } from '@/utils/difference';
import { merge } from '@/utils/merge';

import { settingsSelectors } from './selectors/settings';

type Setter = StoreSetter<UserStore>;

type SystemAgentDiff = Partial<Record<string, unknown>>;

export const createSettingsSlice = (set: Setter, get: () => UserStore, _api?: unknown) =>
  new UserSettingsActionImpl(set, get, _api);

export class UserSettingsActionImpl {
  readonly #get: () => UserStore;
  readonly #set: Setter;

  constructor(set: Setter, get: () => UserStore, _api?: unknown) {
    void _api;
    this.#set = set;
    this.#get = get;
  }

  addToolToAllowList = async (toolKey: string): Promise<void> => {
    const currentAllowList = this.#get().settings.tool?.humanIntervention?.allowList || [];

    if (currentAllowList.includes(toolKey)) return;

    await this.#get().setSettings({
      tool: {
        humanIntervention: {
          allowList: [...currentAllowList, toolKey],
        },
      },
    });
  };

  importAppSettings = async (importAppSettings: UserSettings): Promise<void> => {
    const { setSettings } = this.#get();

    await setSettings(importAppSettings);
  };

  importUrlShareSettings = async (settingsParams: string | null): Promise<void> => {
    if (settingsParams) {
      const importSettings = shareService.decodeShareSettings(settingsParams);
      if (importSettings?.message || !importSettings?.data) {
        // handle some error
        return;
      }

      await this.#get().setSettings(importSettings.data);
    }
  };

  internal_createSignal = (): AbortController => {
    const abortController = this.#get().updateSettingsSignal;
    if (abortController && !abortController.signal.aborted)
      abortController.abort(MESSAGE_CANCEL_FLAT);

    const newSignal = new AbortController();

    this.#set({ updateSettingsSignal: newSignal }, false, 'signalForUpdateSettings');

    return newSignal;
  };

  resetSettings = async (): Promise<void> => {
    await userService.resetUserSettings();
    await this.#get().refreshUserState();
  };

  setSettings = async (settings: PartialDeep<UserSettings>): Promise<void> => {
    const { settings: prevSetting, defaultSettings } = this.#get();

    const nextSettings = merge(prevSetting, settings);

    if (isEqual(prevSetting, nextSettings)) return;

    const diffs = difference(nextSettings, defaultSettings);
    const isEmptyObjectDiff = (value: unknown): boolean =>
      !!value &&
      typeof value === 'object' &&
      !Array.isArray(value) &&
      Object.keys(value as object).length === 0;

    // When user resets a field to default value, we need to explicitly include it in diffs
    // to override the previously saved non-default value in the backend
    const changedFields = difference(nextSettings, prevSetting);
    for (const key of Object.keys(changedFields)) {
      // Only handle fields that were previously set by user (exist in prevSetting)
      const keyDiff = (diffs as any)[key];
      if (key in prevSetting && (!(key in diffs) || isEmptyObjectDiff(keyDiff))) {
        (diffs as any)[key] = (changedFields as any)[key];
      }
    }

    const nextDefaultAgentConfig = nextSettings.defaultAgent?.config;
    const changedDefaultAgentConfig = changedFields.defaultAgent?.config;
    const hasDefaultAgentModelProviderChange =
      !!changedDefaultAgentConfig &&
      ('model' in changedDefaultAgentConfig || 'provider' in changedDefaultAgentConfig);
    const defaultAgentModelProviderDiffersFromDefault =
      nextDefaultAgentConfig?.model !== defaultSettings.defaultAgent?.config?.model ||
      nextDefaultAgentConfig?.provider !== defaultSettings.defaultAgent?.config?.provider;

    if (
      hasDefaultAgentModelProviderChange &&
      (defaultAgentModelProviderDiffersFromDefault || 'defaultAgent' in prevSetting) &&
      nextDefaultAgentConfig?.model &&
      nextDefaultAgentConfig.provider
    ) {
      const defaultAgentDiff = diffs.defaultAgent || {};
      const configDiff = defaultAgentDiff.config || {};

      diffs.defaultAgent = {
        ...defaultAgentDiff,
        config: {
          ...configDiff,
          model: nextDefaultAgentConfig.model,
          provider: nextDefaultAgentConfig.provider,
        },
      };
    }

    const changedSystemAgent = changedFields.systemAgent as SystemAgentDiff | undefined;
    const nextSystemAgent = nextSettings.systemAgent;
    const previousSystemAgent = prevSetting.systemAgent;
    const defaultSystemAgent = defaultSettings.systemAgent;

    if (changedSystemAgent && nextSystemAgent) {
      const mutableDiffs = diffs as PartialDeep<UserSettings> & { systemAgent?: SystemAgentDiff };

      for (const key of Object.keys(changedSystemAgent)) {
        const changedSystemAgentItem = changedSystemAgent[key];
        if (
          !changedSystemAgentItem ||
          typeof changedSystemAgentItem !== 'object' ||
          Array.isArray(changedSystemAgentItem) ||
          (!('model' in changedSystemAgentItem) && !('provider' in changedSystemAgentItem))
        )
          continue;

        const taskKey = key as UserSystemAgentConfigKey;
        const nextSystemAgentItem = nextSystemAgent[taskKey];
        const defaultSystemAgentItem = defaultSystemAgent?.[taskKey];
        const systemAgentModelProviderDiffersFromDefault =
          nextSystemAgentItem?.model !== defaultSystemAgentItem?.model ||
          nextSystemAgentItem?.provider !== defaultSystemAgentItem?.provider;

        if (
          (!systemAgentModelProviderDiffersFromDefault &&
            (!previousSystemAgent || !Object.hasOwn(previousSystemAgent, taskKey))) ||
          !nextSystemAgentItem?.model ||
          !nextSystemAgentItem.provider
        )
          continue;

        const systemAgentDiff = mutableDiffs.systemAgent || {};
        const systemAgentItemDiff = systemAgentDiff[taskKey] || {};

        mutableDiffs.systemAgent = {
          ...systemAgentDiff,
          [taskKey]: {
            ...systemAgentItemDiff,
            model: nextSystemAgentItem.model,
            provider: nextSystemAgentItem.provider,
          },
        };
      }
    }

    this.#set({ settings: diffs }, false, 'optimistic_updateSettings');

    const abortController = this.#get().internal_createSignal();
    await userService.updateUserSettings(diffs, abortController.signal);
    await this.#get().refreshUserState();
  };

  updateDefaultAgent = async (defaultAgent: PartialDeep<LobeAgentSettings>): Promise<void> => {
    const config = defaultAgent.config;
    const shouldNormalizeModelProvider =
      config && (config.model !== undefined || config.provider !== undefined);

    if (!shouldNormalizeModelProvider) {
      await this.#get().setSettings({ defaultAgent });
      return;
    }

    const currentConfig = settingsSelectors.defaultAgentConfig(this.#get());

    await this.#get().setSettings({
      defaultAgent: {
        ...defaultAgent,
        config: {
          ...config,
          model: config.model ?? currentConfig.model,
          provider: config.provider ?? currentConfig.provider,
        },
      },
    });
  };

  updateGeneralConfig = async (general: Partial<UserGeneralConfig>): Promise<void> => {
    await this.#get().setSettings({ general });
  };

  updateHumanIntervention = async (config: {
    allowList?: string[];
    approvalMode?: 'auto-run' | 'allow-list' | 'manual';
  }): Promise<void> => {
    const current = this.#get().settings.tool?.humanIntervention || {};
    await this.#get().setSettings({
      tool: {
        humanIntervention: { ...current, ...config },
      },
    });
  };

  updateKeyVaults = async (keyVaults: Partial<UserKeyVaults>): Promise<void> => {
    await this.#get().setSettings({ keyVaults });
  };

  updateSystemAgent = async (
    key: UserServiceModelConfigKey,
    value: Partial<SystemAgentItem>,
  ): Promise<void> => {
    await this.#get().setSettings({
      systemAgent: { [key]: { ...value } },
    });
  };
}

export type UserSettingsAction = Pick<UserSettingsActionImpl, keyof UserSettingsActionImpl>;
