/*
 * (c) Copyright Ascensio System SIA 2010-2024
 *
 * This program is a free software product. You can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License (AGPL)
 * version 3 as published by the Free Software Foundation. In accordance with
 * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
 * that Ascensio System SIA expressly excludes the warranty of non-infringement
 * of any third-party rights.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE. For
 * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
 *
 * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
 * street, Riga, Latvia, EU, LV-1050.
 *
 * The  interactive user interfaces in modified source and object code versions
 * of the Program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU AGPL version 3.
 *
 * Pursuant to Section 7(b) of the License you must retain the original Product
 * logo when distributing the program. Pursuant to Section 7(e) we decline to
 * grant you any rights under trademark law for use of our trademarks.
 *
 * All the Product's GUI elements, including illustrations and icon sets, as
 * well as technical writing content are licensed under the terms of the
 * Creative Commons Attribution-ShareAlike 4.0 International. See the License
 * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
 *
 */

'use strict';

const {pipeline} = require('stream/promises');
const {URL} = require('url');
const config = require('config');
const utils = require('./../../../Common/sources/utils');
const operationContext = require('./../../../Common/sources/operationContext');
const commonDefines = require('./../../../Common/sources/commondefines');
const tenantManager = require('./../../../Common/sources/tenantManager');
const docsCoServer = require('./../DocsCoServer');
const statsDClient = require('./../../../Common/sources/statsdclient');

const cfgAiApiAllowedOrigins = config.get('aiSettings.allowedCorsOrigins');
const cfgAiApiProxy = config.get('aiSettings.proxy');
const cfgAiApiTimeout = config.get('aiSettings.timeout');
const cfgTokenEnableOutbox = config.get('services.CoAuthoring.token.enable.request.outbox');
const cfgTokenOutboxHeader = config.get('services.CoAuthoring.token.outbox.header');
const cfgTokenOutboxPrefix = config.get('services.CoAuthoring.token.outbox.prefix');
const cfgAiSettings = config.get('aiSettings');

const clientStatsD = statsDClient.getClient();
/**
 * Helper function to set CORS headers if the request origin is allowed
 *
 * @param {object} req - Express request object
 * @param {object} res - Express response object
 * @param {operationContext.Context} ctx - Operation context for logging
 * @param {boolean} handleOptions - Whether to handle OPTIONS requests (default: true)
 * @returns {boolean} - True if this was an OPTIONS request that was handled
 */
