/**
 * @file validation.js
 *
 * Permission is hereby granted to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to use it for personal 
 * or non-commercial purposes, with the following restrictions:
 *
 * 1. **No Copying or Redistribution**: The Software or any of its parts may not 
 *    be copied, merged, distributed, sublicensed, or sold without explicit 
 *    prior written permission from the author.
 * 
 * 2. **Commercial Use**: Any use of the Software for commercial purposes requires 
 *    a valid license, obtainable only with the explicit consent of the author.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 
 * OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 * SOFTWARE.
 *
 * Ownership of this code remains solely with the original author. Unauthorized 
 * use of this Software is strictly prohibited.
 
    * @summary Validation utility for validating and constraining configuration values.
    * @description Validation utility for validating and constraining configuration values.
    * @module ValidationUtils
    * @requires Logger
    * @exports ValidationUtils
    * @version 0.1.0
    * @since 0.1.0
*/

const Logger = require("./logger");

class ValidationUtils {
  constructor(IloggerEnabled, IloggerLevel) {
        const loggerEnabled = IloggerEnabled || true;
        const loggerLevel = IloggerLevel || "warn";
        this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
  }

    constrain(value, min, max) {
        if (typeof value !== "number") {
            this.logger?.warn(`Value '${value}' is not a number. Defaulting to ${min}.`);
            return min;
        }
        return Math.min(Math.max(value, min), max);
    }

    validateSchema(config, schema, name) {

        const validatedConfig = {};
        let configValue;

        // 1. Remove any unknown keys (keys not defined in the schema).
        //    Log a warning and omit them from the final config.
        for (const key of Object.keys(config)) {
            if (!(key in schema)) {
            this.logger.warn(
                `[${name}] Unknown key '${key}' found in config. Removing it.`
            );
            delete config[key];
            }
        }
    
        // Validate each key in the schema and loop over wildcards if they are not in schema
        for ( const key in schema ) {

            if (key === "rules" || key === "description" || key === "schema") {
                continue;
              }

            const fieldSchema = schema[key];
            const { rules = {} } = fieldSchema;
    
            // Default to the schema's default value if the key is missing
            if (config[key] === undefined) {
                if (fieldSchema.default === undefined) {
                    // If there's a nested schema, go deeper with an empty object rather than logging "no rule"
                    if (rules.schema) {
                        this.logger.warn(`${name}.${key} has no default, but has a nested schema.`);
                        validatedConfig[key] = this.validateSchema({}, rules.schema, `${name}.${key}`);
                    }
                    else {
                    this.logger.info(
                        `There is no rule for ${name}.${key} and no default value. ` +
                        `Using full schema value but validating deeper levels first...`
                    );

                    const SubObject = this.validateSchema({}, fieldSchema, `${name}.${key}`);

                    validatedConfig[key] = SubObject;

                    continue;
                    }
                } else {
                    this.logger.info(`There is no value provided for ${name}.${key}. Using default value.`);
                    configValue = fieldSchema.default;
                }
                //continue;
            } else {
                // Use the provided value if it exists, otherwise use the default value
                configValue = config[key] !== undefined ? config[key] : fieldSchema.default;
            }

            // Attempt to parse the value to the expected type if possible
            switch (rules.type) {

                case "number":
                    configValue = this.validateNumber(configValue, rules, fieldSchema, name, key);
                    break;
                case "boolean":
                    configValue = this.validateBoolean(configValue, name, key);
                    break;
    
                case "string":
                    configValue = this.validateString(configValue,rules,fieldSchema, name, key);
                    break;
    
                case "array":
                    configValue = this.validateArray(configValue, rules, fieldSchema, name, key);
                    break;
                
                case "set":
                    configValue = this.validateSet(configValue, rules, fieldSchema, name, key);
                    break;
    
                case "object":
                    configValue = this.validateObject(configValue, rules, fieldSchema, name, key);
                    break;

                case "enum":
                    configValue = this.validateEnum(configValue, rules, fieldSchema, name, key);
                    break;

                case "curve":
                    validatedConfig[key] = this.validateCurve(configValue,fieldSchema.default);
                    continue;

                case "machineCurve":
                    validatedConfig[key] = this.validateMachineCurve(configValue,fieldSchema.default);
                    continue;

                case "integer":
                    validatedConfig[key] = this.validateInteger(configValue, rules, fieldSchema, name, key);
                    continue;

                case undefined:
                    // If we see 'rules.schema' but no 'rules.type', treat it like an object:
                    if (rules.schema && !rules.type) {
                        // Log a warning and skip the extra pass for nested schema
                        this.logger.warn(
                            `${name}.${key} has a nested schema but no type. ` +
                            `Treating it as type="object" to skip extra pass.`
                        );
                    } else {
                        // Otherwise, fallback to your existing "validateUndefined" logic
                        validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
                    }
                    continue;

                default:
                    this.logger.warn(`${name}.${key} has an unknown validation type: ${rules.type}. Skipping validation.`);
                    validatedConfig[key] = fieldSchema.default;
                    continue;
            }
    
            // Assign the validated or converted value
            validatedConfig[key] = configValue;
        }
    
        // Ignore unknown keys by not processing them at all
        this.logger.info(`Validation completed for ${name}.`);

        return validatedConfig;
    } 

