import { v4 as uuidv4 } from 'uuid';
import { Response, NextFunction } from 'express';
import {
  AuthenticatedServiceRequest,
  AuthenticatedUserRequest,
} from '../../../libs/middlewares/types';
import { KeyValueStoreService } from '../../../libs/services/keyValueStore.service';
import { Logger } from '../../../libs/services/logger.service';
import { configPaths } from '../paths/paths';
import {
  BadRequestError,
  ConflictError,
  ForbiddenError,
  InternalServerError,
  NotFoundError,
  ServiceUnavailableError,
  UnauthorizedError,
} from '../../../libs/errors/http.errors';
import {
  googleWorkspaceBusinessCredentialsSchema,
  googleWorkspaceIndividualCredentialsSchema,
} from '../validator/validators';
import { HTTP_STATUS } from '../../../libs/enums/http-status.enum';
import {
  aiModelRoute,
  AIServiceResponse,
  googleWorkspaceTypes,
  storageTypes,
} from '../constants/constants';
import { EncryptionService } from '../../../libs/encryptor/encryptor';
import { loadConfigurationManagerConfig } from '../config/config';
import { Org } from '../../user_management/schema/org.schema';

import { DefaultStorageConfig } from '../../tokens_manager/services/cm.service';
import { AppConfig } from '../../tokens_manager/config/config';
import { generateFetchConfigAuthToken } from '../../auth/utils/generateAuthToken';
import { SamlController } from '../../auth/controller/saml.controller';
import axios from 'axios';
import { ARANGO_DB_NAME, MONGO_DB_NAME } from '../../../libs/enums/db.enum';
import { ConfigService } from '../services/updateConfig.service';
import {
  AiConfigEventProducer,
  ConnectorPublicUrlChangedEvent,
  EmbeddingModelConfiguredEvent,
  EntitiesEventProducer,
  Event,
  EventType,
  GmailUpdatesDisabledEvent,
  GmailUpdatesEnabledEvent,
  LLMConfiguredEvent,
  SyncEventProducer,
} from '../services/kafka_events.service';
import {
  AICommandOptions,
  AIServiceCommand,
} from '../../../libs/commands/ai_service/ai.service.command';
import { HttpMethod } from '../../../libs/enums/http-methods.enum';
import { PLATFORM_FEATURE_FLAGS } from '../constants/constants';
import { getPlatformSettingsFromStore } from '../utils/util';
import { AIModelsConfig } from '../types/ai-models.types';
import { WebSearchConfig } from '../types/web-search.types';
import { WebSearchProviderConfiguration } from '../types/web-search.types';

const logger = Logger.getInstance({
  service: 'ConfigurationManagerController',
});
type SlackBotConfigEntry = {
  id: string;
  name: string;
  botToken: string;
  signingSecret: string;
  agentId?: string;
  createdAt: string;
  updatedAt: string;
};

type SlackBotStore = {
  configs: SlackBotConfigEntry[];
};

const AI_SERVICE_UNAVAILABLE_MESSAGE =
  'AI Service is currently unavailable. Please check your network connection or try again later.';

const DEFAULT_WEB_SEARCH_SETTINGS = Object.freeze({
  includeImages: false,
  maxImages: 3,
});

const normalizeWebSearchSettings = (
  settings?: Partial<{ includeImages: unknown; maxImages: unknown }>,
) => {
  const includeImages =
    typeof settings?.includeImages === 'boolean'
      ? settings.includeImages
      : DEFAULT_WEB_SEARCH_SETTINGS.includeImages;
  const parsedMaxImages = Number(settings?.maxImages);
  const maxImages =
    Number.isInteger(parsedMaxImages) &&
    parsedMaxImages >= 1 &&
    parsedMaxImages <= 500
      ? parsedMaxImages
      : DEFAULT_WEB_SEARCH_SETTINGS.maxImages;

  return {
    includeImages,
    maxImages,
  };
};

const handleBackendError = (error: any, operation: string): Error => {
  if (
    (error?.cause && error.cause.code === 'ECONNREFUSED') ||
    (typeof error?.message === 'string' &&
      error.message.includes('fetch failed'))
  ) {
    return new ServiceUnavailableError(AI_SERVICE_UNAVAILABLE_MESSAGE, error);
  }

  if (error.response) {
    const { status, data } = error.response;
    const errorDetail =
      data?.detail || data?.reason || data?.message || 'Unknown error';

    logger.error(`Backend error during ${operation}`, {
      status,
      errorDetail,
      fullResponse: data,
    });

    if (errorDetail === 'ECONNREFUSED') {
      throw new ServiceUnavailableError(AI_SERVICE_UNAVAILABLE_MESSAGE, error);
    }

    switch (status) {
      case 400:
        return new BadRequestError(errorDetail);
      case 401:
        return new UnauthorizedError(errorDetail);
      case 403:
        return new ForbiddenError(errorDetail);
      case 404:
        return new NotFoundError(errorDetail);
      case 500:
        return new InternalServerError(errorDetail);
      default:
        return new InternalServerError(`Backend error: ${errorDetail}`);
    }
  }

  if (error.request) {
    logger.error(`No response from backend during ${operation}`);
    return new InternalServerError('Backend service unavailable');
  }

  return new InternalServerError(`${operation} failed: ${error.message}`);
};

const normalizeUrl = (url: unknown): string => {
  if (!url || typeof url !== 'string') return '';
  const trimmed = String(url).trim();
  return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
};

function getOrgIdFromRequest(
  req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
): string | undefined {
  return (
    (req as AuthenticatedUserRequest).user?.orgId ||
    (req as AuthenticatedServiceRequest).tokenPayload?.orgId
  );
}

export const createStorageConfig =
  (
    keyValueStoreService: KeyValueStoreService,
    defaultConfig: DefaultStorageConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const storageType = req.body.storageType;
      let config: Record<string, any> = {};
      // config coming from file
      config = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      // Process configuration based on storage type
      switch (storageType.toLowerCase()) {
        case storageTypes.S3.toLowerCase(): {
          const s3Config = {
            accessKeyId: config.s3AccessKeyId,
            secretAccessKey: config.s3SecretAccessKey,
            region: config.s3Region,
            bucketName: config.s3BucketName,
          };
          const encryptedS3Config = EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).encrypt(JSON.stringify(s3Config));

          await keyValueStoreService.set<string>(
            configPaths.storageService,
            JSON.stringify({
              storageType: storageTypes.S3,
              s3: encryptedS3Config,
            }),
          );

          logger.info('S3 storage configuration saved successfully');
          break;
        }

        case storageTypes.AZURE_BLOB.toLowerCase(): {
          if (config.azureBlobConnectionString) {
            const encryptedAzureBlobConnectionString =
              EncryptionService.getInstance(
                configManagerConfig.algorithm,
                configManagerConfig.secretKey,
              ).encrypt(config.azureBlobConnectionString);

            await keyValueStoreService.set<string>(
              configPaths.storageService,
              JSON.stringify({
                storageType: storageTypes.AZURE_BLOB,
                azureBlob: encryptedAzureBlobConnectionString,
              }),
            );
          } else {
            const azureBlobConfig = {
              endpointProtocol: config.endpointProtocol || 'https',
              accountName: config.accountName,
              accountKey: config.accountKey,
              endpointSuffix: config.endpointSuffix || 'core.windows.net',
              containerName: config.containerName,
            };
            const encryptedAzureBlobConfig = EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).encrypt(JSON.stringify(azureBlobConfig));

            await keyValueStoreService.set<string>(
              configPaths.storageService,
              JSON.stringify({
                storageType: storageTypes.AZURE_BLOB,
                azureBlob: encryptedAzureBlobConfig,
              }),
            );
          }
          logger.info('Azure Blob storage configuration saved successfully');
          break;
        }

        case storageTypes.LOCAL.toLowerCase(): {
          const localConfig = {
            mountName: config.mountName || 'PipesHub',
            baseUrl: config.baseUrl || defaultConfig.endpoint,
          };
          await keyValueStoreService.set<string>(
            configPaths.storageService,
            JSON.stringify({
              storageType: storageTypes.LOCAL,
              local: JSON.stringify(localConfig),
            }),
          );

          logger.info('Local storage configuration saved successfully');
          break;
        }

        default:
          throw new BadRequestError(`Unsupported storage type: ${storageType}`);
      }
      res.status(200).json({
        message: 'Storage configuration saved successfully',
      });
    } catch (error: any) {
      logger.error('Error creating storage config', { error });
      next(error);
    }
  };

export const getStorageConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    _req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const storageConfig =
        (await keyValueStoreService.get<string>(configPaths.storageService)) ||
        '{}';

      const parsedConfig = JSON.parse(storageConfig); // Parse JSON string

      const storageType = parsedConfig.storageType;

      if (!storageType) {
        throw new BadRequestError('Storage type not found');
      }

      const configManagerConfig = loadConfigurationManagerConfig();

      if (storageType === storageTypes.S3) {
        const encryptedS3Config = parsedConfig.s3;

        if (encryptedS3Config) {
          const s3Config = EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedS3Config);

          const { accessKeyId, secretAccessKey, region, bucketName } =
            JSON.parse(s3Config);
          res
            .status(200)
            .json({
              storageType,
              accessKeyId,
              secretAccessKey,
              region,
              bucketName,
            })
            .end();
          return;
        } else {
          throw new BadRequestError('Storage config not found');
        }
      }

      if (storageType === storageTypes.AZURE_BLOB) {
        const encryptedAzureBlobConfig = parsedConfig.azureBlob;
        if (encryptedAzureBlobConfig) {
          const azureBlobConfig = JSON.parse(
            EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).decrypt(encryptedAzureBlobConfig),
          );

          const {
            endpointProtocol,
            accountName,
            accountKey,
            endpointSuffix,
            containerName,
          } = azureBlobConfig;
          res
            .status(200)
            .json({
              storageType,
              endpointProtocol,
              accountName,
              accountKey,
              endpointSuffix,
              containerName,
            })
            .end();
          return;
        } else {
          throw new BadRequestError('Storage config not found');
        }
      }

      if (storageType === storageTypes.LOCAL) {
        const localConfig = parsedConfig.local;
        res
          .status(200)
          .json(JSON.parse(localConfig || '{}'))
          .end();
        return;
      }

      res.status(HTTP_STATUS.BAD_REQUEST).json({
        message: 'Unsupported storage type',
      });
    } catch (error: any) {
      logger.error('Error getting storage config', { error });
      next(error);
    }
  };

export const createSmtpConfig =
  (
    keyValueStoreService: KeyValueStoreService,
    communicationBackend: string,
    scopedJwtSecret: string,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      if (!req.user) {
        throw new UnauthorizedError('User not Found');
      }
      const smtpConfig = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedSmtpConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(smtpConfig));
      await keyValueStoreService.set<string>(
        configPaths.smtp,
        encryptedSmtpConfig,
      );
      const config = {
        method: 'post' as const,
        url: `${communicationBackend}/api/v1/mail/updateSmtpConfig`,
        headers: {
          Authorization: `Bearer ${await generateFetchConfigAuthToken(req.user, scopedJwtSecret)}`,
          'Content-Type': 'application/json',
        },
      };

      const response = await axios(config);
      if (response.status != 200) {
        throw new BadRequestError('Error setting smtp config');
      }

      res
        .status(200)
        .json({ message: 'SMTP config created successfully' })
        .end();
    } catch (error: any) {
      logger.error('Error creating smtp config', { error });
      next(error);
    }
  };

export const getSmtpConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedSmtpConfig = await keyValueStoreService.get<string>(
        configPaths.smtp,
      );
      if (encryptedSmtpConfig) {
        const smtpConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedSmtpConfig),
        );
        res.status(200).json(smtpConfig).end();
        return;
      }
      res.status(200).json({}).end();
    } catch (error: any) {
      logger.error('Error getting smtp config', { error });
      next(error);
    }
  };
const SLACK_BOT_CAS_MAX_RETRIES = 5;

const parseSlackBotStore = (
  encrypted: string | null | undefined,
  configManagerConfig: ReturnType<typeof loadConfigurationManagerConfig>,
): SlackBotStore => {
  if (!encrypted) {
    return { configs: [] };
  }

  try {
    const decrypted = EncryptionService.getInstance(
      configManagerConfig.algorithm,
      configManagerConfig.secretKey,
    ).decrypt(encrypted);

    const parsed = JSON.parse(decrypted) as Partial<SlackBotStore>;
    return {
      configs: Array.isArray(parsed.configs) ? parsed.configs : [],
    };
  } catch (error) {
    logger.warn('Failed to parse slack bot settings, using empty config', { error });
    return { configs: [] };
  }
};

export const getSlackBotStore = async (
  keyValueStoreService: KeyValueStoreService,
): Promise<SlackBotStore> => {
  const configManagerConfig = loadConfigurationManagerConfig();
  const encrypted = await keyValueStoreService.get<string>(configPaths.slackBot);
  return parseSlackBotStore(encrypted, configManagerConfig);
};

