import { Providers } from '@librechat/agents';
import { googleSettings, AuthKeys, removeNullishValues } from 'librechat-data-provider';
import type { GoogleClientOptions, VertexAIClientOptions } from '@librechat/agents';
import type { GoogleAIToolType } from '@librechat/agents/langchain/google-common';
import type * as t from '~/types';
import { isEnabled } from '~/utils';

type GoogleThinkingLevel = 'THINKING_LEVEL_UNSPECIFIED' | 'MINIMAL' | 'LOW' | 'MEDIUM' | 'HIGH';
type GoogleThinkingConfig = {
  includeThoughts: boolean;
  thinkingLevel?: GoogleThinkingLevel;
};

const googleThinkingLevels = new Set<GoogleThinkingLevel>([
  'THINKING_LEVEL_UNSPECIFIED',
  'MINIMAL',
  'LOW',
  'MEDIUM',
  'HIGH',
]);

const vertexMultiRegionEndpoints = new Map([
  ['eu', 'aiplatform.eu.rep.googleapis.com'],
  ['us', 'aiplatform.us.rep.googleapis.com'],
  ['global', 'aiplatform.googleapis.com'],
]);

const blockedModelOptionParams = [
  'apiKey',
  'baseUrl',
  'baseURL',
  'endpoint',
  'authOptions',
  'customHeaders',
  'headers',
] as const;

type BlockedModelOptionParam = (typeof blockedModelOptionParams)[number];
type GoogleModelOptions = Partial<t.GoogleParameters> &
  Partial<Record<BlockedModelOptionParam, unknown>>;

/** Known Google/Vertex AI parameters that map directly to the client config */
export const knownGoogleParams = new Set([
  'model',
  'modelName',
  'temperature',
  'maxOutputTokens',
  'maxReasoningTokens',
  'topP',
  'topK',
  'seed',
  'presencePenalty',
  'frequencyPenalty',
  'stopSequences',
  'stop',
  'logprobs',
  'topLogprobs',
  'safetySettings',
  'responseModalities',
  'convertSystemMessageToHumanContent',
  'speechConfig',
  'streamUsage',
  'apiKey',
  'baseUrl',
  'endpoint',
  'location',
  'authOptions',
]);

/**
 * Applies default parameters to the target object only if the field is undefined
 * @param target - The target object to apply defaults to
 * @param defaults - Record of default parameter values
 */
function applyDefaultParams(target: Record<string, unknown>, defaults: Record<string, unknown>) {
  for (const [key, value] of Object.entries(defaults)) {
    if (target[key] === undefined) {
      target[key] = value;
    }
  }
}

function getThresholdMapping(model: string) {
  const gemini1Pattern = /gemini-(1\.0|1\.5|pro$|1\.0-pro|1\.5-pro|1\.5-flash-001)/;
  const restrictedPattern = /(gemini-(1\.5-flash-8b|2\.0|exp)|learnlm)/;

  if (gemini1Pattern.test(model)) {
    return (value: string) => {
      if (value === 'OFF') {
        return 'BLOCK_NONE';
      }
      return value;
    };
  }

  if (restrictedPattern.test(model)) {
    return (value: string) => {
      if (value === 'OFF' || value === 'HARM_BLOCK_THRESHOLD_UNSPECIFIED') {
        return 'BLOCK_NONE';
      }
      return value;
    };
  }

  return (value: string) => value;
}

function normalizeGoogleThinkingLevel(value: unknown): GoogleThinkingLevel | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }
  const normalized = value.toUpperCase() as GoogleThinkingLevel;
  if (!googleThinkingLevels.has(normalized)) {
    return undefined;
  }
  return normalized;
}

function getVertexMultiRegionEndpoint(location: string): string | undefined {
  return vertexMultiRegionEndpoints.get(location);
}

function sanitizeModelOptions(modelOptions: Partial<t.GoogleParameters> | undefined) {
  const sanitizedOptions: GoogleModelOptions = { ...(modelOptions ?? {}) };
  blockedModelOptionParams.forEach((param) => {
    delete sanitizedOptions[param];
  });
  return sanitizedOptions;
}