    removeUnwantedKeys(obj) {

        if (Array.isArray(obj)) {
          return obj.map((item) => this.removeUnwantedKeys(item));
        }
        if (obj && typeof obj === "object") {
          const newObj = {};
          for (const [k, v] of Object.entries(obj)) {

            // Skip or remove keys like 'default', 'rules', 'description', etc.
            if (["rules", "description"].includes(k)) {
              continue;
            }

            if("default" in v){
                //put the default value in the object
                newObj[k] = v.default;
                continue;
            }

            newObj[k] = this.removeUnwantedKeys(v);
          }
          return newObj;
        }
        return obj;
    }
      
    validateMachineCurve(curve, defaultCurve) {
        if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
            this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
            return defaultCurve;
        }

        // Validate that nq and np exist and are objects
        const { nq, np } = curve;
        if (!nq || typeof nq !== "object" || !np || typeof np !== "object") {
            this.logger.warn("Curve must contain valid 'nq' and 'np' objects. Defaulting to basic curve.");
            return defaultCurve;
        }

        // Validate that each dimension key points to a valid object with x and y arrays
        const validatedNq = this.validateDimensionStructure(nq, "nq");
        const validatedNp = this.validateDimensionStructure(np, "np");

        if (!validatedNq || !validatedNp) {
            return defaultCurve;
        }