const updateSlackBotStoreWithCAS = async <T>(
  keyValueStoreService: KeyValueStoreService,
  updater: (store: SlackBotStore) => T,
): Promise<T> => {
  const configManagerConfig = loadConfigurationManagerConfig();

  for (let attempt = 0; attempt < SLACK_BOT_CAS_MAX_RETRIES; attempt++) {
    const encryptedCurrent = await keyValueStoreService.get<string>(configPaths.slackBot);
    const store = parseSlackBotStore(encryptedCurrent, configManagerConfig);
    const result = updater(store);

    const encryptedUpdated = EncryptionService.getInstance(
      configManagerConfig.algorithm,
      configManagerConfig.secretKey,
    ).encrypt(JSON.stringify(store));

    const casSuccess = await keyValueStoreService.compareAndSet<string>(
      configPaths.slackBot,
      encryptedCurrent ?? null,
      encryptedUpdated,
    );

    if (casSuccess) {
      return result;
    }

    if (attempt === SLACK_BOT_CAS_MAX_RETRIES - 1) {
      throw new Error(
        'Failed to update Slack bot config due to concurrent modification. Please try again.',
      );
    }

    await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
  }

  throw new Error('Failed to update Slack bot config.');
};

const slackBotConfig = (config: SlackBotConfigEntry) => ({
  id: config.id,
  name: config.name,
  agentId: config.agentId ?? null,
  createdAt: config.createdAt,
  updatedAt: config.updatedAt,
  botToken: config.botToken,
  signingSecret: config.signingSecret,
});

export const getSlackBotConfigs =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const store = await getSlackBotStore(keyValueStoreService);
      res
        .status(HTTP_STATUS.OK)
        .json({
          status: 'success',
          configs: store.configs.map(slackBotConfig),
        })
        .end();
    } catch (error: any) {
      logger.error('Error getting slack bot configs', { error });
      next(error);
    }
  };

export const createSlackBotConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { name, botToken, signingSecret, agentId } = req.body;
      const normalizedAgentId =
        typeof agentId === 'string' && agentId.trim().length > 0
          ? agentId.trim()
          : undefined;
      const config = await updateSlackBotStoreWithCAS(
        keyValueStoreService,
        (store): SlackBotConfigEntry => {
          if (normalizedAgentId) {
            const duplicate = store.configs.find(
              (configItem) => configItem.agentId === normalizedAgentId,
            );
            if (duplicate) {
              throw new BadRequestError(
                'Selected agent is already linked to another Slack Bot configuration',
              );
            }
          }

          const timestamp = new Date().toISOString();
          const createdConfig: SlackBotConfigEntry = {
            id: uuidv4(),
            name,
            botToken,
            signingSecret,
            agentId: normalizedAgentId,
            createdAt: timestamp,
            updatedAt: timestamp,
          };

          store.configs.push(createdConfig);
          return createdConfig;
        },
      );

      res
        .status(HTTP_STATUS.OK)
        .json({
          status: 'success',
          config: slackBotConfig(config),
        })
        .end();
    } catch (error: any) {
      logger.error('Error creating slack bot config', { error });
      next(error);
    }
  };

export const updateSlackBotConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { configId } = req.params;
      const { name, botToken, signingSecret, agentId } = req.body;
      const normalizedAgentId =
        typeof agentId === 'string' && agentId.trim().length > 0
          ? agentId.trim()
          : undefined;
      const updatedConfig = await updateSlackBotStoreWithCAS(
        keyValueStoreService,
        (store): SlackBotConfigEntry => {
          const configIndex = store.configs.findIndex((config) => config.id === configId);

          if (configIndex === -1) {
            throw new NotFoundError('Slack Bot configuration not found');
          }

          if (normalizedAgentId) {
            const duplicate = store.configs.find(
              (configItem) =>
                configItem.agentId === normalizedAgentId && configItem.id !== configId,
            );
            if (duplicate) {
              throw new BadRequestError(
                'Selected agent is already linked to another Slack Bot configuration',
              );
            }
          }

          const previousConfig = store.configs[configIndex];
          if (!previousConfig) {
            throw new Error("Config not found");
          }
          const nextConfig: SlackBotConfigEntry = {
            ...previousConfig,
            name,
            botToken,
            signingSecret,
            agentId: normalizedAgentId,
            updatedAt: new Date().toISOString(),
          };

          store.configs[configIndex] = nextConfig;
          return nextConfig;
        },
      );

      res
        .status(HTTP_STATUS.OK)
        .json({
          status: 'success',
          config: slackBotConfig(updatedConfig),
        })
        .end();
    } catch (error: any) {
      logger.error('Error updating slack bot config', { error });
      next(error);
    }
  };

export const deleteSlackBotConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { configId } = req.params;
      await updateSlackBotStoreWithCAS(keyValueStoreService, (store) => {
        const configIndex = store.configs.findIndex((config) => config.id === configId);

        if (configIndex === -1) {
          throw new NotFoundError('Slack Bot configuration not found');
        }

        store.configs.splice(configIndex, 1);
      });

      res
        .status(HTTP_STATUS.OK)
        .json({
          status: 'success',
          message: 'Slack Bot configuration deleted',
        })
        .end();
    } catch (error: any) {
      logger.error('Error deleting slack bot config', { error });
      next(error);
    }
  };

// Platform settings
export const setPlatformSettings =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { fileUploadMaxSizeBytes, featureFlags } = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedPlatformSettings = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(
        JSON.stringify({
          fileUploadMaxSizeBytes,
          featureFlags,
          updatedAt: new Date().toISOString(),
        }),
      );

      await keyValueStoreService.set<string>(
        configPaths.platform.settings,
        encryptedPlatformSettings,
      );

      res.status(200).json({ message: 'Platform settings saved' }).end();
    } catch (error: any) {
      logger.error('Error setting platform settings', { error });
      next(error);
    }
  };

export const getPlatformSettings =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    _req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const settings = await getPlatformSettingsFromStore(keyValueStoreService);
      res.status(200).json(settings).end();
    } catch (error: any) {
      logger.error('Error getting platform settings', { error });
      next(error);
    }
  };

export const getAvailablePlatformFeatureFlags =
  () =>
  async (
    _req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    _next: NextFunction,
  ) => {
    // Only expose user-toggleable flags. Hidden flags are still seeded with
    // their defaults by getPlatformSettingsFromStore but never surface in the
    // Labs UI.
    const flags = PLATFORM_FEATURE_FLAGS.filter((f) => !f.hidden);
    res.status(200).json({ flags }).end();
  };

export const getAzureAdAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const encryptedAuthConfig = await keyValueStoreService.get<string>(
        configPaths.auth.azureAD,
      );

      if (encryptedAuthConfig) {
        const authConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAuthConfig),
        );
        res.status(200).json(authConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting auth config', { error });
      next(error);
    }
  };

export const setAzureAdAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const { clientId, tenantId, enableJit } = req.body;
      const authority = `https://login.microsoftonline.com/${tenantId}`;

      const encryptedAuthConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ clientId, tenantId, authority, enableJit: enableJit ?? true }));

      await keyValueStoreService.set<string>(
        configPaths.auth.azureAD,
        encryptedAuthConfig,
      );

      res
        .status(200)
        .json({ message: 'Azure AD config created successfully' })
        .end();
    } catch (error: any) {
      logger.error('Error creating smtp config', { error });
      next(error);
    }
  };

export const getMicrosoftAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const encryptedAuthConfig = await keyValueStoreService.get<string>(
        configPaths.auth.microsoft,
      );

      if (encryptedAuthConfig) {
        const authConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAuthConfig),
        );
        res.status(200).json(authConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting auth config', { error });
      next(error);
    }
  };

export const setMicrosoftAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const { clientId, tenantId, enableJit } = req.body;
      const authority = `https://login.microsoftonline.com/${tenantId}`;

      const encryptedAuthConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ clientId, tenantId, authority, enableJit: enableJit ?? true }));

      await keyValueStoreService.set<string>(
        configPaths.auth.microsoft,
        encryptedAuthConfig,
      );

      res
        .status(200)
        .json({ message: 'Microsoft Auth config created successfully' })
        .end();
    } catch (error: any) {
      logger.error('Error creating smtp config', { error });
      next(error);
    }
  };

export const getGoogleAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const encryptedAuthConfig = await keyValueStoreService.get<string>(
        configPaths.auth.google,
      );

      if (encryptedAuthConfig) {
        const authConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAuthConfig),
        );
        res.status(200).json(authConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting auth config', { error });
      next(error);
    }
  };

export const setGoogleAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const { clientId, enableJit } = req.body;

      const encryptedAuthConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ clientId, enableJit: enableJit ?? true }));

      await keyValueStoreService.set<string>(
        configPaths.auth.google,
        encryptedAuthConfig,
      );

      res
        .status(200)
        .json({ message: 'Google Auth config created successfully' })
        .end();
    } catch (error: any) {
      logger.error('Error creating smtp config', { error });
      next(error);
    }
  };

export const getOAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const encryptedAuthConfig = await keyValueStoreService.get<string>(
        configPaths.auth.oauth,
      );

      if (encryptedAuthConfig) {
        const authConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAuthConfig),
        );
        res.status(200).json(authConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting OAuth config', { error });
      next(error);
    }
  };

export const setOAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const {
        providerName,
        clientId,
        clientSecret,
        authorizationUrl,
        tokenEndpoint,
        userInfoEndpoint,
        scope,
        redirectUri,
        enableJit,
      } = req.body;

      const oauthConfig = {
        providerName,
        clientId,
        ...(clientSecret && { clientSecret }),
        ...(authorizationUrl && { authorizationUrl }),
        ...(tokenEndpoint && { tokenEndpoint }),
        ...(userInfoEndpoint && { userInfoEndpoint }),
        ...(scope && { scope }),
        ...(redirectUri && { redirectUri }),
        enableJit: enableJit ?? true,
      };

      const encryptedAuthConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(oauthConfig));

      await keyValueStoreService.set<string>(
        configPaths.auth.oauth,
        encryptedAuthConfig,
      );

      res
        .status(200)
        .json({ message: 'OAuth config created successfully' })
        .end();
    } catch (error: any) {
      logger.error('Error creating OAuth config', { error });
      next(error);
    }
  };

export const createArangoDbConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { url, username, password } = req.body;
      const db = ARANGO_DB_NAME;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedArangoDBConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ url, username, password, db }));
      await keyValueStoreService.set<string>(
        configPaths.db.arangodb,
        encryptedArangoDBConfig,
      );

      res
        .status(200)
        .json({
          message: 'Arango DB config created successfully',
        })
        .end();
    } catch (error: any) {
      logger.error('Error creating db config', { error });
      next(error);
    }
  };

export const getArangoDbConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedArangoDBConfig = await keyValueStoreService.get<string>(
        configPaths.db.arangodb,
      );
      if (encryptedArangoDBConfig) {
        const arangoDBConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedArangoDBConfig),
        );
        res.status(200).json(arangoDBConfig).end();
        return;
      }
      res.status(200).json({}).end();
    } catch (error: any) {
      logger.error('Error getting db config', { error });
      next(error);
    }
  };

export const createMongoDbConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { uri } = req.body;
      const db = MONGO_DB_NAME;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedMongoDBConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ uri, db }));
      await keyValueStoreService.set<string>(
        configPaths.db.mongodb,
        encryptedMongoDBConfig,
      );

      res
        .status(200)
        .json({
          message: 'Mongo DB config created successfully',
        })
        .end();
    } catch (error: any) {
      logger.error('Error creating db config', { error });
      next(error);
    }
  };

export const getMongoDbConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();

      const encryptedMongoDBConfig = await keyValueStoreService.get<string>(
        configPaths.db.mongodb,
      );
      if (encryptedMongoDBConfig) {
        const mongoDBConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedMongoDBConfig),
        );

        res.status(200).json(mongoDBConfig).end();
        return;
      }
      res.status(200).json({}).end();
    } catch (error: any) {
      logger.error('Error getting db config', { error });
      next(error);
    }
  };

export const createRedisConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { host, port, password, tls } = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedRedisConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ host, port, password, tls }));
      await keyValueStoreService.set<string>(
        configPaths.keyValueStore.redis,
        encryptedRedisConfig,
      );
      res.status(200).json({ message: 'Redis config created successfully' });
    } catch (error: any) {
      logger.error('Error creating key value store config', { error });
      next(error);
    }
  };

export const getRedisConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedRedisConfig = await keyValueStoreService.get<string>(
        configPaths.keyValueStore.redis,
      );
      if (encryptedRedisConfig) {
        const redisConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedRedisConfig),
        );

        res.status(200).json(redisConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting key value store config', { error });
      next(error);
    }
  };

