/**
 * @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.2.0
    * @since 0.1.0
*/

const Logger = require("./logger");
const { validateNumber, validateInteger, validateBoolean, validateString, validateEnum } = require("./validators/typeValidators");
const { validateArray, validateSet, validateObject } = require("./validators/collectionValidators");
const { validateCurve, validateMachineCurve } = require("./validators/curveValidator");

// Strategy registry: maps rules.type to a handler function
const VALIDATORS = {
    number:       (cv, rules, fs, name, key, logger)       => validateNumber(cv, rules, fs, name, key, logger),
    integer:      (cv, rules, fs, name, key, logger)       => validateInteger(cv, rules, fs, name, key, logger),
    boolean:      (cv, _rules, _fs, name, key, logger)     => validateBoolean(cv, name, key, logger),
    string:       (cv, rules, fs, name, key, logger)       => validateString(cv, rules, fs, name, key, logger),
    enum:         (cv, rules, fs, name, key, logger)       => validateEnum(cv, rules, fs, name, key, logger),
    array:        (cv, rules, fs, name, key, logger)       => validateArray(cv, rules, fs, name, key, logger),
    set:          (cv, rules, fs, name, key, logger)       => validateSet(cv, rules, fs, name, key, logger),
};

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

    _logOnce(level, onceKey, message) {
        if (onceKey && this._onceLogCache.has(onceKey)) {
            return;
        }
        if (onceKey) {
            this._onceLogCache.add(onceKey);
        }
        if (typeof this.logger?.[level] === "function") {
            this.logger[level](message);
        }
    }

    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" || key === "version") {
                continue;
              }

            const fieldSchema = schema[key];

            // Skip non-object schema entries (e.g. primitive values injected by migration)
            if (fieldSchema === null || typeof fieldSchema !== 'object') {
                this.logger.debug(`${name}.${key} has a non-object schema entry (${typeof fieldSchema}). Skipping.`);
                validatedConfig[key] = fieldSchema;
                continue;
            }

            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.debug(`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;
            }

            // Handle curve types (they use continue, so handle separately)
            if (rules.type === "curve") {
                validatedConfig[key] = validateCurve(configValue, fieldSchema.default, this.logger);
                continue;
            }
            if (rules.type === "machineCurve") {
                validatedConfig[key] = validateMachineCurve(configValue, fieldSchema.default, this.logger);
                continue;
            }

            // Handle object type (needs recursive validateSchema reference)
            if (rules.type === "object") {
                validatedConfig[key] = validateObject(
                    configValue, rules, fieldSchema, name, key,
                    (c, s, n) => this.validateSchema(c, s, n),
                    this.logger
                );
                continue;
            }

            // Handle undefined type
            if (rules.type === undefined) {
                if (rules.schema && !rules.type) {
                    this.logger.warn(
                        `${name}.${key} has a nested schema but no type. ` +
                        `Treating it as type="object" to skip extra pass.`
                    );
                } else {
                    validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
                }
                continue;
            }

            // Use the strategy registry for all other types
            const handler = VALIDATORS[rules.type];
            if (handler) {
                configValue = handler(configValue, rules, fieldSchema, name, key, this.logger);
            } else {
                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(v && typeof v === "object" && "default" in v){
                //put the default value in the object
                newObj[k] = v.default;
                continue;
            }

            newObj[k] = this.removeUnwantedKeys(v);
          }
          return newObj;
        }
        return obj;
    }

    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;
            }
    }

    // Public wrapper for the curve validator — exposes the helper so
    // callers (and tests) can validate a raw curve without going
    // through validateSchema.
    validateCurve(input, defaultCurve) {
        return validateCurve(input, defaultCurve, this.logger);
    }
}

module.exports = ValidationUtils;
