/**
 * Copyright 2024-2025 Wingify Software Pvt. Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { StorageDecorator } from '../../../decorators/StorageDecorator';
import { SettingsModel } from '../../../models/settings/SettingsModel';
import { LogManager } from '../../logger';
import { StorageService } from '../../../services/StorageService';
import { dynamic } from '../../../types/Common';
import { isObject } from '../../../utils/DataTypeUtil';
import { SegmentOperatorValueEnum } from '../enums/SegmentOperatorValueEnum';
import { Segmentation } from '../Segmentation';
import { getKeyValue } from '../utils/SegmentUtil';
import { SegmentOperandEvaluator } from './SegmentOperandEvaluator';
import { ContextModel } from '../../../models/user/ContextModel';
import { FeatureModel } from '../../../models/campaign/FeatureModel';

export class SegmentEvaluator implements Segmentation {
  context: ContextModel;
  settings: SettingsModel;
  feature: FeatureModel;

  /**
   * Validates if the segmentation defined in the DSL is applicable based on the provided properties.
   * @param dsl The domain-specific language defining the segmentation rules.
   * @param properties The properties against which the DSL rules are evaluated.
   * @returns A Promise resolving to a boolean indicating if the segmentation is valid.
   */
  async isSegmentationValid(dsl: Record<string, dynamic>, properties: Record<string, dynamic>): Promise<boolean> {
    const { key, value } = getKeyValue(dsl);
    const operator = key;
    const subDsl = value;

    // Evaluate based on the type of segmentation operator
    switch (operator) {
      case SegmentOperatorValueEnum.NOT:
        return !(await this.isSegmentationValid(subDsl, properties));
      case SegmentOperatorValueEnum.AND:
        return await this.every(subDsl, properties);
      case SegmentOperatorValueEnum.OR:
        return await this.some(subDsl, properties);
      case SegmentOperatorValueEnum.CUSTOM_VARIABLE:
        return await new SegmentOperandEvaluator().evaluateCustomVariableDSL(subDsl, properties);
      case SegmentOperatorValueEnum.USER:
        return new SegmentOperandEvaluator().evaluateUserDSL(subDsl, properties);
      case SegmentOperatorValueEnum.UA:
        return new SegmentOperandEvaluator().evaluateUserAgentDSL(subDsl, this.context);
      default:
        return false;
    }
  }

  /**
   * Evaluates if any of the DSL nodes are valid using the OR logic.
   * @param dslNodes Array of DSL nodes to evaluate.
   * @param customVariables Custom variables provided for evaluation.
   * @returns A Promise resolving to a boolean indicating if any of the nodes are valid.
   */
  async some(dslNodes: Array<Record<string, dynamic>>, customVariables: Record<string, dynamic>): Promise<boolean> {
    const uaParserMap: Record<string, string[]> = {};
    let keyCount: number = 0; // Initialize count of keys encountered
    let isUaParser = false;

    for (const dsl of dslNodes) {
      for (const key in dsl) {
        // Check for user agent related keys
        if (
          key === SegmentOperatorValueEnum.OPERATING_SYSTEM ||
          key === SegmentOperatorValueEnum.BROWSER_AGENT ||
          key === SegmentOperatorValueEnum.DEVICE_TYPE ||
          key === SegmentOperatorValueEnum.DEVICE
        ) {
          isUaParser = true;
          const value = dsl[key];

          if (!uaParserMap[key]) {
            uaParserMap[key] = [];
          }

          // Ensure value is treated as an array of strings
          const valuesArray = Array.isArray(value) ? value : [value];
          valuesArray.forEach((val: dynamic) => {
            if (typeof val === 'string') {
              uaParserMap[key].push(val);
            }
          });

          keyCount++; // Increment count of keys encountered
        }

        // Check for feature toggle based on feature ID
        if (key === SegmentOperatorValueEnum.FEATURE_ID) {
          const featureIdObject = dsl[key] as Record<string, string>;
          const featureIdKey: string = Object.keys(featureIdObject)[0];
          const featureIdValue: string = featureIdObject[featureIdKey];

          if (featureIdValue === 'on' || featureIdValue === 'off') {
            const features = this.settings.getFeatures();
            const feature = features.find((feature) => feature.getId() === parseInt(featureIdKey));

            if (feature) {
              const featureKey = feature.getKey();
              const result = await this.checkInUserStorage(this.settings, featureKey, this.context);
              // if the result is false, then we need to return true as feature is not present in the user storage
              if (featureIdValue === 'off') {
                return !result;
              }
              return result;
            } else {
              LogManager.Instance.error('Feature not found with featureIdKey: ' + featureIdKey);
              return null; // Handle the case when feature is not found
            }
          }
        }
      }

      // Check if the count of keys encountered is equal to dslNodes.length
      if (isUaParser && keyCount === dslNodes.length) {
        try {
          const uaParserResult = await this.checkUserAgentParser(uaParserMap);
          return uaParserResult;
        } catch (err) {
          LogManager.Instance.error('Failed to validate User Agent. Erro: ' + err);
        }
      }

      // Recursively check each DSL node
      if (await this.isSegmentationValid(dsl, customVariables)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Evaluates all DSL nodes using the AND logic.
   * @param dslNodes Array of DSL nodes to evaluate.
   * @param customVariables Custom variables provided for evaluation.
   * @returns A Promise resolving to a boolean indicating if all nodes are valid.
   */
  async every(dslNodes: Array<Record<string, dynamic>>, customVariables: Record<string, dynamic>): Promise<boolean> {
    const locationMap: Record<string, dynamic> = {};
    for (const dsl of dslNodes) {
      // Check if the DSL node contains location-related keys
      if (
        SegmentOperatorValueEnum.COUNTRY in dsl ||
        SegmentOperatorValueEnum.REGION in dsl ||
        SegmentOperatorValueEnum.CITY in dsl
      ) {
        this.addLocationValuesToMap(dsl, locationMap);
        // Check if the number of location keys matches the number of DSL nodes
        if (Object.keys(locationMap).length === dslNodes.length) {
          const segmentResult = await this.checkLocationPreSegmentation(locationMap);
          return segmentResult;
        }
        continue;
      }
      const res = await this.isSegmentationValid(dsl, customVariables);
      if (!res) {
        return false;
      }
    }
    return true;
  }

  /**
   * Adds location values from a DSL node to a map.
   * @param dsl DSL node containing location data.
   * @param locationMap Map to store location data.
   */
  addLocationValuesToMap(dsl: Record<string, dynamic>, locationMap: Record<string, dynamic>): void {
    // Add country, region, and city information to the location map if present
    if (SegmentOperatorValueEnum.COUNTRY in dsl) {
      locationMap[SegmentOperatorValueEnum.COUNTRY] = dsl[SegmentOperatorValueEnum.COUNTRY];
    }
    if (SegmentOperatorValueEnum.REGION in dsl) {
      locationMap[SegmentOperatorValueEnum.REGION] = dsl[SegmentOperatorValueEnum.REGION];
    }
    if (SegmentOperatorValueEnum.CITY in dsl) {
      locationMap[SegmentOperatorValueEnum.CITY] = dsl[SegmentOperatorValueEnum.CITY];
    }
  }

  /**
   * Checks if the user's location matches the expected location criteria.
   * @param locationMap Map of expected location values.
   * @returns A Promise resolving to a boolean indicating if the location matches.
   */
  async checkLocationPreSegmentation(locationMap: Record<string, dynamic>): Promise<boolean> {
    // Ensure user's IP address is available
    if (this.context?.getIpAddress() === undefined && typeof process.env !== 'undefined') {
      LogManager.Instance.error('To evaluate location pre Segment, please pass ipAddress in context object');
      return false;
    }
    // Check if location data is available and matches the expected values
    if (
      !this.context?.getVwo()?.getLocation() ||
      this.context?.getVwo()?.getLocation() === undefined ||
      this.context?.getVwo()?.getLocation() === null
    ) {
      return false;
    }
    return this.valuesMatch(locationMap, this.context?.getVwo()?.getLocation());
  }

  /**
   * Checks if the user's device information matches the expected criteria.
   * @param uaParserMap Map of expected user agent values.
   * @returns A Promise resolving to a boolean indicating if the user agent matches.
   */
  async checkUserAgentParser(uaParserMap: Record<string, string[]>): Promise<boolean> {
    // Ensure user's user agent is available
    if (!this.context?.getUserAgent() || this.context?.getUserAgent() === undefined) {
      LogManager.Instance.error('To evaluate user agent related segments, please pass userAgent in context object');
      return false;
    }
    // Check if user agent data is available and matches the expected values
    if (!this.context?.getVwo()?.getUaInfo() || this.context?.getVwo()?.getUaInfo() === undefined) {
      return false;
    }

    return this.checkValuePresent(uaParserMap, this.context?.getVwo()?.getUaInfo());
  }

  /**
   * Checks if the feature is enabled for the user by querying the storage.
   * @param settings The settings model containing configuration.
   * @param featureKey The key of the feature to check.
   * @param user The user object to check against.
   * @returns A Promise resolving to a boolean indicating if the feature is enabled for the user.
   */
  async checkInUserStorage(settings: SettingsModel, featureKey: string, context: ContextModel): Promise<any> {
    const storageService = new StorageService();
    // Retrieve feature data from storage
    const storedData: Record<any, any> = await new StorageDecorator().getFeatureFromStorage(
      featureKey,
      context,
      storageService,
    );

    // Check if the stored data is an object and not empty
    if (isObject(storedData) && Object.keys(storedData).length > 0) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * Checks if the actual values match the expected values specified in the map.
   * @param expectedMap A map of expected values for different keys.
   * @param actualMap A map of actual values to compare against.
   * @returns A Promise resolving to a boolean indicating if all actual values match the expected values.
   */
  async checkValuePresent(expectedMap: Record<string, string[]>, actualMap: Record<string, string>): Promise<boolean> {
    for (const key in actualMap) {
      if (Object.prototype.hasOwnProperty.call(expectedMap, key)) {
        const expectedValues = expectedMap[key];
        // convert expected values to lowercase
        expectedValues.forEach((value, index) => {
          expectedValues[index] = value.toLowerCase();
        });
        const actualValue = actualMap[key];

        // Handle wildcard patterns for all keys
        for (const val of expectedValues) {
          // Check if the value is a wildcard pattern and matches the actual value using regex
          if (val.startsWith('wildcard(') && val.endsWith(')')) {
            // Extract pattern from wildcard string
            const wildcardPattern = val.slice(9, -1);
            // Convert wildcard pattern to regex and check if it matches the actual value
            const regex = new RegExp(wildcardPattern.replace(/\*/g, '.*'), 'i'); // Convert wildcard pattern to regex, 'i' for case-insensitive
            // Check if the actual value matches the regex pattern for the key
            if (regex.test(actualValue)) {
              // match found, return true as we only need to check if any of the expected values match the actual value
              return true;
            }
          }
        }

        // this will be checked for all cases where wildcard is not present
        if (expectedValues.includes(actualValue?.toLowerCase())) {
          return true; // Direct value match found, return true
        }
      }
    }
    return false; // No matches found
  }

  /**
   * Compares expected location values with user's location to determine a match.
   * @param expectedLocationMap A map of expected location values.
   * @param userLocation The user's actual location.
   * @returns A boolean indicating if the user's location matches the expected values.
   */
  async valuesMatch(expectedLocationMap, userLocation) {
    for (const [key, value] of Object.entries(expectedLocationMap)) {
      if (key in userLocation) {
        const normalizedValue1 = this.normalizeValue(value);
        const normalizedValue2 = this.normalizeValue(userLocation[key]);
        if (normalizedValue1 !== normalizedValue2) {
          return false;
        }
      } else {
        return false;
      }
    }
    return true; // If all values match, return true
  }

  /**
   * Normalizes a value to a consistent format for comparison.
   * @param value The value to normalize.
   * @returns The normalized value.
   */
  normalizeValue(value) {
    if (value === null || value === undefined) {
      return null;
    }
    // Remove quotes and trim whitespace
    return value.toString().replace(/^"|"$/g, '').trim();
  }
}