export const createKafkaConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { brokers, sasl } = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedKafkaConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ brokers, sasl }));
      await keyValueStoreService.set<string>(
        configPaths.broker.kafka,
        encryptedKafkaConfig,
      );
      const warningMessage = res.getHeader('warning');
      res
        .status(200)
        .json({ message: 'Kafka config created successfully', warningMessage })
        .end();
    } catch (error: any) {
      logger.error('Error creating kafka config', { error });
      next(error);
    }
  };

export const getKafkaConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedKafkaConfig = await keyValueStoreService.get<string>(
        configPaths.broker.kafka,
      );
      if (encryptedKafkaConfig) {
        const kafkaConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedKafkaConfig),
        );

        res.status(200).json(kafkaConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting kafka config', { error });
      next(error);
    }
  };

export const createGoogleWorkspaceCredentials =
  (
    keyValueStoreService: KeyValueStoreService,
    userId: string,
    orgId: string,
    eventService: SyncEventProducer,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const org = await Org.findOne({ orgId, isDeleted: false });
      if (!org) {
        throw new BadRequestError('Organisaton not found');
      }
      const userType = org.accountType;

      let configData;
      const configManagerConfig = loadConfigurationManagerConfig();
      let encryptedGoogleWorkspaceConfig: string;
      switch (userType.toLowerCase()) {
        case googleWorkspaceTypes.INDIVIDUAL.toLowerCase():
          {
            configData = req.body;

            // validate config schema

            const validationResult =
              googleWorkspaceIndividualCredentialsSchema.safeParse(configData);
            if (!validationResult.success) {
              throw new BadRequestError(validationResult.error.message);
            }
            const enableRealTimeUpdates = req.body.enableRealTimeUpdates;
            let topicName = '';
            const realTimeUpdatesEnabled =
              typeof enableRealTimeUpdates === 'string'
                ? enableRealTimeUpdates.toLowerCase() === 'true'
                : !!enableRealTimeUpdates;

            if (realTimeUpdatesEnabled) {
              if (!req.body.topicName) {
                throw new BadRequestError(
                  'Topic name is required when real-time updates are enabled',
                );
              }
              topicName = req.body.topicName;
            }
            const {
              access_token,
              refresh_token,
              access_token_expiry_time,
              refresh_token_expiry_time,
            } = configData;

            encryptedGoogleWorkspaceConfig = EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).encrypt(
              JSON.stringify({
                access_token,
                refresh_token,
                access_token_expiry_time,
                refresh_token_expiry_time,
                enableRealTimeUpdates: realTimeUpdatesEnabled,
                topicName,
              }),
            );
          }
          await keyValueStoreService.set<string>(
            `${configPaths.connectors.googleWorkspace.credentials.individual}/${userId}`,
            encryptedGoogleWorkspaceConfig,
          );
          break;
        case googleWorkspaceTypes.BUSINESS.toLowerCase(): {
          const fileChanged =
            req.body.fileChanged === true || req.body.fileChanged === 'true';
          let existingConfig = null;
          // validate config schema
          if (!fileChanged) {
            try {
              const encryptedExistingConfig =
                await keyValueStoreService.get<string>(
                  `${configPaths.connectors.googleWorkspace.credentials.business}/${orgId}`,
                );

              if (encryptedExistingConfig) {
                existingConfig = JSON.parse(
                  EncryptionService.getInstance(
                    configManagerConfig.algorithm,
                    configManagerConfig.secretKey,
                  ).decrypt(encryptedExistingConfig),
                );

                // We'll use this existing config later
                logger.debug('Using existing config, file not changed');
              } else {
                // No existing config found, need to validate the new file
                throw new BadRequestError('File Not found');
              }
            } catch (error) {
              throw error;
            }
          }

          // Validate admin email regardless of whether file changed
          if (!req.body.adminEmail) {
            throw new BadRequestError(
              'Google Workspace Admin Email is required',
            );
          }
          const adminEmail = req.body.adminEmail;

          // Process real-time updates settings
          const enableRealTimeUpdates = req.body.enableRealTimeUpdates;
          let topicName = '';
          const realTimeUpdatesEnabled =
            enableRealTimeUpdates === undefined
              ? false
              : typeof enableRealTimeUpdates === 'string'
                ? enableRealTimeUpdates.toLowerCase() === 'true'
                : Boolean(enableRealTimeUpdates);

          if (realTimeUpdatesEnabled) {
            if (!req.body.topicName) {
              throw new BadRequestError(
                'Topic name is required when real-time updates are enabled',
              );
            }
            topicName = req.body.topicName;
          }

          logger.debug('enableRealTimeUpdates:', enableRealTimeUpdates);
          logger.debug('realTimeUpdatesEnabled:', realTimeUpdatesEnabled);
          logger.debug('topicName:', topicName);

          let configData;

          if (existingConfig) {
            if (
              existingConfig.topicName != topicName ||
              existingConfig.enableRealTimeUpdates != realTimeUpdatesEnabled
            ) {
              if (realTimeUpdatesEnabled) {
                await eventService.start();
                const event: Event = {
                  eventType: EventType.GmailUpdatesEnabledEvent,
                  timestamp: Date.now(),
                  payload: {
                    orgId,
                    topicName: req.body.topicName,
                  } as GmailUpdatesEnabledEvent,
                };
                await eventService.publishEvent(event);
                await eventService.stop();
              } else {
                await eventService.start();
                const event: Event = {
                  eventType: EventType.GmailUpdatesDisabledEvent,
                  timestamp: Date.now(),
                  payload: {
                    orgId,
                  } as GmailUpdatesDisabledEvent,
                };
                await eventService.publishEvent(event);
                await eventService.stop();
              }
            }
          } else {
            if (realTimeUpdatesEnabled) {
              await eventService.start();
              const event: Event = {
                eventType: EventType.GmailUpdatesEnabledEvent,
                timestamp: Date.now(),
                payload: {
                  orgId,
                  topicName: req.body.topicName,
                } as GmailUpdatesEnabledEvent,
              };
              await eventService.publishEvent(event);
              await eventService.stop();
            }
          }

          if (fileChanged) {
            // Only validate the file if it's changed
            configData = req.body.fileContent;

            const validationResult =
              googleWorkspaceBusinessCredentialsSchema.safeParse(configData);

            if (!validationResult.success) {
              const formattedErrors = validationResult.error.errors
                .map((err) => {
                  const fieldName = err.path[0] || 'Unknown field';
                  return `  • ${fieldName}: ${err.message}  `;
                })
                .join('');

              const errorMessage = `Google Workspace validation failed:\n${formattedErrors}`;
              throw new BadRequestError(errorMessage);
            }
          } else {
            // Use existing file data but with updated settings
            configData = {
              type: existingConfig.type,
              project_id: existingConfig.project_id,
              private_key_id: existingConfig.private_key_id,
              private_key: existingConfig.private_key,
              client_email: existingConfig.client_email,
              client_id: existingConfig.client_id,
              auth_uri: existingConfig.auth_uri,
              token_uri: existingConfig.token_uri,
              auth_provider_x509_cert_url:
                existingConfig.auth_provider_x509_cert_url,
              client_x509_cert_url: existingConfig.client_x509_cert_url,
              universe_domain: existingConfig.universe_domain,
            };
          }

          // Combine file data with updated settings
          const {
            type,
            project_id,
            private_key_id,
            private_key,
            client_email,
            client_id,
            auth_uri,
            token_uri,
            auth_provider_x509_cert_url,
            client_x509_cert_url,
            universe_domain,
          } = configData;

          // Encrypt and store the updated config
          encryptedGoogleWorkspaceConfig = EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).encrypt(
            JSON.stringify({
              type,
              project_id,
              private_key_id,
              private_key,
              client_email,
              client_id,
              auth_uri,
              token_uri,
              auth_provider_x509_cert_url,
              client_x509_cert_url,
              universe_domain,
              adminEmail,
              enableRealTimeUpdates: realTimeUpdatesEnabled,
              topicName,
            }),
          );

          await keyValueStoreService.set<string>(
            `${configPaths.connectors.googleWorkspace.credentials.business}/${orgId}`,
            encryptedGoogleWorkspaceConfig,
          );
          break;
        }
        default: {
          throw new BadRequestError(
            `Unsupported google workspace type: ${userType}`,
          );
        }
      }
      res.status(200).json({ message: 'Successfully updated' });
    } catch (error: any) {
      logger.error('Error creating google workspace credentials', { error });
      next(error);
    }
  };

export const getGoogleWorkspaceCredentials =
  (keyValueStoreService: KeyValueStoreService, userId: string, orgId: string) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const org = await Org.findOne({ orgId, isDeleted: false });
      if (!org) {
        throw new BadRequestError('Organisaton not found');
      }
      const userType = org.accountType;
      const configManagerConfig = loadConfigurationManagerConfig();
      let path;
      let googleWorkspaceCredentials: any;
      let encryptedGoogleWorkspaceCredentials;
      switch (userType.toLowerCase()) {
        case googleWorkspaceTypes.INDIVIDUAL.toLowerCase():
          path = `${configPaths.connectors.googleWorkspace.credentials.individual}/${userId}`;
          const oauthPath = `${configPaths.connectors.googleWorkspace.config}`;

          encryptedGoogleWorkspaceCredentials =
            await keyValueStoreService.get<string>(path);
          const encryptedGoogleWorkspaceOauthConfig =
            await keyValueStoreService.get<string>(oauthPath);

          if (encryptedGoogleWorkspaceOauthConfig) {
            const googleWorkspaceOauthConfig = JSON.parse(
              EncryptionService.getInstance(
                configManagerConfig.algorithm,
                configManagerConfig.secretKey,
              ).decrypt(encryptedGoogleWorkspaceOauthConfig),
            );
            if (encryptedGoogleWorkspaceCredentials) {
              googleWorkspaceCredentials = JSON.parse(
                EncryptionService.getInstance(
                  configManagerConfig.algorithm,
                  configManagerConfig.secretKey,
                ).decrypt(encryptedGoogleWorkspaceCredentials),
              );

              const combinedResponse = {
                ...googleWorkspaceCredentials,
                ...googleWorkspaceOauthConfig,
              };

              res.status(200).json(combinedResponse).end();
            } else {
              res.status(200).json({}).end();
            }
          } else {
            res.status(200).json({}).end();
          }

          break;

        case googleWorkspaceTypes.BUSINESS.toLowerCase():
          path = `${configPaths.connectors.googleWorkspace.credentials.business}/${orgId}`;
          encryptedGoogleWorkspaceCredentials =
            await keyValueStoreService.get<string>(path);
          if (encryptedGoogleWorkspaceCredentials) {
            googleWorkspaceCredentials = JSON.parse(
              EncryptionService.getInstance(
                configManagerConfig.algorithm,
                configManagerConfig.secretKey,
              ).decrypt(encryptedGoogleWorkspaceCredentials),
            );
            res.status(200).json(googleWorkspaceCredentials).end();
          } else {
            res.status(200).json({}).end();
          }
          break;

        default:
          throw new BadRequestError(
            `Unsupported google workspace type: ${userType}`,
          );
      }
    } catch (error: any) {
      logger.error('Error getting google workspace credentials', { error });
      next(error);
    }
  };

export const getGoogleWorkspaceBusinessCredentials =
  (keyValueStoreService: KeyValueStoreService, orgId: string) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      let path;
      let googleWorkspaceConfig: any;
      let encryptedGoogleWorkspaceConfig;

      path = `${configPaths.connectors.googleWorkspace.credentials.business}/${orgId}`;
      encryptedGoogleWorkspaceConfig =
        await keyValueStoreService.get<string>(path);
      if (encryptedGoogleWorkspaceConfig) {
        googleWorkspaceConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedGoogleWorkspaceConfig),
        );
        res.status(200).json(googleWorkspaceConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting google workspace credentials', { error });
      next(error);
    }
  };

export const deleteGoogleWorkspaceCredentials =
  (keyValueStoreService: KeyValueStoreService, orgId: string) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const org = await Org.findOne({ orgId, isDeleted: false });
      if (!org) {
        throw new BadRequestError('Organisaton not found');
      }
      const userType = org.accountType;
      let path;
      switch (userType.toLowerCase()) {
        case googleWorkspaceTypes.INDIVIDUAL.toLowerCase():
          throw new UnauthorizedError(
            'Deleting credentials fro individual type not allowed',
          );

        case googleWorkspaceTypes.BUSINESS.toLowerCase():
          path = `${configPaths.connectors.googleWorkspace.credentials.business}/${orgId}`;
          await keyValueStoreService.delete(path);
          res.status(200).json({}).end();
          break;

        default:
          throw new BadRequestError(
            `Unsupported google workspace type: ${userType}`,
          );
      }
    } catch (error: any) {
      logger.error('Error getting google workspace credentials', { error });
      next(error);
    }
  };