function isAllowedVertexEndpoint(endpoint: string): boolean {
  if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(endpoint)) {
    return false;
  }

  if (endpoint === 'aiplatform.googleapis.com') {
    return true;
  }

  if (/^[a-z0-9-]+-aiplatform\.googleapis\.com$/.test(endpoint)) {
    return true;
  }

  if (/^aiplatform-[a-z0-9-]+\.p\.googleapis\.com$/.test(endpoint)) {
    return true;
  }

  if (/^[a-z0-9-]+-aiplatform-restricted\.p\.googleapis\.com$/.test(endpoint)) {
    return true;
  }

  return /^aiplatform\.[a-z0-9-]+\.rep\.googleapis\.com$/.test(endpoint);
}

function hasAllowedVertexEndpoint(config: Record<string, unknown>): boolean {
  const endpoint = config.endpoint;
  if (endpoint === undefined) {
    return false;
  }

  if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint.length === 0) {
    delete config.endpoint;
    return false;
  }

  const normalizedEndpoint = endpoint.toLowerCase();
  if (!isAllowedVertexEndpoint(normalizedEndpoint)) {
    delete config.endpoint;
    return false;
  }

  config.endpoint = normalizedEndpoint;
  return true;
}

function applyVertexMultiRegionEndpoint(config: VertexAIClientOptions & { endpoint?: string }) {
  const location = config.location;
  if (typeof location !== 'string') {
    return;
  }
  const multiRegionEndpoint = getVertexMultiRegionEndpoint(location);
  if (multiRegionEndpoint) {
    config.endpoint = multiRegionEndpoint;
  }
}

export function getSafetySettings(
  model?: string,
): Array<{ category: string; threshold: string }> | undefined {
  if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) {
    return undefined;
  }
  const mapThreshold = getThresholdMapping(model ?? '');

  return [
    {
      category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
      threshold: mapThreshold(
        process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
      ),
    },
    {
      category: 'HARM_CATEGORY_HATE_SPEECH',
      threshold: mapThreshold(
        process.env.GOOGLE_SAFETY_HATE_SPEECH || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
      ),
    },
    {
      category: 'HARM_CATEGORY_HARASSMENT',
      threshold: mapThreshold(
        process.env.GOOGLE_SAFETY_HARASSMENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
      ),
    },
    {
      category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
      threshold: mapThreshold(
        process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
      ),
    },
    {
      category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
      threshold: mapThreshold(process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY || 'BLOCK_NONE'),
    },
  ];
}

/**
 * Replicates core logic from GoogleClient's constructor and setOptions, plus client determination.
 * Returns an object with the provider label and the final options that would be passed to createLLM.
 *
 * @param credentials - Either a JSON string or an object containing Google keys
 * @param options - The same shape as the "GoogleClient" constructor options
 */