function handleCorsHeaders(req, res, ctx, handleOptions = true) {
  const requestOrigin = req.headers.origin;

  const tenAiApiAllowedOrigins = ctx.getCfg('aiSettings.allowedCorsOrigins', cfgAiApiAllowedOrigins);

  // If no origin in request or allowed origins list is empty, do nothing
  if (!requestOrigin || tenAiApiAllowedOrigins.length === 0) {
    return false;
  }

  // If the origin is in our allowed list
  if (tenAiApiAllowedOrigins.includes(requestOrigin)) {
    res.setHeader('Access-Control-Allow-Origin', requestOrigin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin'); // Important when using dynamic origin

    // If debug logging is available
    if (ctx && ctx.logger) {
      ctx.logger.debug('CORS headers set for origin: %s (matched allowed list)', requestOrigin);
    }

    // Handle preflight OPTIONS requests if requested
    if (handleOptions && req.method === 'OPTIONS') {
      res.setHeader('Access-Control-Allow-Methods', 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT');
      // Allow all headers with wildcard
      res.setHeader('Access-Control-Allow-Headers', '*');

      // For preflight request, we should also set non-CORS headers to match the API
      res.setHeader('Allow', 'OPTIONS, HEAD, GET, POST, PUT, DELETE, PATCH');
      res.setHeader('Content-Length', '0');
      res.setHeader('Content-Type', 'text/html; charset=utf-8');

      // Return 204 which is standard for OPTIONS preflight
      res.sendStatus(204); // No Content response for OPTIONS
      return true; // Signal that we handled an OPTIONS request
    }
  }

  return false; // Not an OPTIONS request or origin not allowed
}

/**
 * Detects provider type and generates appropriate authentication headers based on URL patterns
 *
 * @param {operationContext.Context} ctx - Operation context for logging
 * @param {string} providerUrl - Provider base URL to detect type
 * @param {string} providerKey - API key for the provider
 * @param {string} uri - Full target request URI
 * @param {object} providerHeaders - Optional provider headers from customProviders
 * @returns {object} Headers object with authentication added
 */
function insertKeyToProvider(ctx, providerUrl, providerKey, uri, providerHeaders) {
  if (!providerKey) {
    return uri;
  }
  const urlLower = providerUrl.toLowerCase();

  if (urlLower.includes('anthropic.com')) {
    // Anthropic uses x-api-key header
    providerHeaders['x-api-key'] = providerKey;
  } else if (urlLower.includes('generativelanguage.googleapis.com')) {
    // Google Gemini uses API key as query parameter (already in URI)
    if (uri.includes('?')) uri += `&key=${providerKey}`;
    else uri += `?key=${providerKey}`;
  } else {
    // Default: Bearer Authorization (OpenAI, Deepseek, Groq, xAI, Mistral, Together.ai, etc.)
    providerHeaders['Authorization'] = `Bearer ${providerKey}`;
  }

  return uri;
}

/**
 * Makes an HTTP request to an AI API endpoint using the provided request and response objects
 *
 * @param {object} req - Express request object
 * @param {object} res - Express response object
 * @returns {Promise<void>} - Promise resolving when the request is complete
 */
async function proxyRequest(req, res) {
  // Create operation context for logging
  const ctx = new operationContext.Context();
  ctx.initFromRequest(req);
  const startDate = new Date();
  let success = false;

  try {
    ctx.logger.info('Start proxyRequest');
    await ctx.initTenantCache();
    const tenAiApiTimeout = ctx.getCfg('aiSettings.timeout', cfgAiApiTimeout);
    const tenAiApi = ctx.getCfg('aiSettings', cfgAiSettings);
    const tenAiApiProxy = ctx.getCfg('aiSettings.proxy', cfgAiApiProxy);

    // 1. Handle CORS preflight (OPTIONS) requests if necessary
    if (handleCorsHeaders(req, res, ctx) === true) {
      return; // OPTIONS request handled, stop further processing
    }

    let docId = '';
    let userId = '';
    let userName = '';
    let userCustomerId = '';
    const checkJwtRes = await docsCoServer.checkJwtHeader(ctx, req, 'Authorization', 'Bearer ', commonDefines.c_oAscSecretType.Session);
    if (!checkJwtRes || checkJwtRes.err) {
      ctx.logger.error('proxyRequest: checkJwtHeader error: %s', checkJwtRes?.err);
      res.status(403).json({
        error: {
          message: 'proxyRequest: checkJwtHeader error',
          code: '403'
        }
      });
      return;
    } else {
      docId = checkJwtRes?.decoded?.document?.key;
      userId = checkJwtRes?.decoded?.editorConfig?.user?.id;
      userName = checkJwtRes?.decoded?.editorConfig?.user?.name;
      userCustomerId = checkJwtRes?.decoded?.editorConfig?.user?.customerId;

      ctx.setDocId(docId);
      ctx.setUserId(userId);
    }

    if (!tenAiApi?.providers) {
      ctx.logger.error('proxyRequest: No providers configured');
      res.status(403).json({
        error: {
          message: 'proxyRequest: No providers configured',
          code: '403'
        }
      });
      return;
    }

    const body = JSON.parse(req.body);
    let uri = body.target;

    const providerHeaders = {};
    let providerMatched = false;
    // Determine which API key to use based on the target URL
    if (uri) {
      for (const providerName in tenAiApi.providers) {
        const tenProvider = tenAiApi.providers[providerName];
        if (uri.startsWith(tenProvider.url)) {
          providerMatched = true;

          // Generate appropriate headers based on provider type
          uri = insertKeyToProvider(ctx, tenProvider.url, tenProvider.key, uri, providerHeaders);
          break;
        }
      }
    }

    // If body.target was provided but no provider was matched, return 403
    if (!providerMatched) {
      ctx.logger.warn(`proxyRequest: target '${uri}' does not match any configured AI provider. Denying access.`);
      res.status(403).json({
        error: {
          message: 'proxyRequest: target does not match any configured AI provider',
          code: '403'
        }
      });
      return;
    }

    // Merge key in headers
    const headers = {...body.headers, ...providerHeaders};

    // Preserve Accept-Encoding from original request if not explicitly provided
    if (!headers['accept-encoding'] && req.headers['accept-encoding']) {
      headers['accept-encoding'] = req.headers['accept-encoding'];
    }

    // use proxy instead of direct request
    if (tenAiApiProxy) {
      const tenTokenEnableOutbox = ctx.getCfg('services.CoAuthoring.token.enable.request.outbox', cfgTokenEnableOutbox);
      if (tenTokenEnableOutbox) {
        const tenTokenOutboxHeader = ctx.getCfg('services.CoAuthoring.token.outbox.header', cfgTokenOutboxHeader);
        const tenTokenOutboxPrefix = ctx.getCfg('services.CoAuthoring.token.outbox.prefix', cfgTokenOutboxPrefix);
        const [licenseInfo] = await tenantManager.getTenantLicense(ctx);

        const dataObject = {
          key: docId,
          user: {
            id: userId,
            name: userName,
            customerId: userCustomerId || licenseInfo.customerId
          }
        };

        const secret = await tenantManager.getTenantSecret(ctx, commonDefines.c_oAscSecretType.Outbox);
        const auth = utils.fillJwtForRequest(ctx, dataObject, secret, false);
        headers[tenTokenOutboxHeader] = tenTokenOutboxPrefix + auth;
      }
      // Replace protocol, host and port in URI with proxy URL
      const proxyUrl = new URL(tenAiApiProxy);
      const targetUrl = new URL(uri);
      targetUrl.protocol = proxyUrl.protocol;
      targetUrl.host = proxyUrl.host;
      targetUrl.port = proxyUrl.port || targetUrl.port;
      uri = targetUrl.toString();
      ctx.logger.debug(`proxyRequest: Updated URI to use proxy host: ${tenAiApiProxy}`);
    }

    // Configure timeout options for the request
    const timeoutOptions = {
      connectionAndInactivity: tenAiApiTimeout,
      wholeCycle: tenAiApiTimeout
    };
    // Create request parameters object
    const requestParams = {
      method: body.method,
      uri,
      headers,
      body: body.data,
      timeout: timeoutOptions,
      limit: null,
      isInJwtToken: providerMatched //true because it passed provider's filter
    };

    // Log the sanitized request parameters
    ctx.logger.debug(`proxyRequest request: %j`, requestParams);

    // Use utils.httpRequest to make the request
    const result = await utils.httpRequest(
      ctx, // Operation context
      requestParams.method, // HTTP method
      requestParams.uri, // Target URL
      requestParams.headers, // Request headers
      requestParams.body, // Request body
      requestParams.timeout, // Timeout configuration
      requestParams.limit, // Size limit
      requestParams.isInJwtToken, // Filter private requests
      {
        decompress: false
      }
    );

    // Set the response headers to match the target response
    res.set(result.response.headers);

    // Use pipeline to pipe the response data to the client
    await pipeline(result.stream, res);
    success = true;
  } catch (error) {
    ctx.logger.error(`proxyRequest: AI API request error: %s`, error);
    if (error.response) {
      // Set the response headers to match the target response
      res.set(error.response.headers);

      // Use pipeline to pipe the response data to the client
      await pipeline(error.response.data, res);
    } else {
      res.status(500).json({
        error: {
          message: 'proxyRequest: AI API request error',
          code: '500'
        }
      });
    }
  } finally {
    // Record the time taken for the proxyRequest in StatsD (skip cors requests and errors)
    if (clientStatsD && success) {
      clientStatsD.timing('coauth.aiProxy', new Date() - startDate);
    }
    ctx.logger.info('End proxyRequest');
  }
}

/**
 * Retrieves all AI models from the configuration and dynamically from providers
 *
 * @param {operationContext.Context} ctx - Operation context
 * @returns {Promise<Object>} Object containing providers and their models along with action configurations
 */
async function getPluginSettings(ctx) {
  return {
    version: 3,
    actions: ctx.getCfg('aiSettings.actions', cfgAiSettings.actions),
    providers: ctx.getCfg('aiSettings.providers', cfgAiSettings.providers),
    customProviders: ctx.getCfg('aiSettings.customProviders', cfgAiSettings.customProviders),
    models: ctx.getCfg('aiSettings.models', cfgAiSettings.models)
  };
}

async function getPluginSettingsForInterface(ctx) {
  let pluginSettings = await getPluginSettings(ctx);

  // Create deep copy to avoid modifying cached config
  if (pluginSettings) {
    pluginSettings = JSON.parse(JSON.stringify(pluginSettings));
  }

  //check empty settings
  if (pluginSettings && pluginSettings.actions) {
    let isEmptySettings = true;
    for (const key in pluginSettings.actions) {
      if (pluginSettings.actions[key].model) {
        isEmptySettings = false;
      }
    }
    if (isEmptySettings) {
      pluginSettings = undefined;
    }
  }

  //remove keys from providers
  if (pluginSettings?.providers) {
    for (const key in pluginSettings.providers) {
      if (pluginSettings.providers[key]?.key) {
        pluginSettings.providers[key].key = '';
      }
    }
  }
  return pluginSettings;
}

module.exports = {
  proxyRequest,
  getPluginSettings,
  getPluginSettingsForInterface
};