export const setGoogleWorkspaceOauthConfig =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: SyncEventProducer,
    orgId: string,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { clientId, clientSecret, enableRealTimeUpdates } = req.body;
      let topicName = '';
      const realTimeUpdatesEnabled =
        enableRealTimeUpdates === undefined
          ? false
          : typeof enableRealTimeUpdates === 'string'
            ? enableRealTimeUpdates.toLowerCase() === 'true'
            : Boolean(enableRealTimeUpdates);

      if (realTimeUpdatesEnabled) {
        if (!req.body.topicName) {
          throw new BadRequestError(
            'Topic name is required when real-time updates are enabled',
          );
        }
        topicName = req.body.topicName;
      }
      const configManagerConfig = loadConfigurationManagerConfig();
      const existingGoogleWorkSpaceConfig =
        await keyValueStoreService.get<string>(
          configPaths.connectors.googleWorkspace.config,
        );
      if (existingGoogleWorkSpaceConfig) {
        const googleWorkSpaceConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(existingGoogleWorkSpaceConfig),
        );
        if (
          googleWorkSpaceConfig.topicName != topicName ||
          googleWorkSpaceConfig.enableRealTimeUpdates != realTimeUpdatesEnabled
        ) {
          if (realTimeUpdatesEnabled) {
            await eventService.start();
            const event: Event = {
              eventType: EventType.GmailUpdatesEnabledEvent,
              timestamp: Date.now(),
              payload: {
                orgId,
                topicName: req.body.topicName,
              } as GmailUpdatesEnabledEvent,
            };
            await eventService.publishEvent(event);
            await eventService.stop();
          } else {
            await eventService.start();
            const event: Event = {
              eventType: EventType.GmailUpdatesDisabledEvent,
              timestamp: Date.now(),
              payload: {
                orgId,
              } as GmailUpdatesDisabledEvent,
            };
            await eventService.publishEvent(event);
            await eventService.stop();
          }
        }
      } else {
        if (realTimeUpdatesEnabled) {
          await eventService.start();
          const event: Event = {
            eventType: EventType.GmailUpdatesEnabledEvent,
            timestamp: Date.now(),
            payload: {
              orgId,
              topicName: req.body.topicName,
            } as GmailUpdatesEnabledEvent,
          };
          await eventService.publishEvent(event);
          await eventService.stop();
        }
      }

      const encryptedGoogleWorkSpaceConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(
        JSON.stringify({
          clientId,
          clientSecret,
          enableRealTimeUpdates: realTimeUpdatesEnabled,
          topicName,
        }),
      );
      await keyValueStoreService.set<string>(
        configPaths.connectors.googleWorkspace.config,
        encryptedGoogleWorkSpaceConfig,
      );

      res
        .status(200)
        .json({ message: 'Google Workspace credentials created successfully' });
    } catch (error: any) {
      logger.error('Error creating Google Workspace config', { error });
      next(error);
    }
  };

export const getGoogleWorkspaceOauthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedGoogleWorkSpaceConfig =
        await keyValueStoreService.get<string>(
          configPaths.connectors.googleWorkspace.config,
        );
      if (encryptedGoogleWorkSpaceConfig) {
        const googleWorkSpaceConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedGoogleWorkSpaceConfig),
        );
        res.status(200).json(googleWorkSpaceConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting Google Workspace config', { error });
      next(error);
    }
  };

export const getAtlassianOauthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisaton not found');
      }
      const encryptedAtlassianConfig = await keyValueStoreService.get<string>(
        `${configPaths.connectors.atlassian.config}/${orgId}`,
      );
      if (encryptedAtlassianConfig) {
        const atlassianConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAtlassianConfig),
        );
        res.status(200).json(atlassianConfig).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting Atlassian config', { error });
      next(error);
    }
  };

export const setAtlassianOauthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const oauthConfig = req.body;
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAtlassianConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(oauthConfig));
      await keyValueStoreService.set<string>(
        `${configPaths.connectors.atlassian.config}/${orgId}`,
        encryptedAtlassianConfig,
      );
      res
        .status(200)
        .json({ message: 'Atlassian config created successfully' });
    } catch (error: any) {
      logger.error('Error creating Atlassian config', { error });
      next(error);
    }
  };

export const getAtlassianCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      const encryptedAtlassianCredentials =
        await keyValueStoreService.get<string>(
          `${configPaths.connectors.atlassian.credentials}/${orgId}`,
        );
      if (encryptedAtlassianCredentials) {
        const atlassianCredentials = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAtlassianCredentials),
        );
        res.status(200).json(atlassianCredentials).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting Atlassian credentials', { error });
      next(error);
    }
  };

export const setAtlassianCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const credentials = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      // Todo: Do a health check for the credentials
      const encryptedAtlassianCredentials = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(credentials));
      await keyValueStoreService.set<string>(
        `${configPaths.connectors.atlassian.credentials}/${orgId}`,
        encryptedAtlassianCredentials,
      );
      res
        .status(200)
        .json({ message: 'Atlassian credentials created successfully' });
    } catch (error: any) {
      logger.error('Error creating Atlassian credentials', { error });
      next(error);
    }
  };

export const getOneDriveCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      const encryptedOneDriveCredentials =
        await keyValueStoreService.get<string>(
          `${configPaths.connectors.onedrive.config}/${orgId}`,
        );
      if (encryptedOneDriveCredentials) {
        const oneDriveCredentials = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedOneDriveCredentials),
        );
        res.status(200).json(oneDriveCredentials).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting OneDrive credentials', { error });
      next(error);
    }
  };

export const setOneDriveCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const credentials = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      // Todo: Do a health check for the credentials
      const encryptedOneDriveCredentials = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(credentials));
      await keyValueStoreService.set<string>(
        `${configPaths.connectors.onedrive.config}/${orgId}`,
        encryptedOneDriveCredentials,
      );
      res
        .status(200)
        .json({ message: 'OneDrive credentials created successfully' });
    } catch (error: any) {
      logger.error('Error creating OneDrive credentials', { error });
      next(error);
    }
  };

export const getSharePointCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      const encryptedSharePointCredentials =
        await keyValueStoreService.get<string>(
          `${configPaths.connectors.sharepoint.config}/${orgId}`,
        );
      if (encryptedSharePointCredentials) {
        const sharePointCredentials = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedSharePointCredentials),
        );
        res.status(200).json(sharePointCredentials).end();
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting SharePoint credentials', { error });
      next(error);
    }
  };

export const setSharePointCredentials =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const credentials = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const orgId = getOrgIdFromRequest(req);
      if (!orgId) {
        throw new BadRequestError('Organisation not found');
      }
      // Todo: Do a health check for the credentials
      const encryptedSharePointCredentials = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(credentials));
      await keyValueStoreService.set<string>(
        `${configPaths.connectors.sharepoint.config}/${orgId}`,
        encryptedSharePointCredentials,
      );
      res
        .status(200)
        .json({ message: 'SharePoint credentials created successfully' });
    } catch (error: any) {
      logger.error('Error creating SharePoint credentials', { error });
      next(error);
    }
  };

export const setSsoAuthConfig =
  (
    keyValueStoreService: KeyValueStoreService,
    samlController: SamlController,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { entryPoint, emailKey, enableJit , samlPlatform} = req.body;
      let { certificate } = req.body;
      certificate = certificate
        .replace(/\\n/g, '') // Remove \n
        .replace(/\n/g, '') // Remove newline characters
        .replace(/\s+/g, '') // Remove all whitespace
        .replace(/\\/g, ''); // Remove any remaining backslashes

      // Step 2: Remove BEGIN and END certificate markers if present
      certificate = certificate
        .replace(/-----BEGINCERTIFICATE-----/g, '')
        .replace(/-----ENDCERTIFICATE-----/g, '');

      certificate = certificate
        .replace(/-----BEGIN CERTIFICATE-----/g, '')
        .replace(/-----END ERTIFICATE-----/g, '');
      // Step 3: Ensure the certificate content is clean
      certificate = certificate.trim();
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedSsoConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ certificate, entryPoint, emailKey, enableJit: enableJit ?? true , samlPlatform }));
      await keyValueStoreService.set<string>(
        configPaths.auth.sso,
        encryptedSsoConfig,
      );
      await samlController.updateSamlStrategiesWithCallback();
      res.status(200).json({ message: 'Sso config created successfully' });
    } catch (error: any) {
      logger.error('Error creating Sso config', { error });
      next(error);
    }
  };

export const getSsoAuthConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedSsoConfig = await keyValueStoreService.get<string>(
        configPaths.auth.sso,
      );
      if (encryptedSsoConfig) {
        const ssoConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedSsoConfig),
        );
        res.status(200).json(ssoConfig).end();
        return;
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting SsoConfig', { error });
      next(error);
    }
  };

export const createQdrantConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { port, apiKey, host, grpcPort } = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedQdrantConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify({ port, apiKey, host, grpcPort }));
      await keyValueStoreService.set<string>(
        configPaths.db.qdrant,
        encryptedQdrantConfig,
      );
      const warningMessage = res.getHeader('Warning');
      res.status(200).json({
        message: 'Qdrant config created successfully',
        warningMessage,
      });
    } catch (error: any) {
      logger.error('Error creating Sso config', { error });
      next(error);
    }
  };

export const getQdrantConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedQdrantConfig = await keyValueStoreService.get<string>(
        configPaths.db.qdrant,
      );
      if (encryptedQdrantConfig) {
        const qdrantConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedQdrantConfig),
        );
        res.status(200).json(qdrantConfig).end();
        return;
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting SsoConfig', { error });
      next(error);
    }
  };
export const getFrontendUrl =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const url =
        (await keyValueStoreService.get<string>(configPaths.endpoint)) || '{}';
      const parsedUrl = JSON.parse(url);
      if (parsedUrl?.frontend?.publicEndpoint) {
        res
          .status(200)
          .json({ url: parsedUrl?.frontend?.publicEndpoint })
          .end();
        return;
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting Frontend Public Url', { error });
      next(error);
    }
  };

export const setFrontendUrl =
  (
    keyValueStoreService: KeyValueStoreService,
    scopedJwtSecret: string,
    configService: ConfigService,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      if (!req.user) {
        throw new NotFoundError('User not found');
      }
      const { url } = req.body;
      const normalizedUrl = normalizeUrl(url);
      if (!normalizedUrl) {
        throw new BadRequestError('Invalid URL');
      }
      try {
        new URL(normalizedUrl);
      } catch (e) {
        throw new BadRequestError(
          'Invalid URL format. A protocol (e.g., http://) is required.',
        );
      }
      const urls =
        (await keyValueStoreService.get<string>(configPaths.endpoint)) || '{}';
      let parsedUrls = JSON.parse(urls);
      // Preserve existing `auth` object if it exists, otherwise create a new one
      parsedUrls.frontend = {
        ...parsedUrls.frontend,
        publicEndpoint: normalizedUrl,
      };
      // Save the updated object back to configPaths.endpoint
      await keyValueStoreService.set<string>(
        configPaths.endpoint,
        JSON.stringify(parsedUrls),
      );

      const scopedToken = await generateFetchConfigAuthToken(
        req.user,
        scopedJwtSecret,
      );
      const response = await configService.updateConfig(scopedToken);
      if (response.statusCode != 200) {
        throw new BadRequestError('Error updating configs');
      }
      res.status(200).json({
        message: 'Frontend Url saved successfully',
      });
    } catch (error: any) {
      logger.error('Error setting frontend url', { error });
      next(error);
    }
  };

export const getConnectorPublicUrl =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const url =
        (await keyValueStoreService.get<string>(configPaths.endpoint)) || '{}';
      const parsedUrl = JSON.parse(url);
      if (parsedUrl?.connectors?.publicEndpoint) {
        res
          .status(200)
          .json({ url: parsedUrl?.connectors?.publicEndpoint })
          .end();
        return;
      } else {
        res.status(200).json({}).end();
      }
    } catch (error: any) {
      logger.error('Error getting Connector Public Url', { error });
      next(error);
    }
  };

export const setConnectorPublicUrl =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: SyncEventProducer,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      if (!req.user) {
        throw new NotFoundError('User not found');
      }
      const { url } = req.body;
      const normalizedUrl = normalizeUrl(url);
      if (!normalizedUrl) {
        throw new BadRequestError('Invalid URL');
      }
      try {
        new URL(normalizedUrl);
      } catch (e) {
        throw new BadRequestError(
          'Invalid URL format. A protocol (e.g., http://) is required.',
        );
      }
      const urls =
        (await keyValueStoreService.get<string>(configPaths.endpoint)) || '{}';

      let parsedUrls = JSON.parse(urls);

      // Preserve existing `auth` object if it exists, otherwise create a new one
      parsedUrls.connectors = {
        ...parsedUrls.connectors,
        publicEndpoint: normalizedUrl,
      };

      // Save the updated object back to configPaths.endpoint
      await keyValueStoreService.set<string>(
        configPaths.endpoint,
        JSON.stringify(parsedUrls),
      );

      await eventService.start();
      let event: Event = {
        eventType: EventType.ConnectorPublicUrlChangedEvent,
        timestamp: Date.now(),
        payload: {
          url,
          orgId: req.user.orgId,
        } as ConnectorPublicUrlChangedEvent,
      };
      await eventService.publishEvent(event);

      res.status(200).json({
        message: 'Connector Url saved successfully',
      });
    } catch (error: any) {
      logger.error('Error setting Connector url', { error });
      next(error);
    }
  };

