/*
 * (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 os = require('os');
const util = require('util');
const config = require('config');
const locale = require('windows-locale');
const ms = require('ms');
const decodeHeic = require('heic-decode');
const operationContext = require('./../../Common/sources/operationContext');

function initializeSharp() {
  let originalValues = {};
  try {
    const tmp = os.tmpdir();
    // Save original values
    originalValues = {
      XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
      HOME: process.env.HOME
    };
    // Set temporary values for Sharp initialization
    process.env.XDG_CACHE_HOME = tmp;
    process.env.HOME = tmp;

    sharp = require('sharp');
  } catch (error) {
    operationContext.global.logger.warn('Sharp module failed to load. Image processing functionality will be limited.');
    operationContext.global.logger.warn('Sharp load error:', error.message);
  } finally {
    // Restore original values
    Object.keys(originalValues).forEach(key => {
      if (originalValues[key] !== undefined) {
        process.env[key] = originalValues[key];
      } else {
        delete process.env[key];
      }
    });
  }

  if (sharp) {
    // todo test.
    // Set concurrency to 2 for better performance
    sharp.concurrency(2);
    // Disable cache - not needed for one-time image conversion (writes to ./.cache dir)
    sharp.cache(false);
  }
}

// Load Sharp with graceful fallback for pkg-builds and missing dependencies
let sharp = null;
initializeSharp();

const {notificationTypes, ...notificationService} = require('../../Common/sources/notificationService');

const cfgStartNotifyFrom = ms(config.get('license.warning_license_expiration'));
const cfgNotificationRuleLicenseExpirationWarning = config.get('notification.rules.licenseExpirationWarning.template');
const cfgNotificationRuleLicenseExpirationError = config.get('notification.rules.licenseExpirationError.template');

/**
 * Determine optimal format (PNG vs JPEG) for image conversion based on image characteristics.
 * @param {operationContext} ctx Operation context for logging
 * @param {Object} metadata Image metadata from sharp
 * @returns {('png'|'jpeg')} Optimal format for conversion
 */
function determineOptimalFormat(ctx, metadata) {
  // If image has alpha channel, only PNG can preserve transparency
  if (metadata.hasAlpha) {
    return 'png';
  }

  // Analyze color characteristics
  const width = metadata.width || 0;
  const height = metadata.height || 0;

  // Small images (likely icons/logos) - prefer PNG
  // Only apply when dimensions are known (greater than zero)
  if (width > 0 && height > 0 && width <= 256 && height <= 256) {
    return 'png';
  }

  // Large photographic images - prefer JPEG
  if (width > 800 || height > 600) {
    return 'jpeg';
  }

  // Default to JPEG for general compatibility and smaller file sizes
  return 'jpeg';
}

/**
 * Convert Sharp pipeline to buffer in optimal format (PNG or JPEG).
 * @param {Object} pipeline Sharp pipeline instance
 * @param {string} format Target format ('png' or 'jpeg')
 * @returns {Promise<Buffer>} Converted image buffer
 */
async function convertToFormat(pipeline, format) {
  if (format === 'png') {
    return await pipeline.png({compressionLevel: 7}).toBuffer();
  }
  return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
}

/**
 * Decode HEIC/HEIF buffer using heic-decode library and create Sharp instance.
 * @param {Buffer} buffer HEIC/HEIF image buffer
 * @returns {Promise<Object>} Sharp instance with decoded raw image data
 */
async function decodeHeicToSharp(buffer) {
  const decodedImage = await decodeHeic({buffer});
  return sharp(decodedImage.data, {
    failOn: 'none',
    raw: {
      width: decodedImage.width,
      height: decodedImage.height,
      channels: 4
    }
  });
}

/**
 * Process and optimize image buffer with EXIF rotation fix and modern format conversion.
 * 1. Fixes EXIF rotation and strips metadata for all images
 * 2. Converts modern/unsupported formats to optimal formats:
 *    - WebP/HEIC/HEIF/AVIF/TIFF: Convert to optimal format (PNG or JPEG) based on image characteristics
 * @param {operationContext} ctx Operation context for logging
 * @param {Buffer} buffer Source image bytes
 * @returns {Promise<Buffer>} Processed and optimally converted buffer or original buffer
 */
