/**
 * configLoader — load and validate .acurity.json project-level configuration.
 *
 * Kept separate from cli/index.js so that parseConfig, matchesGlob, and resolveFailOn
 * can be unit-tested without spawning a subprocess.
 *
 * .acurity.json schema:
 * {
 *   "naming": {                              // optional
 *     "charToTypeMap":  { "<char>": "<TYPE>", ... },
 *     "scopeToCharMap": { "<scope>": "<char>", ... }
 *   },
 *   "overrides": [                           // optional
 *     { "paths": ["src/legacy/**"], "failOn": "none" },
 *     { "paths": ["src/new/**"],    "failOn": "warning" }
 *   ]
 * }
 */

import * as fs from "fs";

const VALID_FAIL_ON = new Set(["error", "warning", "information", "hint", "none"]);

// ---------------------------------------------------------------------------
// parseConfig
// ---------------------------------------------------------------------------

/**
 * Parses and validates the contents of an .acurity.json string.
 * Throws an Error with a human-readable message on any schema violation.
 *
 * @param {string} jsonStr - raw file contents
 * @returns {object} - validated config object
 */
function parseConfig(jsonStr) {
    let raw;
    try {
        raw = JSON.parse(jsonStr);
    } catch (e) {
        throw new Error(`Invalid JSON: ${e.message}`);
    }

    if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
        throw new Error("Config root must be a JSON object.");
    }

    if (raw.overrides !== undefined) {
        if (!Array.isArray(raw.overrides)) {
            throw new Error('"overrides" must be an array.');
        }
        for (let i = 0; i < raw.overrides.length; i++) {
            const ov = raw.overrides[i];
            if (typeof ov !== "object" || ov === null || Array.isArray(ov)) {
                throw new Error(`overrides[${i}] must be an object.`);
            }
            if (!Array.isArray(ov.paths)) {
                throw new Error(`overrides[${i}].paths must be an array of glob strings.`);
            }
            for (let j = 0; j < ov.paths.length; j++) {
                if (typeof ov.paths[j] !== "string") {
                    throw new Error(`overrides[${i}].paths[${j}] must be a string.`);
                }
            }
            if (!VALID_FAIL_ON.has(ov.failOn)) {
                throw new Error(
                    `overrides[${i}].failOn must be one of: ${[...VALID_FAIL_ON].join(", ")}.`
                );
            }
        }
    }

    if (raw.naming !== undefined) {
        if (typeof raw.naming !== "object" || raw.naming === null || Array.isArray(raw.naming)) {
            throw new Error('"naming" must be an object.');
        }
        if (raw.naming.charToTypeMap !== undefined) {
            if (
                typeof raw.naming.charToTypeMap !== "object" ||
                raw.naming.charToTypeMap === null ||
                Array.isArray(raw.naming.charToTypeMap)
            ) {
                throw new Error('"naming.charToTypeMap" must be an object.');
            }
            for (const [k, v] of Object.entries(raw.naming.charToTypeMap)) {
                if (typeof k !== "string" || k.length !== 1) {
                    throw new Error(`naming.charToTypeMap key "${k}" must be a single character.`);
                }
                if (typeof v !== "string") {
                    throw new Error(`naming.charToTypeMap["${k}"] must be a string type name.`);
                }
            }
        }
        if (raw.naming.scopeToCharMap !== undefined) {
            if (
                typeof raw.naming.scopeToCharMap !== "object" ||
                raw.naming.scopeToCharMap === null ||
                Array.isArray(raw.naming.scopeToCharMap)
            ) {
                throw new Error('"naming.scopeToCharMap" must be an object.');
            }
            for (const [k, v] of Object.entries(raw.naming.scopeToCharMap)) {
                if (typeof v !== "string" || v.length !== 1) {
                    throw new Error(
                        `naming.scopeToCharMap["${k}"] must be a single character string.`
                    );
                }
            }
        }
    }

    return raw;
}

// ---------------------------------------------------------------------------
// loadConfig
// ---------------------------------------------------------------------------

/**
 * Reads and parses .acurity.json from the given path.
 * Exits with code 2 on file-not-found or schema violation.
 *
 * @param {string} configPath - absolute path to .acurity.json
 * @returns {object} - validated config object
 */
function loadConfig(configPath) {
    let raw;
    try {
        raw = fs.readFileSync(configPath, "utf8");
    } catch (_) {
        console.error(`Error: cannot read config file: ${configPath}`);
        process.exit(2);
    }
    try {
        return parseConfig(raw);
    } catch (e) {
        console.error(`Error: invalid config file ${configPath}: ${e.message}`);
        process.exit(2);
    }
}

// ---------------------------------------------------------------------------
// matchesGlob
// ---------------------------------------------------------------------------

/**
 * Returns true when filePath matches pattern.
 * Supports ** (zero or more path segments) and * (any chars within one segment).
 * Both separators (/ and \) are normalised to / before matching.
 *
 * @param {string} pattern - glob pattern, e.g. "src/legacy/**"
 * @param {string} filePath - relative file path, e.g. "src/legacy/report.TXT"
 * @returns {boolean}
 */
function matchesGlob(pattern, filePath) {
    const patParts  = pattern.replace(/\\/g,  "/").split("/");
    const fileParts = filePath.replace(/\\/g, "/").split("/");
    return _matchParts(patParts, fileParts, 0, 0);
}

function _matchParts(patParts, fileParts, pi, fi) {
    while (pi < patParts.length) {
        const pat = patParts[pi];
        if (pat === "**") {
            // ** matches zero or more file path segments — try every possible skip count.
            for (let skip = 0; skip <= fileParts.length - fi; skip++) {
                if (_matchParts(patParts, fileParts, pi + 1, fi + skip)) return true;
            }
            return false;
        }
        if (fi >= fileParts.length) return false;
        if (!_matchSegment(pat, fileParts[fi])) return false;
        pi++;
        fi++;
    }
    return fi === fileParts.length;
}

function _matchSegment(pattern, str) {
    if (!pattern.includes("*")) return pattern === str;
    // Convert * to a regex fragment that matches anything except a path separator.
    const regexStr = pattern
        .split("*")
        .map((s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&"))
        .join("[^/]*");
    return new RegExp(`^${regexStr}$`).test(str);
}

// ---------------------------------------------------------------------------
// resolveFailOn
// ---------------------------------------------------------------------------

/**
 * Resolves the effective failOn threshold for a given file path by matching
 * it against the overrides in the config. First match wins.
 * Returns defaultFailOn when config is absent or no override matches.
 *
 * @param {object|null} config - parsed .acurity.json (or null)
 * @param {string} relPath - relative file path from the scanned folder root
 * @param {string} defaultFailOn - the global --fail-on value
 * @returns {string}
 */
function resolveFailOn(config, relPath, defaultFailOn) {
    if (!config || !Array.isArray(config.overrides)) return defaultFailOn;
    const normalised = relPath.replace(/\\/g, "/");
    for (const override of config.overrides) {
        for (const pattern of override.paths) {
            if (matchesGlob(pattern, normalised)) {
                return override.failOn;
            }
        }
    }
    return defaultFailOn;
}

export { parseConfig, loadConfig, matchesGlob, resolveFailOn };