export const toggleMetricsCollection =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { enableMetricCollection } = req.body;
      const metricsCollection = JSON.parse(
        (await keyValueStoreService.get<string>(
          configPaths.metricsCollection,
        )) || '{}',
      );

      if (enableMetricCollection !== metricsCollection.enableMetricCollection) {
        metricsCollection.enableMetricCollection = enableMetricCollection;
        await keyValueStoreService.set<string>(
          configPaths.metricsCollection,
          JSON.stringify(metricsCollection),
        );
      }
      res
        .status(200)
        .json({ message: 'Metrics collection toggled successfully' });
    } catch (error: any) {
      logger.error('Error toggling metrics collection', { error });
      next(error);
    }
  };

export const getMetricsCollection =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const metricsCollection = JSON.parse(
        (await keyValueStoreService.get<string>(
          configPaths.metricsCollection,
        )) || '{}',
      );
      res.status(200).json(metricsCollection).end();
    } catch (error: any) {
      logger.error('Error getting metrics collection', { error });
      next(error);
    }
  };

export const setMetricsCollectionPushInterval =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { pushIntervalMs } = req.body;

      const metricsCollection = JSON.parse(
        (await keyValueStoreService.get<string>(
          configPaths.metricsCollection,
        )) || '{}',
      );

      if (pushIntervalMs !== metricsCollection.pushIntervalMs) {
        metricsCollection.pushIntervalMs = pushIntervalMs;
        await keyValueStoreService.set<string>(
          configPaths.metricsCollection,
          JSON.stringify(metricsCollection),
        );
      }
      res
        .status(200)
        .json({ message: 'Metrics collection push interval set successfully' });
    } catch (error: any) {
      logger.error('Error setting metrics collection push interval', { error });
      next(error);
    }
  };

export const setMetricsCollectionRemoteServer =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { serverUrl } = req.body;
      const metricsCollection = JSON.parse(
        (await keyValueStoreService.get<string>(
          configPaths.metricsCollection,
        )) || '{}',
      );
      if (serverUrl !== metricsCollection.serverUrl) {
        metricsCollection.serverUrl = serverUrl;
        await keyValueStoreService.set<string>(
          configPaths.metricsCollection,
          JSON.stringify(metricsCollection),
        );
      }
      res
        .status(200)
        .json({ message: 'Metrics collection remote server set successfully' });
    } catch (error: any) {
      logger.error('Error setting metrics collection remote server', { error });
      next(error);
    }
  };

async function sendEvent(eventService: EntitiesEventProducer | AiConfigEventProducer, event: Event) {
  try {
    await eventService.start();
    await eventService.publishEvent(event);
    await eventService.stop();
  } catch (error) {
    logger.error('Error sending event', { error });
  }
}

export const createAIModelsConfig =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: AiConfigEventProducer,
    appConfig: AppConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const aiConfig = req.body;
      if (!aiConfig) {
        throw new BadRequestError('Invalid configuration passed');
      }

      // Handle LLM health check
      if (aiConfig.llm.length > 0) {
        const aiCommandOptions: AICommandOptions = {
          uri: `${appConfig.aiBackend}/api/v1/llm-health-check`,
          method: HttpMethod.POST,
          headers: req.headers as Record<string, string>,
          body: aiConfig.llm,
        };

        logger.debug('Health Check for AI llm Config API calling');

        // Don't use nested try/catch with next() inside
        const aiServiceCommand = new AIServiceCommand(aiCommandOptions);
        const aiResponseData =
          (await aiServiceCommand.execute()) as AIServiceResponse;

        if (!aiResponseData?.data || aiResponseData.statusCode !== 200) {
          throw new InternalServerError(
            'Failed to do health check of llm configuration, check credentials again',
            aiResponseData?.data,
          );
        }
      }

      // Handle embedding health check
      if (aiConfig.embedding.length > 0) {
        const aiCommandOptions: AICommandOptions = {
          uri: `${appConfig.aiBackend}/api/v1/embedding-health-check`,
          method: HttpMethod.POST,
          headers: req.headers as Record<string, string>,
          body: aiConfig.embedding,
        };

        logger.debug('Health Check for AI embedding Config API calling');

        // Don't use nested try/catch with next() inside
        const aiServiceCommand = new AIServiceCommand(aiCommandOptions);
        const aiResponseData =
          (await aiServiceCommand.execute()) as AIServiceResponse;

        if (!aiResponseData?.data || aiResponseData.statusCode !== 200) {
          throw new InternalServerError(
            'Failed to do health check of embedding configuration, check credentials again',
            aiResponseData?.data,
          );
        }
      }

      if (aiConfig.llm.length > 0) {
        aiConfig.llm.forEach((llm: any, index: number) => {
          const modelKey = uuidv4();
          llm.modelKey = modelKey;
          llm.isMultimodal = false;
          llm.isReasoning = false;
          llm.isDefault = index === 0;
        });
      }

      if (aiConfig.embedding.length > 0) {
        aiConfig.embedding.forEach((embedding: any, index: number) => {
          const modelKey = uuidv4();
          embedding.modelKey = modelKey;
          embedding.isMultimodal = false;
          embedding.isDefault = index === 0;
        });
      }

      // Encrypt and store configuration
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(aiConfig));

      await keyValueStoreService.set<string>(
        configPaths.aiModels,
        encryptedAIConfig,
      );

      // Notify other services about the new AI config. The initial config
      // may include LLM and/or embedding models; fire a separate event per
      // model type so downstream caches (e.g. the Python retrieval service's
      // embedding instance) get invalidated appropriately.
      if (aiConfig.llm.length > 0) {
        const llmEvent: Event = {
          eventType: EventType.LLMConfiguredEvent,
          timestamp: Date.now(),
          payload: {
            credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
          } as LLMConfiguredEvent,
        };
        await sendEvent(eventService, llmEvent);
      }

      if (aiConfig.embedding.length > 0) {
        const embeddingEvent: Event = {
          eventType: EventType.EmbeddingModelConfiguredEvent,
          timestamp: Date.now(),
          payload: {
            credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
          } as EmbeddingModelConfiguredEvent,
        };
        await sendEvent(eventService, embeddingEvent);
      }

      res.status(200).json({ message: 'AI config created successfully' }).end();
    } catch (error: any) {
      logger.error('Error creating ai models config', { error });
      next(error);
    }
  };

export const getAIModelsConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );
      if (encryptedAIConfig) {
        const decryptedAIConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAIConfig),
        );
        res.status(200).json(decryptedAIConfig).end();
        return;
      } else {
        res.status(200).json({}).end();
        return;
      }
    } catch (error: any) {
      logger.error('Error getting ai models config', { error });
      next(error);
    }
  };

// AI Models Provider Management Functions (Direct Node.js Implementation)
export const getAIModelsProviders =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(200).json({
          status: 'success',
          models: {
            ocr: [],
            embedding: [],
            slm: [],
            llm: [],
            reasoning: [],
            multiModal: [],
            imageGeneration: [],
            tts: [],
            stt: [],
          },
          message: 'No AI models found',
        });
        return;
      }

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      // Ensure all top-level keys exist
      const defaultStructure = {
        ocr: [],
        embedding: [],
        slm: [],
        llm: [],
        reasoning: [],
        multiModal: [],
        imageGeneration: [],
        tts: [],
        stt: [],
      };

      for (const key of Object.keys(defaultStructure)) {
        if (!aiModels[key]) {
          aiModels[key] = [];
        }
      }

      res.status(200).json({
        status: 'success',
        models: aiModels,
        message: 'AI models retrieved successfully',
      });
    } catch (error: any) {
      logger.error('Error getting AI models providers', { error });
      next(error);
    }
  };

export const getModelsByType =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { modelType } = req.params;
      if (!modelType) {
        res.status(400).json({
          status: 'error',
          message: 'modelType is required',
        });
        return;
      }
      const validTypes = [
        'llm',
        'embedding',
        'ocr',
        'slm',
        'reasoning',
        'multiModal',
        'imageGeneration',
        'tts',
        'stt',
      ];
      if (!validTypes.includes(modelType)) {
        res.status(400).json({
          status: 'error',
          message: `Invalid model type. Must be one of: ${validTypes.join(', ')}`,
        });
        return;
      }
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(200).json({
          status: 'success',
          models: [],
          message: `No ${modelType} models found`,
        });
        return;
      }

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      if (!aiModels[modelType]) {
        res.status(200).json({
          status: 'success',
          models: [],
          message: `No ${modelType} models found`,
        });
        return;
      }
      const configs = aiModels[modelType];
      res.status(200).json({
        status: 'success',
        models: configs,
        message: `Found ${configs.length} ${modelType} models`,
      });
    } catch (error: any) {
      logger.error('Error getting models by type', { error });
      next(error);
    }
  };

export const getAvailableModelsByType =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { modelType } = req.params;
      if (!modelType) {
        res.status(400).json({
          status: 'error',
          message: 'modelType is required',
        });
        return;
      }
      // Validate model type
      const validTypes = [
        'llm',
        'embedding',
        'ocr',
        'slm',
        'reasoning',
        'multiModal',
        'imageGeneration',
        'tts',
        'stt',
      ];
      if (!validTypes.includes(modelType)) {
        res.status(400).json({
          status: 'error',
          message: `Invalid model type. Must be one of: ${validTypes.join(', ')}`,
        });
        return;
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(200).json({
          status: 'success',
          models: [],
          message: `No ${modelType} models found`,
        });
        return;
      }
      logger.debug('encryptedAIConfig', encryptedAIConfig);

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      if (!aiModels[modelType]) {
        res.status(200).json({
          status: 'success',
          models: [],
          message: `No ${modelType} models found`,
        });
        return;
      }

      const configs = aiModels[modelType];
      const flattenedModels = [];

      for (const config of configs) {
        // Extract individual model names from comma-separated string
        let modelNames = [];
        if (config.configuration?.model) {
          const modelString = config.configuration.model;
          modelNames = modelString
            .split(',')
            .map((name: string) => name.trim())
            .filter(Boolean);
        }

        // Create a flattened entry for each individual model
        let markDefault = false;
        if (config.isDefault) {
          markDefault = true;
        }

        // Only include modelFriendlyName if there's a single model (not comma-separated)
        const shouldIncludeFriendlyName = modelNames.length === 1 && config.modelFriendlyName;

        for (const modelName of modelNames) {
          const flattenedModel = {
            modelType,
            provider: config.provider,
            modelName,
            modelKey: config.modelKey,
            isMultimodal: config.isMultimodal || false,
            isReasoning: config.isReasoning || false,
            isDefault: markDefault,
            ...(shouldIncludeFriendlyName && { modelFriendlyName: config.modelFriendlyName }),
          };
          markDefault = false; // Only mark first model as default
          flattenedModels.push(flattenedModel);
        }
      }

      res.status(200).json({
        status: 'success',
        models: flattenedModels,
        message: `Found ${flattenedModels.length} ${modelType} models`,
      });
      return;
    } catch (error: any) {
      logger.error('Error getting available models by type', { error });
      next(error);
    }
  };