export function getGoogleConfig(
  credentials: string | t.GoogleCredentials | undefined,
  options: t.GoogleConfigOptions = {},
  acceptRawApiKey = false,
) {
  let creds: t.GoogleCredentials = {};
  if (acceptRawApiKey && typeof credentials === 'string') {
    creds[AuthKeys.GOOGLE_API_KEY] = credentials;
  } else if (typeof credentials === 'string') {
    try {
      creds = JSON.parse(credentials);
    } catch (err: unknown) {
      throw new Error(
        `Error parsing string credentials: ${err instanceof Error ? err.message : 'Unknown error'}`,
      );
    }
  } else if (credentials && typeof credentials === 'object') {
    creds = credentials;
  }

  const serviceKeyRaw = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
  const serviceKey =
    typeof serviceKeyRaw === 'string' ? JSON.parse(serviceKeyRaw) : (serviceKeyRaw ?? {});

  const apiKey = creds[AuthKeys.GOOGLE_API_KEY] ?? null;
  const project_id = !apiKey ? (serviceKey?.project_id ?? null) : null;

  const reverseProxyUrl = options.reverseProxyUrl;
  const authHeader = options.authHeader;

  const {
    web_search,
    thinkingLevel,
    thinking = googleSettings.thinking.default,
    thinkingBudget = googleSettings.thinkingBudget.default,
    ...modelOptions
  } = sanitizeModelOptions(options.modelOptions);

  let enableWebSearch = web_search;

  const llmConfig = removeNullishValues(
    {
      ...(modelOptions || {}),
      model: modelOptions?.model ?? '',
      maxRetries: 2,
      topP: modelOptions?.topP ?? undefined,
      topK: modelOptions?.topK ?? undefined,
      temperature: modelOptions?.temperature ?? undefined,
      maxOutputTokens: modelOptions?.maxOutputTokens ?? undefined,
    },
    true,
  ) as GoogleClientOptions | VertexAIClientOptions;
  const initialConfig = llmConfig as Record<string, unknown>;
  let hasCustomVertexEndpoint = hasAllowedVertexEndpoint(initialConfig);
  let shouldSyncVertexEndpoint = true;

  /** Used only for Safety Settings */
  llmConfig.safetySettings = getSafetySettings(llmConfig.model);

  let provider;

  if (project_id) {
    provider = Providers.VERTEXAI;
  } else {
    provider = Providers.GOOGLE;
  }

  // If we have a GCP project => Vertex AI
  if (provider === Providers.VERTEXAI) {
    (llmConfig as VertexAIClientOptions).authOptions = {
      credentials: { ...serviceKey },
      projectId: project_id,
    };
    const location = process.env.GOOGLE_LOC || 'us-central1';
    (llmConfig as VertexAIClientOptions).location = location;
  } else if (apiKey && provider === Providers.GOOGLE) {
    llmConfig.apiKey = apiKey;
  } else {
    throw new Error(
      `Invalid credentials provided. Please provide either a valid API key or service account credentials for Google Cloud.`,
    );
  }

  const modelName = (modelOptions?.model ?? '') as string;

  /**
   * Gemini 3+ uses a qualitative `thinkingLevel` ('minimal'|'low'|'medium'|'high')
   * instead of the numeric `thinkingBudget` used by Gemini 2.5 and earlier.
   * When `thinking` is enabled (default: true), we always send `thinkingConfig`
   * with `includeThoughts: true`. The `thinkingBudget` param is ignored for Gemini 3+.
   *
   * For Vertex AI, top-level `includeThoughts` is still required because
   * `@librechat/agents/langchain/google-common`'s `formatGenerationConfig` reads it separately
   * from `thinkingConfig` — they serve different purposes in the request pipeline.
   */
  const isGemini3Plus = /gemini-([3-9]|\d{2,})/i.test(modelName);

  if (isGemini3Plus && thinking) {
    const thinkingConfig: GoogleThinkingConfig = {
      includeThoughts: true,
    };
    const normalizedThinkingLevel = normalizeGoogleThinkingLevel(thinkingLevel);
    if (normalizedThinkingLevel) {
      thinkingConfig.thinkingLevel = normalizedThinkingLevel;
    }
    if (provider === Providers.GOOGLE) {
      (llmConfig as { thinkingConfig?: GoogleThinkingConfig }).thinkingConfig = thinkingConfig;
    } else if (provider === Providers.VERTEXAI) {
      (llmConfig as { thinkingConfig?: GoogleThinkingConfig }).thinkingConfig = thinkingConfig;
      (llmConfig as VertexAIClientOptions).includeThoughts = true;
    }
  } else if (!isGemini3Plus) {
    const shouldEnableThinking =
      thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1);

    if (shouldEnableThinking && provider === Providers.GOOGLE) {
      (llmConfig as GoogleClientOptions).thinkingConfig = {
        thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default,
        includeThoughts: Boolean(thinking),
      };
    } else if (shouldEnableThinking && provider === Providers.VERTEXAI) {
      (llmConfig as VertexAIClientOptions).thinkingBudget = thinking
        ? thinkingBudget
        : googleSettings.thinkingBudget.default;
      (llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking);
    }
  }

  /*
  let legacyOptions = {};
  // Filter out any "examples" that are empty
  legacyOptions.examples = (legacyOptions.examples ?? [])
    .filter(Boolean)
    .filter((obj) => obj?.input?.content !== '' && obj?.output?.content !== '');

  // If user has "examples" from legacyOptions, push them onto llmConfig
  if (legacyOptions.examples?.length) {
    llmConfig.examples = legacyOptions.examples.map((ex) => {
      const { input, output } = ex;
      if (!input?.content || !output?.content) {return undefined;}
      return {
        input: new HumanMessage(input.content),
        output: new AIMessage(output.content),
      };
    }).filter(Boolean);
  }
  */

  if (reverseProxyUrl) {
    (llmConfig as GoogleClientOptions).baseUrl = reverseProxyUrl;
  }

  if (authHeader) {
    (llmConfig as GoogleClientOptions).customHeaders = {
      Authorization: `Bearer ${apiKey}`,
    };
  }

  /** Handle defaultParams first - only process Google-native params if undefined */
  if (options.defaultParams && typeof options.defaultParams === 'object') {
    for (const [key, value] of Object.entries(options.defaultParams)) {
      /** Handle web_search separately - don't add to config */
      if (key === 'web_search') {
        if (enableWebSearch === undefined && typeof value === 'boolean') {
          enableWebSearch = value;
        }
        continue;
      }

      if (knownGoogleParams.has(key)) {
        /** Route known Google params to llmConfig only if undefined */
        applyDefaultParams(llmConfig as Record<string, unknown>, { [key]: value });
        if (key === 'endpoint') {
          hasCustomVertexEndpoint = hasAllowedVertexEndpoint(llmConfig as Record<string, unknown>);
        }
      }
      /** Leave other params for transform to handle - they might be OpenAI params */
    }
  }

  /** Handle addParams - can override defaultParams */
  if (options.addParams && typeof options.addParams === 'object') {
    for (const [key, value] of Object.entries(options.addParams)) {
      /** Handle web_search separately - don't add to config */
      if (key === 'web_search') {
        if (typeof value === 'boolean') {
          enableWebSearch = value;
        }
        continue;
      }

      if (knownGoogleParams.has(key)) {
        /** Route known Google params to llmConfig */
        (llmConfig as Record<string, unknown>)[key] = value;
        if (key === 'endpoint') {
          hasCustomVertexEndpoint = hasAllowedVertexEndpoint(llmConfig as Record<string, unknown>);
        }
      }
      /** Leave other params for transform to handle - they might be OpenAI params */
    }
  }

  /** Handle dropParams - only drop from Google config */
  if (options.dropParams && Array.isArray(options.dropParams)) {
    options.dropParams.forEach((param) => {
      if (param === 'web_search') {
        enableWebSearch = false;
        return;
      }

      if (param === 'endpoint') {
        shouldSyncVertexEndpoint = false;
        hasCustomVertexEndpoint = false;
      }

      if (param in llmConfig) {
        delete (llmConfig as Record<string, unknown>)[param];
      }
    });
  }

  if (provider === Providers.VERTEXAI && shouldSyncVertexEndpoint && !hasCustomVertexEndpoint) {
    applyVertexMultiRegionEndpoint(llmConfig as VertexAIClientOptions & { endpoint?: string });
  }

  const tools: GoogleAIToolType[] = [];

  if (enableWebSearch) {
    tools.push({ googleSearch: {} });
  }

  // Return the final shape
  return {
    /** @type {GoogleAIToolType[]} */
    tools,
    /** @type {Providers.GOOGLE | Providers.VERTEXAI} */
    provider,
    /** @type {GoogleClientOptions | VertexAIClientOptions} */
    llmConfig,
  };
}