async function processImageOptimal(ctx, buffer) {
  if (!buffer) return buffer;

  // Check if Sharp is available
  if (!sharp) {
    ctx.logger.warn('processImageOptimal: Sharp module not available, returning original buffer. Image processing disabled.');
    return buffer;
  }

  try {
    const meta = await sharp(buffer, {failOn: 'none'}).metadata();
    const fmt = (meta.format || '').toLowerCase();
    const needsRotation = meta.orientation && meta.orientation > 1;

    // Handle modern formats requiring conversion
    if (fmt === 'heic' || fmt === 'heif' || fmt === 'webp' || fmt === 'avif' || fmt === 'tiff' || fmt === 'tif') {
      const optimalFormat = determineOptimalFormat(ctx, meta);
      ctx.logger.debug('processImageOptimal: converting %s to %s%s', fmt, optimalFormat, needsRotation ? ' with rotation' : '');

      try {
        const pipeline = sharp(buffer, {failOn: 'none'}).rotate();
        return await convertToFormat(pipeline, optimalFormat);
      } catch (sharpError) {
        // Fallback to heic-decode for HEIC/HEIF when Sharp fails
        if (fmt === 'heic' || fmt === 'heif') {
          ctx.logger.debug('processImageOptimal: Sharp failed for %s, using heic-decode fallback', fmt);
          const heicPipeline = await decodeHeicToSharp(buffer);
          return await convertToFormat(heicPipeline, optimalFormat);
        }
        throw sharpError;
      }
    }

    // For standard formats, only apply EXIF rotation if needed
    if (needsRotation) {
      ctx.logger.debug('processImageOptimal: applying EXIF rotation to %s', fmt);
      const pipeline = sharp(buffer, {failOn: 'none'}).rotate();

      if (fmt === 'jpeg' || fmt === 'jpg') {
        return await pipeline.jpeg({quality: 90, chromaSubsampling: '4:4:4'}).toBuffer();
      }
      if (fmt === 'png') {
        return await pipeline.png({compressionLevel: 7}).toBuffer();
      }
      return await pipeline.toBuffer();
    }
  } catch (e) {
    ctx.logger.debug('processImageOptimal error: %s', e.stack);
  }

  return buffer;
}

/**
 *
 * @param {string} lang
 * @returns {number | undefined}
 */
function localeToLCID(lang) {
  const elem = locale[lang && lang.toLowerCase()];
  return elem && elem.id;
}

function humanFriendlyExpirationTime(endTime) {
  const month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

  return `${month[endTime.getUTCMonth()]} ${endTime.getUTCDate()}, ${endTime.getUTCFullYear()}`;
}

/**
 * Notify server user about license expiration via configured notification transports.
 * @param {Context} ctx Context.
 * @param {Date} endDate Date of expiration.
 * @returns {undefined}
 */
async function notifyLicenseExpiration(ctx, endDate) {
  if (!endDate) {
    ctx.logger.warn('notifyLicenseExpiration(): expiration date is not defined');
    return;
  }

  const currentDate = new Date();
  if (currentDate.getTime() >= endDate.getTime() - cfgStartNotifyFrom) {
    //todo remove stub for "new Date(1)" and "setMonth + 1" in license.js; bug 70676
    if (endDate.getUTCFullYear() < 2000) {
      endDate = currentDate;
    }
    const formattedExpirationTime = humanFriendlyExpirationTime(endDate);
    const applicationName = (process.env.APPLICATION_NAME || '').toUpperCase();
    if (endDate <= currentDate) {
      const tenNotificationRuleLicenseExpirationError = ctx.getCfg(
        'notification.rules.licenseExpirationError.template',
        cfgNotificationRuleLicenseExpirationError
      );
      const title = util.format(tenNotificationRuleLicenseExpirationError.title, applicationName);
      const message = util.format(tenNotificationRuleLicenseExpirationError.body, formattedExpirationTime);
      ctx.logger.error(message);
      await notificationService.notify(ctx, notificationTypes.LICENSE_EXPIRATION_ERROR, title, message);
    } else {
      const tenNotificationRuleLicenseExpirationWarning = ctx.getCfg(
        'notification.rules.licenseExpirationWarning.template',
        cfgNotificationRuleLicenseExpirationWarning
      );
      const title = util.format(tenNotificationRuleLicenseExpirationWarning.title, applicationName);
      const message = util.format(tenNotificationRuleLicenseExpirationWarning.body, formattedExpirationTime);
      ctx.logger.warn(message);
      await notificationService.notify(ctx, notificationTypes.LICENSE_EXPIRATION_WARNING, title, message);
    }
  }
}

module.exports.processImageOptimal = processImageOptimal;
module.exports.determineOptimalFormat = determineOptimalFormat;
module.exports.localeToLCID = localeToLCID;
module.exports.notifyLicenseExpiration = notifyLicenseExpiration;