export const addAIModelProvider =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: AiConfigEventProducer,
    appConfig: AppConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const {
        modelType,
        provider,
        configuration,
        isMultimodal = false,
        isDefault = false,
        isReasoning = false,
        contextLength,
      } = req.body;

      // Validate required fields
      if (!modelType || !provider || !configuration) {
        res.status(400).json({
          status: 'error',
          message: 'modelType, provider, and configuration are required',
        });
        return;
      }

      // Validate model type
      const validTypes = [
        'llm',
        'embedding',
        'ocr',
        'slm',
        'reasoning',
        'multiModal',
        'imageGeneration',
        'tts',
        'stt',
      ];
      if (!validTypes.includes(modelType)) {
        res.status(400).json({
          status: 'error',
          message: `Invalid model type. Must be one of: ${validTypes.join(', ')}`,
        });
        return;
      }

      const healthCheckPayload = {
        provider,
        configuration,
        modelType,
        isMultimodal,
        isDefault,
        isReasoning,
        contextLength,
      };

      const aiCommandOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/health-check/${modelType}`,
        method: HttpMethod.POST,
        headers: req.headers as Record<string, string>,
        body: healthCheckPayload,
      };

      logger.debug('Health Check for AI embedding Config API calling');

      // Don't use nested try/catch with next() inside
      const aiServiceCommand = new AIServiceCommand(aiCommandOptions);
      const aiResponseData =
        (await aiServiceCommand.execute()) as AIServiceResponse;

      if (!aiResponseData?.data || aiResponseData.statusCode !== 200) {
        const errData: any = aiResponseData?.data ?? {};
        const reasonMessage =
          (errData && (errData.message ?? errData.error?.message)) ??
          `Failed to do health check of ${modelType} configuration, check credentials again`;

        res.status(aiResponseData?.statusCode ?? 500).json({
          error: {
            status: 'error',
            message: reasonMessage,
            details: errData,
          },
        });
        return;
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      let aiModels: any = {};
      if (encryptedAIConfig) {
        aiModels = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedAIConfig),
        );
      }

      // Ensure all top-level keys exist
      const defaultStructure = {
        ocr: [],
        embedding: [],
        slm: [],
        llm: [],
        reasoning: [],
        multiModal: [],
        imageGeneration: [],
        tts: [],
        stt: [],
      };
      for (const key of Object.keys(defaultStructure)) {
        if (!(key in aiModels)) {
          aiModels[key] = [];
        }
      }

      // Generate unique model key with collision check
      let modelKey: string;
      let existingKeys: string[];
      do {
        modelKey = uuidv4();
        existingKeys = aiModels[modelType].map(
          (config: any) => config.modelKey,
        );
      } while (existingKeys.includes(modelKey));

      // Extract modelFriendlyName from configuration if present
      const modelFriendlyName = configuration.modelFriendlyName;

      // Prepare the new configuration
      const newConfig = {
        provider,
        configuration,
        modelKey,
        isMultimodal,
        isDefault,
        isReasoning,
        contextLength,
        ...(modelFriendlyName && { modelFriendlyName }),
      };

      // If this is set as default, remove default flag from other models
      if (isDefault) {
        for (const config of aiModels[modelType]) {
          config.isDefault = false;
        }
      }

      // Add the new configuration
      aiModels[modelType].push(newConfig);

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(aiModels));

      await keyValueStoreService.set<string>(
        configPaths.aiModels,
        encryptedUpdatedConfig,
      );

      // Emit an event specific to the model type so downstream services
      // refresh the right cache. Embedding changes MUST NOT use the LLM
      // event because the Python retrieval service only invalidates its
      // embedding instance on `embeddingModelConfigured`.
      const event: Event =
        modelType === 'embedding'
          ? {
              eventType: EventType.EmbeddingModelConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as EmbeddingModelConfiguredEvent,
            }
          : {
              eventType: EventType.LLMConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as LLMConfiguredEvent,
            };
      await sendEvent(eventService, event);

      res.status(200).json({
        status: 'success',
        message: `${modelType.toUpperCase()} provider added successfully`,
        details: {
          modelKey,
          modelType,
          provider,
          model: configuration.model,
          isDefault,
          contextLength,
        },
      });
    } catch (error: any) {
      logger.error('Error adding AI model provider', { error });
      const handleError = handleBackendError(error, 'add AI model provider');
      next(handleError);
    }
  };

export const updateAIModelProvider =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: AiConfigEventProducer,
    appConfig: AppConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { modelType, modelKey } = req.params;
      const {
        provider,
        configuration,
        isMultimodal = false,
        isReasoning = false,
        isDefault = false,
        contextLength,
      } = req.body;

      logger.debug('updateAIModelProvider', {
        modelType,
        modelKey,
        provider,
        configuration,
        isMultimodal,
        isReasoning,
        isDefault,
        contextLength,
      });

      // Validate required fields
      if (!provider || !configuration) {
        res.status(400).json({
          status: 'error',
          message: 'provider and configuration are required',
        });
        return;
      }

      const healthCheckPayload = {
        provider,
        configuration,
        modelType,
        isMultimodal,
        isReasoning,
        isDefault,
        contextLength,
      };

      const aiCommandOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/health-check/${modelType}`,
        method: HttpMethod.POST,
        headers: req.headers as Record<string, string>,
        body: healthCheckPayload,
      };

      logger.debug('Health Check for AI embedding Config API calling');

      // Don't use nested try/catch with next() inside
      const aiServiceCommand = new AIServiceCommand(aiCommandOptions);
      const aiResponseData =
        (await aiServiceCommand.execute()) as AIServiceResponse;

      if (!aiResponseData?.data || aiResponseData.statusCode !== 200) {
        const errData: any = aiResponseData?.data ?? {};
        const reasonMessage =
          (errData && (errData.message ?? errData.error?.message)) ??
          `Failed to do health check of ${modelType} configuration, check credentials again`;

        res.status(aiResponseData?.statusCode ?? 500).json({
          error: {
            status: 'error',
            message: reasonMessage,
            details: errData,
          },
        });
        return;
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No AI models configuration found',
        });
        return;
      }

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      // Find the model with the specified key across all model types
      let targetModel = null;
      let targetModelType = null;

      for (const [modelTypeKey, modelConfigs] of Object.entries(aiModels)) {
        for (const config of modelConfigs as any[]) {
          if (config.modelKey === modelKey) {
            targetModel = config;
            targetModelType = modelTypeKey;
            break;
          }
        }
        if (targetModel) break;
      }

      if (!targetModel || !targetModelType) {
        res.status(404).json({
          status: 'error',
          message: `Model with key '${modelKey}' not found or model type not found`,
        });
        return;
      }

      // Verify the model type matches if provided
      if (modelType && targetModelType !== modelType) {
        res.status(400).json({
          status: 'error',
          message: `Model key '${modelKey}' belongs to type '${targetModelType}', not '${modelType}'`,
        });
        return;
      }

      // Extract modelFriendlyName from configuration if present
      const modelFriendlyName = configuration.modelFriendlyName;

      // Update the model configuration
      targetModel.configuration = configuration;
      targetModel.isMultimodal = isMultimodal;
      targetModel.isDefault = isDefault;
      targetModel.isReasoning = isReasoning;
      targetModel.contextLength = contextLength || null;
      if (modelFriendlyName !== undefined) {
        targetModel.modelFriendlyName = modelFriendlyName;
      }
      // If this is set as default, remove default flag from other models of the same type
      if (isDefault) {
        for (const config of aiModels[targetModelType]) {
          if (config.modelKey !== modelKey) {
            config.isDefault = false;
          }
        }
      }

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(aiModels));

      await keyValueStoreService.set<string>(
        configPaths.aiModels,
        encryptedUpdatedConfig,
      );

      const event: Event =
        targetModelType === 'embedding'
          ? {
              eventType: EventType.EmbeddingModelConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as EmbeddingModelConfiguredEvent,
            }
          : {
              eventType: EventType.LLMConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as LLMConfiguredEvent,
            };
      await sendEvent(eventService, event);
      res.status(200).json({
        status: 'success',
        message: `${targetModelType.toUpperCase()} provider updated successfully`,
        details: {
          modelKey,
          modelType: targetModelType,
          provider: targetModel.provider,
          model: targetModel.configuration?.model,
          contextLength: targetModel.contextLength,
          isMultimodal: targetModel.isMultimodal,
          isReasoning: targetModel.isReasoning,
        },
      });
    } catch (error: any) {
      logger.error('Error updating AI model provider', { error });
      const handleError = handleBackendError(error, 'update AI model provider');
      next(handleError);
    }
  };