        return { nq: validatedNq, np: validatedNp }; // Return the validated curve
    }

    validateCurve(curve, defaultCurve) {
        if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
            this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
            return defaultCurve;
        }

        // Validate that each dimension key points to a valid object with x and y arrays
        const validatedCurve = this.validateDimensionStructure(curve, "curve");
        if (!validatedCurve) {
            return defaultCurve;
        }

        return validatedCurve; // Return the validated curve
    }

    validateDimensionStructure(dimension, name) {
        const validatedDimension = {};

        for (const [key, value] of Object.entries(dimension)) {
            // Validate that each key points to an object with x and y arrays
            if (typeof value !== "object") {
                this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
                return false;
            }
            // Validate that x and y are arrays
            else if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
                this.logger.warn(`Dimension '${name}' key '${key}' is missing x or y arrays. Converting to arrays.`);
                // Try to convert to arrays first
                value.x = Object.values(value.x);
                value.y = Object.values(value.y);

                // If still not arrays return false
                if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
                    this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
                    return false;
                }
            }
            // Validate that x and y arrays are the same length
            else if (value.x.length !== value.y.length) {
                this.logger.warn(`Dimension '${name}' key '${key}' has mismatched x and y lengths. Ignoring this key.`);
                return false;
            }
            // Validate that x values are in ascending order
            else if (!this.isSorted(value.x)) {
                this.logger.warn(`Dimension '${name}' key '${key}' has unsorted x values. Sorting...`);
                return false;
            }
            // Validate that x values are unique
            else if (!this.isUnique(value.x)) {
                this.logger.warn(`Dimension '${name}' key '${key}' has duplicate x values. Removing duplicates...`);
                return false;
            }
            // Validate that y values are numbers
            else if (!this.areNumbers(value.y)) {
                this.logger.warn(`Dimension '${name}' key '${key}' has non-numeric y values. Ignoring this key.`);
                return false;
            }

            validatedDimension[key] = value;
        }
        return validatedDimension;
    }

    isSorted(arr) {
        return arr.every((_, i) => i === 0 || arr[i] >= arr[i - 1]);
    }

    isUnique(arr) {
        return new Set(arr).size === arr.length;
    }

    areNumbers(arr) {
        return arr.every((x) => typeof x === "number");
    }

    validateNumber(configValue, rules, fieldSchema, name, key) {

        if (typeof configValue !== "number") {
            const parsedValue = parseFloat(configValue);
            if (!isNaN(parsedValue)) {
                this.logger.warn(`${name}.${key} was parsed to a number: ${configValue} -> ${parsedValue}`);
                configValue = parsedValue;
            }
        }

        if (rules.min !== undefined && configValue < rules.min) {
            this.logger.warn(
                `${name}.${key} is below the minimum (${rules.min}). Using default value.`
            );
            return fieldSchema.default;
        }
        if (rules.max !== undefined && configValue > rules.max) {
            this.logger.warn(
                `${name}.${key} exceeds the maximum (${rules.max}). Using default value.`
            );
            return fieldSchema.default;
        }
        
        this.logger.debug(`${name}.${key} is a valid number: ${configValue}`);

        return configValue;
    }


    validateInteger(configValue, rules, fieldSchema, name, key) {
        if (typeof configValue !== "number" || !Number.isInteger(configValue)) {
            const parsedValue = parseInt(configValue, 10);
            if (!isNaN(parsedValue) && Number.isInteger(parsedValue)) {
                this.logger.warn(`${name}.${key} was parsed to an integer: ${configValue} -> ${parsedValue}`);
                configValue = parsedValue;
            } else {
                this.logger.warn(`${name}.${key} is not a valid integer. Using default value.`);
                return fieldSchema.default;
            }
        }

        if (rules.min !== undefined && configValue < rules.min) {
            this.logger.warn(`${name}.${key} is below the minimum integer value (${rules.min}). Using default value.`);
            return fieldSchema.default;
        }

        if (rules.max !== undefined && configValue > rules.max) {
            this.logger.warn(`${name}.${key} exceeds the maximum integer value (${rules.max}). Using default value.`);
            return fieldSchema.default;
        }

        this.logger.debug(`${name}.${key} is a valid integer: ${configValue}`);
        return configValue;
    }

    validateBoolean(configValue, name, key) {
        if (typeof configValue !== "boolean") {
            if (configValue === "true" || configValue === "false") {
                const parsedValue = configValue === "true";
                this.logger.debug(`${name}.${key} was parsed to a boolean: ${configValue} -> ${parsedValue}`);
                configValue = parsedValue;
            }
        }
        return configValue;
    }

    validateString(configValue, rules, fieldSchema, name, key) {
        let newConfigValue = configValue;

        if (typeof configValue !== "string") {
            //check if the value is nullable
            if(rules.nullable){
                if(configValue === null){
                    return null;
                }
            }

            this.logger.warn(`${name}.${key} is not a string. Trying to convert to string.`);
            newConfigValue = String(configValue); // Coerce to string if not already
        }
        
        //check if the string is a valid string after conversion
        if (typeof newConfigValue !== "string") {
            this.logger.warn(`${name}.${key} is not a valid string. Using default value.`);
            return fieldSchema.default;
        }

        // Check for uppercase characters and convert to lowercase if present
        if (newConfigValue !== newConfigValue.toLowerCase()) {
            this.logger.warn(`${name}.${key} contains uppercase characters. Converting to lowercase: ${newConfigValue} -> ${newConfigValue.toLowerCase()}`);
            newConfigValue = newConfigValue.toLowerCase();
        }

        return newConfigValue;
    }

    validateSet(configValue, rules, fieldSchema, name, key) {
        // 1. Ensure we have a Set. If not, use default.
        if (!(configValue instanceof Set)) {
            this.logger.info(`${name}.${key} is not a Set. Converting to one using default value.`);
            return new Set(fieldSchema.default);
        }
    
        // 2. Convert the Set to an array for easier filtering.
        const validatedArray = [...configValue]
            .filter((item) => {
                // 3. Filter based on `rules.itemType`.
                switch (rules.itemType) {
                    case "number":
                        return typeof item === "number";
                    case "string":
                        return typeof item === "string";
                    case "null":
                        // "null" might mean no type restriction (your usage may vary).
                        return true;
                    default:
                        // Fallback if itemType is something else
                        return typeof item === rules.itemType;
                }
            })
            .slice(0, rules.maxLength || Infinity);
    
        // 4. Check if the filtered array meets the minimum length.
        if (validatedArray.length < (rules.minLength || 1)) {
            this.logger.warn(
                `${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
            );
            return new Set(fieldSchema.default);
        }
    
        // 5. Return a new Set containing only the valid items.
        return new Set(validatedArray);
    }

    validateArray(configValue, rules, fieldSchema, name, key) {
        if (!Array.isArray(configValue)) {
            this.logger.info(`${name}.${key} is not an array. Using default value.`);
            return fieldSchema.default;
        }

        // Validate individual items in the array
        const validatedArray = configValue
            .filter((item) => {
                switch (rules.itemType) {
                    case "number":
                        return typeof item === "number";
                    case "string":
                        return typeof item === "string";
                    case "null":
                        // anything goes
                        return true;
                    default:
                        return typeof item === rules.itemType;
                }
            })
            .slice(0, rules.maxLength || Infinity);

        if (validatedArray.length < (rules.minLength || 1)) {
            this.logger.warn(
                `${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
            );
            return fieldSchema.default;
        }

        return validatedArray;
    }

    validateObject(configValue, rules, fieldSchema, name, key) {
        if (typeof configValue !== "object" || Array.isArray(configValue)) {
            this.logger.warn(`${name}.${key} is not a valid object. Using default value.`);
            return fieldSchema.default;
        }

        if (rules.schema) {
            // Recursively validate nested objects if a schema is defined
            return this.validateSchema(configValue || {}, rules.schema, `${name}.${key}`);
        } else {
            // If no schema is defined, log a warning and use the default
            this.logger.warn(`${name}.${key} is an object with no schema. Using default value.`);
            return fieldSchema.default;
        }
    }

    validateEnum(configValue, rules, fieldSchema, name, key) {

        if (Array.isArray(rules.values)) {

            //if value is null take default
            if(configValue === null){
                this.logger.warn(`${name}.${key} is null. Using default value.`);
                return fieldSchema.default;
            }

            const validValues = rules.values.map(e => e.value.toLowerCase());

            //remove caps
            configValue = configValue.toLowerCase();

            if (!validValues.includes(configValue)) {
                this.logger.warn(
                    `${name}.${key} has an invalid value : ${configValue}. Allowed values: [${validValues.join(", ")}]. Using default value.`
                );
                return fieldSchema.default;
            }
        } else {
            this.logger.warn(
                `${name}.${key} is an enum with no 'values' array. Using default value.`
            );
            return fieldSchema.default;
        }
        return configValue;
    }

    validateUndefined(configValue, fieldSchema, name, key) {
        if (typeof configValue === "object" && !Array.isArray(configValue)) {

            this.logger.debug(`${name}.${key} has no defined rules but is an object going 1 level deeper.`);

            // Recursively validate the nested object
            return this.validateSchema( configValue || {}, fieldSchema, `${name}.${key}`);
            } 
            else {
                this.logger.warn(`${name}.${key} has no defined rules. Using default value.`);
                return fieldSchema.default;
            }
    }
}

module.exports = ValidationUtils;