export const deleteAIModelProvider =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: AiConfigEventProducer,
    appConfig: AppConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { modelType, modelKey } = req.params;

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No AI models configuration found',
        });
        return;
      }

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      // Find the model with the specified key across all model types
      let deletedModel = null;
      let targetModelType = null;
      let modelIndex = -1;

      for (const [modelTypeKey, modelConfigs] of Object.entries(aiModels)) {
        if (!Array.isArray(modelConfigs)) continue;
        for (let i = 0; i < modelConfigs.length; i++) {
          const config = modelConfigs[i];
          if (
            config &&
            typeof config === 'object' &&
            'modelKey' in config &&
            config.modelKey === modelKey
          ) {
            deletedModel = config;
            targetModelType = modelTypeKey;
            modelIndex = i;
            break;
          }
        }
        if (deletedModel) break;
      }

      if (!deletedModel || !targetModelType) {
        res.status(404).json({
          status: 'error',
          message: `Model with key '${modelKey}' not found`,
        });
        return;
      }

      // Verify the model type matches if provided
      if (modelType && targetModelType !== modelType) {
        res.status(400).json({
          status: 'error',
          message: `Model key '${modelKey}' belongs to type '${targetModelType}', not '${modelType}'`,
        });
        return;
      }

      // Check if any agents are using this model before allowing deletion.
      // Fail-closed: if the usage check itself errors, block deletion rather than risk
      // deleting a model that active agents depend on.
      const aiCommandOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/agent/model-usage/${encodeURIComponent(deletedModel.modelKey)}`,
        method: HttpMethod.GET,
        headers: {
          ...(req.headers as Record<string, string>),
          'Content-Type': 'application/json',
        },
      };
      const aiCommand = new AIServiceCommand<{ success?: boolean; agents?: any[] }>(aiCommandOptions);

      let aiResponse;
      try {
        aiResponse = await aiCommand.execute();
      } catch (usageError: any) {
        logger.error('Agent usage check failed; blocking AI model deletion (fail-closed)', { error: usageError.message });
        throw new InternalServerError(
          'Cannot delete this model: unable to verify if it is in use by agents. Please try again or contact support.',
        );
      }

      if (aiResponse?.statusCode !== 200 || !aiResponse?.data?.success) {
        logger.error('Agent usage check returned non-success response; blocking AI model deletion', {
          statusCode: aiResponse?.statusCode,
          data: aiResponse?.data,
        });
        throw new InternalServerError(
          'Cannot delete this model: unable to verify if it is in use by agents. Please try again or contact support.',
        );
      }

      const agentsUsing = Array.isArray(aiResponse.data.agents) ? aiResponse.data.agents : [];

      if (agentsUsing.length > 0) {
        // Bake agent names into the message string itself. The error middleware
        // strips `metadata` in production (dev-only), so anything kept only in
        // metadata.agents would never reach the user. Wording mirrors the
        // connector delete path (see _format_connector_in_use_detail in router.py)
        // so 409 copy is identical across resource types.
        const MAX_AGENT_NAMES_DISPLAY = 3;
        const agentNames: string[] = agentsUsing
          .map((a: { name?: string }) => a?.name)
          .filter((n: unknown): n is string => typeof n === 'string' && n.length > 0);
        // Prefer the user-defined friendly name (what the UI card shows, e.g. "gpt").
        // Fall back through the technical model id (e.g. "gpt-5.4-mini") and finally
        // the opaque modelKey so the message is never empty.
        const modelDisplayName: string =
          (deletedModel?.modelFriendlyName as string | undefined) ||
          (deletedModel?.configuration?.modelFriendlyName as string | undefined) ||
          (deletedModel?.configuration?.model as string | undefined) ||
          (deletedModel?.modelKey as string | undefined) ||
          'model';
        let message: string;
        if (agentNames.length === 1) {
          message = `Cannot delete model '${modelDisplayName}': currently in use by agent '${agentNames[0]}'. Remove it from the agent first.`;
        } else {
          const displayed = agentNames.slice(0, MAX_AGENT_NAMES_DISPLAY).map((n) => `'${n}'`).join(', ');
          const remainder = agentNames.length - MAX_AGENT_NAMES_DISPLAY;
          const namesDisplay = remainder > 0 ? `${displayed} and ${remainder} more` : displayed;
          message = `Cannot delete model '${modelDisplayName}': currently in use by ${agentNames.length} agents (${namesDisplay}). Remove it from all agents first.`;
        }
        throw new ConflictError(message, { agents: agentsUsing });
      }

      const wasDefault = deletedModel.isDefault || false;

      // Remove the model from the configuration
      aiModels[targetModelType].splice(modelIndex, 1);

      // If the deleted model was default, set the first remaining model as default
      if (wasDefault && aiModels[targetModelType].length > 0) {
        aiModels[targetModelType][0].isDefault = true;
      }

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(aiModels));

      await keyValueStoreService.set<string>(
        configPaths.aiModels,
        encryptedUpdatedConfig,
      );

      const event: Event =
        targetModelType === 'embedding'
          ? {
              eventType: EventType.EmbeddingModelConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as EmbeddingModelConfiguredEvent,
            }
          : {
              eventType: EventType.LLMConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as LLMConfiguredEvent,
            };
      await sendEvent(eventService, event);

      res.status(200).json({
        status: 'success',
        message: `${targetModelType.toUpperCase()} provider deleted successfully`,
        details: {
          modelKey,
          modelType: targetModelType,
          provider: deletedModel.provider,
          model: deletedModel.configuration?.model,
          wasDefault,
          contextLength: deletedModel.contextLength,
        },
      });
    } catch (error: any) {
      logger.error('Error deleting AI model provider', { error });
      next(error);
    }
  };

export const updateDefaultAIModel =
  (
    keyValueStoreService: KeyValueStoreService,
    eventService: AiConfigEventProducer,
    appConfig: AppConfig,
  ) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { modelType, modelKey } = req.params;

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No AI models configuration found',
        });
        return;
      }

      const aiModels = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      // Find the model with the specified key across all model types
      let targetModel = null;
      let targetModelType = null;

      for (const [modelTypeKey, modelConfigs] of Object.entries(aiModels)) {
        for (const config of modelConfigs as any[]) {
          if (config.modelKey === modelKey) {
            targetModel = config;
            targetModelType = modelTypeKey;
            break;
          }
        }
        if (targetModel) break;
      }

      if (!targetModel || !targetModelType) {
        res.status(404).json({
          status: 'error',
          message: `Model with key '${modelKey}' not found`,
        });
        return;
      }

      // Verify the model type matches if provided
      if (modelType && targetModelType !== modelType) {
        res.status(400).json({
          status: 'error',
          message: `Model key '${modelKey}' belongs to type '${targetModelType}', not '${modelType}'`,
        });
        return;
      }

      // If the model is already the default, no-op: nothing to check or update.
      if (targetModel.isDefault) {
        res.status(200).json({
          status: 'success',
          message: `Default ${targetModelType} model unchanged`,
          details: {
            modelKey,
            modelType: targetModelType,
            provider: targetModel.provider,
            model: targetModel.configuration?.model,
            contextLength: targetModel.contextLength,
          },
        });
        return;
      }

      // Run a health check on the target model BEFORE promoting it to default.
      // This is critical for `embedding`: switching the default to a model with a
      // different vector dimension or model identity while the collection already
      // contains points would corrupt retrieval. The Python
      // `/api/v1/health-check/embedding` endpoint enforces that policy.
      // For other model types (llm, ocr, slm, reasoning, multiModal) we also
      // verify the model is reachable/credentials work before flipping the flag.
      const healthCheckSupportedTypes = [
        'llm',
        'embedding',
        'ocr',
        'slm',
        'reasoning',
        'multiModal',
        'imageGeneration',
        'tts',
        'stt',
      ];
      if (healthCheckSupportedTypes.includes(targetModelType)) {
        const healthCheckPayload = {
          provider: targetModel.provider,
          configuration: targetModel.configuration,
          modelType: targetModelType,
          isMultimodal: targetModel.isMultimodal ?? false,
          isReasoning: targetModel.isReasoning ?? false,
          isDefault: true,
          contextLength: targetModel.contextLength ?? null,
          ...(targetModel.modelFriendlyName && {
            modelFriendlyName: targetModel.modelFriendlyName,
          }),
        };

        const aiCommandOptions: AICommandOptions = {
          uri: `${appConfig.aiBackend}/api/v1/health-check/${targetModelType}`,
          method: HttpMethod.POST,
          headers: req.headers as Record<string, string>,
          body: healthCheckPayload,
        };

        logger.debug(
          `Health Check for AI ${targetModelType} default-update API calling`,
        );

        const aiServiceCommand = new AIServiceCommand(aiCommandOptions);
        const aiResponseData =
          (await aiServiceCommand.execute()) as AIServiceResponse;

        if (!aiResponseData?.data || aiResponseData?.statusCode !== 200) {
          const errData: any = aiResponseData?.data ?? {};
          const reasonMessage =
            (errData && (errData.message ?? errData.error?.message)) ??
            `Failed health check while setting default ${targetModelType} model. ` +
              `Refusing to change default to prevent breaking the system.`;

          res.status(aiResponseData?.statusCode ?? 500).json({
            error: {
              status: 'error',
              message: reasonMessage,
              details: errData,
            },
          });
          return;
        }
      }

      // Remove default flag from all models in this type
      for (const config of aiModels[targetModelType]) {
        config.isDefault = false;
      }

      // Set the target model as default
      targetModel.isDefault = true;

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(aiModels));

      await keyValueStoreService.set<string>(
        configPaths.aiModels,
        encryptedUpdatedConfig,
      );

      const event: Event =
        targetModelType === 'embedding'
          ? {
              eventType: EventType.EmbeddingModelConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as EmbeddingModelConfiguredEvent,
            }
          : {
              eventType: EventType.LLMConfiguredEvent,
              timestamp: Date.now(),
              payload: {
                credentialsRoute: `${appConfig.cmBackend}/${aiModelRoute}`,
              } as LLMConfiguredEvent,
            };
      await sendEvent(eventService, event);

      res.status(200).json({
        status: 'success',
        message: `Default ${targetModelType} model updated successfully`,
        details: {
          modelKey,
          modelType: targetModelType,
          provider: targetModel.provider,
          model: targetModel.configuration?.model,
          contextLength: targetModel.contextLength,
        },
      });
    } catch (error: any) {
      logger.error('Error updating default AI model', { error });
      next(error);
    }
  };

// Generic, parameterized connector config getter
export const getConnectorConfig =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const { connector } = req.params as { connector: string };
      if (!connector || typeof connector !== 'string') {
        throw new BadRequestError('connector path parameter is required');
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const key = `/services/connectors/${connector}/config`;
      const encryptedConfig = await keyValueStoreService.get<string>(key);

      if (!encryptedConfig) {
        res.status(200).json({}).end();
        return;
      }

      const config = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedConfig),
      );

      res.status(200).json(config).end();
    } catch (error: any) {
      logger.error('Error getting connector config by name', { error });
      next(error);
    }
  };

// Custom System Prompt Management
export const getCustomSystemPrompt =
  (keyValueStoreService: KeyValueStoreService) =>
  async (
    _req: AuthenticatedUserRequest | AuthenticatedServiceRequest,
    res: Response,
    next: NextFunction,
  ) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedAIConfig = await keyValueStoreService.get<string>(
        configPaths.aiModels,
      );

      if (!encryptedAIConfig) {
        res.status(200).json({ customSystemPrompt: '', customSystemPromptWebSearch: '' }).end();
        return;
      }

      const aiModels: AIModelsConfig = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedAIConfig),
      );

      const customSystemPrompt = aiModels.customSystemPrompt || '';
      const customSystemPromptWebSearch = aiModels.customSystemPromptWebSearch || '';
      res.status(200).json({ customSystemPrompt, customSystemPromptWebSearch }).end();
    } catch (error: any) {
      logger.error('Error getting custom system prompt', { error });
      next(error);
    }
  };

export const setCustomSystemPrompt =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { customSystemPrompt, customSystemPromptWebSearch } = req.body;

      if (typeof customSystemPrompt !== 'string') {
        throw new BadRequestError('customSystemPrompt must be a string');
      }
      if (typeof customSystemPromptWebSearch !== 'string') {
        throw new BadRequestError('customSystemPromptWebSearch must be a string');
      }

      const configManagerConfig = loadConfigurationManagerConfig();

      // Use Compare-and-Set (CAS) pattern with retries to prevent race conditions
      const MAX_RETRIES = 5;
      let success = false;

      for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
        const encryptedAIConfig = await keyValueStoreService.get<string>(
          configPaths.aiModels,
        );

        let aiModels: AIModelsConfig = {};
        if (encryptedAIConfig) {
          aiModels = JSON.parse(
            EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).decrypt(encryptedAIConfig),
          );
        }

        // Update only the custom prompt fields, keeping everything else intact
        aiModels.customSystemPrompt = customSystemPrompt;
        aiModels.customSystemPromptWebSearch = customSystemPromptWebSearch;

        // Encrypt the updated configuration
        const encryptedUpdatedConfig = EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).encrypt(JSON.stringify(aiModels));

        // Attempt atomic compare-and-set operation
        const casSuccess = await keyValueStoreService.compareAndSet<string>(
          configPaths.aiModels,
          encryptedAIConfig,
          encryptedUpdatedConfig,
        );

        if (casSuccess) {
          success = true;
          break;
        } else if (attempt === MAX_RETRIES - 1) {
          throw new Error(
            'Failed to update custom system prompts due to persistent concurrent modification. Please try again.',
          );
        }
        // If CAS failed, retry with exponential backoff
        await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
      }

      if (!success) {
        throw new Error(
          'Failed to update custom system prompts after maximum retries.',
        );
      }

      res.status(200).json({
        message: 'Custom system prompts updated successfully',
        customSystemPrompt,
        customSystemPromptWebSearch,
      });
    } catch (error: any) {
      logger.error('Error setting custom system prompt', { error });
      next(error);
    }
  };

// ---------------------------------------------------------------------------
// AI Model Registry proxy (forwards to Python backend)
// ---------------------------------------------------------------------------

async function proxyAiBackendGet(
  appConfig: AppConfig,
  req: AuthenticatedUserRequest,
  res: Response,
  next: NextFunction,
  path: string,
  options: { query?: URLSearchParams; logMessage: string },
): Promise<void> {
  try {
    const qs = options.query?.toString();
    const url = `${appConfig.aiBackend}${path}${qs ? `?${qs}` : ''}`;

    const headers: Record<string, string> = {};
    if (req.headers.authorization) {
      headers['Authorization'] = req.headers.authorization;
    }

    const response = await axios.get(url, { timeout: 10000, headers });
    res.status(response.status).json(response.data);
  } catch (error: any) {
    logger.error(options.logMessage, { error });
    if (error?.response) {
      res.status(error.response.status).json(error.response.data);
    } else {
      next(new ServiceUnavailableError('AI model registry service is unavailable'));
    }
  }
}

export const getAIModelRegistry =
  (appConfig: AppConfig) =>
  async (
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> => {
    const { search, capability } = req.query;
    const params = new URLSearchParams();
    if (search) params.set('search', String(search));
    if (capability) params.set('capability', String(capability));

    await proxyAiBackendGet(appConfig, req, res, next, '/api/v1/ai-models/registry', {
      query: params,
      logMessage: 'Error proxying AI model registry request',
    });
  };

export const getAIModelRegistryCapabilities =
  (appConfig: AppConfig) =>
  async (
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> => {
    await proxyAiBackendGet(
      appConfig,
      req,
      res,
      next,
      '/api/v1/ai-models/registry/capabilities',
      { logMessage: 'Error proxying AI model capabilities request' },
    );
  };

export const getAIModelProviderSchema =
  (appConfig: AppConfig) =>
  async (
    req: AuthenticatedUserRequest,
    res: Response,
    next: NextFunction,
  ): Promise<void> => {
    const providerId = req.params.providerId ?? '';
    const { capability } = req.query;
    const params = new URLSearchParams();
    if (capability) params.set('capability', String(capability));

    await proxyAiBackendGet(
      appConfig,
      req,
      res,
      next,
      `/api/v1/ai-models/registry/${encodeURIComponent(providerId)}/schema`,
      { query: params, logMessage: 'Error proxying AI model provider schema request' },
    );
  };

// Web Search Provider Management Functions
export const getWebSearchProviders =
  (keyValueStoreService: KeyValueStoreService) =>
  async (_req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
        configPaths.webSearch,
      );

      if (!encryptedWebSearchConfig) {
        res.status(200).json({
          status: 'success',
          providers: [],
          settings: DEFAULT_WEB_SEARCH_SETTINGS,
          message: 'No web search providers found',
        });
        return;
      }

      const webSearchConfig = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedWebSearchConfig),
      ) as WebSearchConfig;

      const providers = Array.isArray(webSearchConfig.providers)
        ? webSearchConfig.providers
        : [];
      const settings = normalizeWebSearchSettings(webSearchConfig.settings);

      res.status(200).json({
        status: 'success',
        providers,
        settings,
        message: 'Web search providers retrieved successfully',
      });
    } catch (error: any) {
      logger.error('Error getting web search providers', { error });
      next(error);
    }
  };

export const updateWebSearchSettings =
  (keyValueStoreService: KeyValueStoreService) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { includeImages, maxImages } = req.body;
      const configManagerConfig = loadConfigurationManagerConfig();

      // Use Compare-and-Set (CAS) pattern with retries to prevent race conditions
      const MAX_RETRIES = 5;
      let success = false;
      let normalizedSettings: ReturnType<typeof normalizeWebSearchSettings> | null =
        null;

      for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
        const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
          configPaths.webSearch,
        );

        let webSearchConfig: WebSearchConfig = { providers: [] };
        if (encryptedWebSearchConfig) {
          webSearchConfig = JSON.parse(
            EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).decrypt(encryptedWebSearchConfig),
          ) as WebSearchConfig;
        }

        if (!Array.isArray(webSearchConfig.providers)) {
          webSearchConfig.providers = [];
        }

        const existingSettings = normalizeWebSearchSettings(
          webSearchConfig.settings,
        );
        normalizedSettings = normalizeWebSearchSettings({
          includeImages,
          maxImages:
            typeof maxImages === 'number'
              ? maxImages
              : existingSettings.maxImages,
        });

        webSearchConfig.settings = normalizedSettings;

        const encryptedUpdatedConfig = EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).encrypt(JSON.stringify(webSearchConfig));

        const casSuccess = await keyValueStoreService.compareAndSet<string>(
          configPaths.webSearch,
          encryptedWebSearchConfig ?? null,
          encryptedUpdatedConfig,
        );

        if (casSuccess) {
          success = true;
          break;
        } else if (attempt === MAX_RETRIES - 1) {
          throw new Error(
            'Failed to update web search settings due to persistent concurrent modification. Please try again.',
          );
        }
        // If CAS failed, retry with exponential backoff
        await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
      }

      if (!success || normalizedSettings === null) {
        throw new Error(
          'Failed to update web search settings after maximum retries.',
        );
      }

      res.status(200).json({
        status: 'success',
        message: 'Web search settings updated successfully',
        settings: normalizedSettings,
      });
    } catch (error: any) {
      logger.error('Error updating web search settings', { error });
      next(error);
    }
  };

export const addWebSearchProvider =
  (keyValueStoreService: KeyValueStoreService, appConfig: AppConfig) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { provider, configuration, isDefault = false } = req.body;

      // Validate required fields
      if (!provider || !configuration) {
        res.status(400).json({
          status: 'error',
          message: 'provider and configuration are required',
        });
        return;
      }

      // Health check: verify provider credentials before saving
      const webSearchHealthCheckOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/web-search-health-check`,
        method: HttpMethod.POST,
        headers: req.headers as Record<string, string>,
        body: { provider, configuration },
      };

      logger.debug('Health check for web search provider before adding');

      const webSearchHealthCheckCommand = new AIServiceCommand(webSearchHealthCheckOptions);
      const webSearchHealthCheckResponse = (await webSearchHealthCheckCommand.execute()) as AIServiceResponse;

      if (!webSearchHealthCheckResponse?.data || webSearchHealthCheckResponse.statusCode !== 200) {
        const errData: any = webSearchHealthCheckResponse?.data ?? {};
        res.status(webSearchHealthCheckResponse?.statusCode ?? 500).json({
          status: 'error',
          message: errData.error ?? 'Failed to validate web search provider configuration',
          details: errData,
        });
        return;
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
        configPaths.webSearch,
      );

      let webSearchConfig: WebSearchConfig = { providers: [] };
      if (encryptedWebSearchConfig) {
        webSearchConfig = JSON.parse(
          EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).decrypt(encryptedWebSearchConfig),
        );
      }

      // Ensure providers array exists
      if (!webSearchConfig.providers) {
        webSearchConfig.providers = [];
      }

      // Generate a unique providerKey
      const providerKey = uuidv4();

      // If this is set as default, remove default flag from other providers
      if (isDefault) {
        for (const config of webSearchConfig.providers) {
          config.isDefault = false;
        }
      }

      // If this is the first provider, make it default
      const shouldBeDefault = webSearchConfig.providers.length === 0 || isDefault;

      // Add the new configuration
      const newConfig = {
        provider,
        configuration,
        providerKey,
        isDefault: shouldBeDefault,
      };

      webSearchConfig.providers.push(newConfig);

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(webSearchConfig));

      await keyValueStoreService.set<string>(
        configPaths.webSearch,
        encryptedUpdatedConfig,
      );

      res.status(200).json({
        status: 'success',
        message: 'Web search provider added successfully',
        details: {
          providerKey,
          provider,
          isDefault: shouldBeDefault,
        },
      });
    } catch (error: any) {
      logger.error('Error adding web search provider', { error });
      next(error);
    }
  };

export const updateWebSearchProvider =
  (keyValueStoreService: KeyValueStoreService, appConfig: AppConfig) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { providerKey } = req.params;
      const { provider, configuration, isDefault = false } = req.body;

      // Validate required fields
      if (!provider || !configuration) {
        res.status(400).json({
          status: 'error',
          message: 'provider and configuration are required',
        });
        return;
      }

      // Health check: verify provider credentials before updating
      const webSearchHealthCheckOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/web-search-health-check`,
        method: HttpMethod.POST,
        headers: req.headers as Record<string, string>,
        body: { provider, configuration },
      };

      logger.debug('Health check for web search provider before updating');

      const webSearchHealthCheckCommand = new AIServiceCommand(webSearchHealthCheckOptions);
      const webSearchHealthCheckResponse = (await webSearchHealthCheckCommand.execute()) as AIServiceResponse;

      if (!webSearchHealthCheckResponse?.data || webSearchHealthCheckResponse.statusCode !== 200) {
        const errData: any = webSearchHealthCheckResponse?.data ?? {};
        res.status(webSearchHealthCheckResponse?.statusCode ?? 500).json({
          status: 'error',
          message: errData.error ?? 'Failed to validate web search provider configuration',
          details: errData,
        });
        return;
      }

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
        configPaths.webSearch,
      );

      if (!encryptedWebSearchConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No web search configuration found',
        });
        return;
      }

      const webSearchConfig = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedWebSearchConfig),
      );

      // Find the provider with the specified key
      const targetProvider = webSearchConfig.providers?.find(
        (p: WebSearchProviderConfiguration) => p.providerKey === providerKey,
      );

      if (!targetProvider) {
        res.status(404).json({
          status: 'error',
          message: `Provider with key '${providerKey}' not found`,
        });
        return;
      }

      // Update the provider configuration
      targetProvider.provider = provider;
      targetProvider.configuration = configuration;
      targetProvider.isDefault = isDefault;

      // If this is set as default, remove default flag from other providers
      if (isDefault) {
        for (const config of webSearchConfig.providers) {
          if (config.providerKey !== providerKey) {
            config.isDefault = false;
          }
        }
      }

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(webSearchConfig));

      await keyValueStoreService.set<string>(
        configPaths.webSearch,
        encryptedUpdatedConfig,
      );

      res.status(200).json({
        status: 'success',
        message: 'Web search provider updated successfully',
        details: {
          providerKey,
          provider: targetProvider.provider,
        },
      });
    } catch (error: any) {
      logger.error('Error updating web search provider', { error });
      next(error);
    }
  };

export const deleteWebSearchProvider =
  (keyValueStoreService: KeyValueStoreService, appConfig: AppConfig) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { providerKey } = req.params;

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
        configPaths.webSearch,
      );

      if (!encryptedWebSearchConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No web search configuration found',
        });
        return;
      }

      const webSearchConfig = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedWebSearchConfig),
      );

      // Find the provider index
      const providerIndex = webSearchConfig.providers?.findIndex(
        (p: any) => p.providerKey === providerKey,
      );

      if (providerIndex === -1 || providerIndex === undefined) {
        res.status(404).json({
          status: 'error',
          message: `Provider with key '${providerKey}' not found`,
        });
        return;
      }

      const deletedProvider = webSearchConfig.providers[providerIndex];

      // Check if any agents are using this provider before allowing deletion
      try {
        const aiCommandOptions: AICommandOptions = {
          uri: `${appConfig.aiBackend}/api/v1/agent/web-search-usage/${encodeURIComponent(deletedProvider.provider)}`,
          method: HttpMethod.GET,
          headers: {
            ...(req.headers as Record<string, string>),
            'Content-Type': 'application/json',
          },
        };
        const aiCommand = new AIServiceCommand<{ success?: boolean; agents?: any[] }>(aiCommandOptions);
        const aiResponse = await aiCommand.execute();

        const agents =
          aiResponse?.data?.success && Array.isArray(aiResponse.data.agents)
            ? aiResponse.data.agents
            : [];

        if (agents.length > 0) {
          res.status(409).json({
            status: 'error',
            message: `Cannot delete this provider because it is currently used by ${agents.length} ${agents.length === 1 ? 'agent' : 'agents'}. Please remove the web search configuration from these agents first.`,
            agents,
          });
          return;
        }
      } catch (usageError: any) {
        logger.warn('Failed to check agent usage before deleting web search provider; proceeding with deletion', { error: usageError.message });
      }

      const wasDefault = deletedProvider.isDefault || false;

      // Remove the provider from the configuration
      webSearchConfig.providers.splice(providerIndex, 1);

      // If the deleted provider was default, set the first remaining provider as default
      if (wasDefault && webSearchConfig.providers.length > 0) {
        webSearchConfig.providers[0].isDefault = true;
      }

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(webSearchConfig));

      await keyValueStoreService.set<string>(
        configPaths.webSearch,
        encryptedUpdatedConfig,
      );

      res.status(200).json({
        status: 'success',
        message: 'Web search provider deleted successfully',
        details: {
          providerKey,
          provider: deletedProvider.provider,
          wasDefault,
        },
      });
    } catch (error: any) {
      logger.error('Error deleting web search provider', { error });
      next(error);
    }
  };

export const updateDefaultWebSearchProvider =
  (keyValueStoreService: KeyValueStoreService, appConfig: AppConfig) =>
  async (req: AuthenticatedUserRequest, res: Response, next: NextFunction) => {
    try {
      const { providerKey } = req.params;

      const configManagerConfig = loadConfigurationManagerConfig();
      const encryptedWebSearchConfig = await keyValueStoreService.get<string>(
        configPaths.webSearch,
      );

      // DuckDuckGo is a built-in provider that requires no stored configuration.
      // Setting it as default means clearing the default flag from all stored providers.
      if (providerKey === 'duckduckgo') {
        if (encryptedWebSearchConfig) {
          const webSearchConfigForDDG = JSON.parse(
            EncryptionService.getInstance(
              configManagerConfig.algorithm,
              configManagerConfig.secretKey,
            ).decrypt(encryptedWebSearchConfig),
          );

          for (const config of webSearchConfigForDDG.providers || []) {
            config.isDefault = false;
          }

          const encryptedUpdatedConfig = EncryptionService.getInstance(
            configManagerConfig.algorithm,
            configManagerConfig.secretKey,
          ).encrypt(JSON.stringify(webSearchConfigForDDG));

          await keyValueStoreService.set<string>(
            configPaths.webSearch,
            encryptedUpdatedConfig,
          );
        }

        res.status(200).json({
          status: 'success',
          message: 'Default web search provider updated successfully',
          details: {
            providerKey,
            provider: 'duckduckgo',
          },
        });
        return;
      }

      if (!encryptedWebSearchConfig) {
        res.status(404).json({
          status: 'error',
          message: 'No web search configuration found',
        });
        return;
      }

      const webSearchConfig = JSON.parse(
        EncryptionService.getInstance(
          configManagerConfig.algorithm,
          configManagerConfig.secretKey,
        ).decrypt(encryptedWebSearchConfig),
      );

      // Find the provider with the specified key
      const targetProvider = webSearchConfig.providers?.find(
        (p: any) => p.providerKey === providerKey,
      );

      if (!targetProvider) {
        res.status(404).json({
          status: 'error',
          message: `Provider with key '${providerKey}' not found`,
        });
        return;
      }

      // Health check: verify provider is reachable before setting as default
      const webSearchHealthCheckOptions: AICommandOptions = {
        uri: `${appConfig.aiBackend}/api/v1/web-search-health-check`,
        method: HttpMethod.POST,
        headers: req.headers as Record<string, string>,
        body: { provider: targetProvider.provider, configuration: targetProvider.configuration },
      };

      logger.debug('Health check for web search provider before setting as default');

      const webSearchHealthCheckCommand = new AIServiceCommand(webSearchHealthCheckOptions);
      const webSearchHealthCheckResponse = (await webSearchHealthCheckCommand.execute()) as AIServiceResponse;

      if (!webSearchHealthCheckResponse?.data || webSearchHealthCheckResponse.statusCode !== 200) {
        const errData: any = webSearchHealthCheckResponse?.data ?? {};
        res.status(webSearchHealthCheckResponse?.statusCode ?? 500).json({
          status: 'error',
          message: errData.error ?? 'Failed to validate web search provider configuration',
          details: errData,
        });
        return;
      }

      // Remove default flag from all providers
      for (const config of webSearchConfig.providers) {
        config.isDefault = false;
      }

      // Set the target provider as default
      targetProvider.isDefault = true;

      // Encrypt and save the updated configuration
      const encryptedUpdatedConfig = EncryptionService.getInstance(
        configManagerConfig.algorithm,
        configManagerConfig.secretKey,
      ).encrypt(JSON.stringify(webSearchConfig));

      await keyValueStoreService.set<string>(
        configPaths.webSearch,
        encryptedUpdatedConfig,
      );

      res.status(200).json({
        status: 'success',
        message: 'Default web search provider updated successfully',
        details: {
          providerKey,
          provider: targetProvider.provider,
        },
      });
    } catch (error: any) {
      logger.error('Error updating default web search provider', { error });
      next(error);
    }
  };