// Pure helper functions for VCL Compliance Reporting
// No database dependencies — all functions are stateless and testable in isolation.

/**
 * Truncates text to maxLen characters with an ellipsis.
 * Returns '' for null/undefined input.
 */
function truncateText(text, maxLen = 80) {
    if (text == null) return '';
    if (text.length <= maxLen) return text;
    return text.slice(0, maxLen) + '\u2026';
}

/**
 * Validates that a remediation plan does not exceed 2000 characters.
 * Null/undefined/empty values are considered valid (no plan documented).
 */
function validateRemediationPlan(text) {
    if (text == null || text === '') return { valid: true };
    if (text.length > 2000) return { valid: false, error: 'Remediation plan exceeds 2000 characters' };
    return { valid: true };
}

/**
 * Returns true only for strings parseable as real calendar dates.
 * Rejects null, undefined, empty string, and invalid dates like "2026-02-30".
 */
function isValidDateString(str) {
    if (str == null || str === '') return false;
    if (typeof str !== 'string') return false;

    // Expect YYYY-MM-DD format
    const match = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if (!match) return false;

    const year = parseInt(match[1], 10);
    const month = parseInt(match[2], 10);
    const day = parseInt(match[3], 10);

    // Month must be 1-12
    if (month < 1 || month > 12) return false;

    // Create date and verify components match (catches invalid days like Feb 30)
    const date = new Date(year, month - 1, day);
    return (
        date.getFullYear() === year &&
        date.getMonth() === month - 1 &&
        date.getDate() === day
    );
}

/**
 * Formats a decimal as a whole-number percentage string.
 * Returns '0%' for null, undefined, or NaN input.
 */
function formatPct(decimal) {
    if (decimal == null || isNaN(decimal)) return '0%';
    return Math.round(decimal * 100) + '%';
}

/**
 * Computes VCL summary statistics from an array of device objects.
 * Each item should have at least { is_compliant: boolean, in_scope: boolean }.
 */
function computeVCLStats(items, targetPct) {
    const total = items.length;
    const in_scope = items.filter(item => item.in_scope).length;
    const compliant = items.filter(item => item.is_compliant).length;
    const non_compliant = in_scope - compliant;
    const remediations_required = non_compliant;
    const compliance_pct = in_scope > 0 ? Math.round((compliant / in_scope) * 100) : 0;

    return {
        total,
        in_scope,
        compliant,
        non_compliant,
        remediations_required,
        compliance_pct,
        target_pct: targetPct,
    };
}

/**
 * Partitions non-compliant items into "blocked" (no resolution_date) and
 * "in_progress" (resolution_date set). Returns counts and percentages.
 */
function categorizeNonCompliant(items) {
    const total = items.length;
    const blocked = items.filter(item => item.resolution_date == null);
    const in_progress = items.filter(item => item.resolution_date != null);

    return {
        blocked: {
            count: blocked.length,
            pct: total > 0 ? Math.round((blocked.length / total) * 100) : 0,
        },
        in_progress: {
            count: in_progress.length,
            pct: total > 0 ? Math.round((in_progress.length / total) * 100) : 0,
        },
    };
}

/**
 * Sorts verticals by non_compliant count in descending order.
 * Returns a new sorted array (does not mutate input).
 */
function rankHeavyHitters(verticalData) {
    return [...verticalData].sort((a, b) => b.non_compliant - a.non_compliant);
}

/**
 * Buckets non-compliant items by resolution_date month (YYYY-MM).
 * Items with null resolution_date are skipped.
 * Returns an object like { '2026-05': 3, '2026-06': 7 }.
 */
function computeForecastBurndown(items) {
    const buckets = {};
    for (const item of items) {
        if (item.resolution_date == null) continue;
        const dateStr = typeof item.resolution_date === 'string'
            ? item.resolution_date
            : item.resolution_date.toISOString().slice(0, 10);
        const month = dateStr.slice(0, 7); // YYYY-MM
        buckets[month] = (buckets[month] || 0) + 1;
    }
    return buckets;
}

/**
 * Matches uploaded rows to existing hostnames.
 * Returns { matched: [...], unmatched: [...] }.
 */
function matchByHostname(uploadedRows, existingHostnames) {
    const matched = [];
    const unmatched = [];
    for (const row of uploadedRows) {
        if (existingHostnames.has(row.hostname)) {
            matched.push(row);
        } else {
            unmatched.push(row);
        }
    }
    return { matched, unmatched };
}

/**
 * Compares uploaded row values against current DB values.
 * currentData is a Map of hostname -> { resolution_date, remediation_plan, notes }.
 * Returns array of { hostname, status: 'changed'|'unchanged', fields: { fieldName: { old, new } } }.
 */
function computeBulkDiff(matchedRows, currentData) {
    const results = [];
    const COMPARE_FIELDS = ['resolution_date', 'remediation_plan', 'notes'];

    for (const row of matchedRows) {
        const current = currentData.get(row.hostname) || {};
        const fields = {};
        let hasChange = false;

        for (const field of COMPARE_FIELDS) {
            if (field in row) {
                const oldVal = current[field] != null ? current[field] : null;
                const newVal = row[field] != null ? row[field] : null;
                if (oldVal !== newVal) {
                    fields[field] = { old: oldVal, new: newVal };
                    hasChange = true;
                }
            }
        }

        results.push({
            hostname: row.hostname,
            status: hasChange ? 'changed' : 'unchanged',
            fields,
        });
    }

    return results;
}

/**
 * Maps column header strings to known field names (case-insensitive).
 * Returns a mapping object like { hostname: 0, resolution_date: 3 } where values are column indices.
 */
function mapColumnHeaders(headers) {
    const mapping = {};
    const KNOWN_MAPPINGS = {
        hostname: 'hostname',
        'resolution date': 'resolution_date',
        resolution_date: 'resolution_date',
        'remediation plan': 'remediation_plan',
        remediation_plan: 'remediation_plan',
        notes: 'notes',
    };

    for (let i = 0; i < headers.length; i++) {
        const normalized = headers[i].trim().toLowerCase();
        if (KNOWN_MAPPINGS[normalized]) {
            mapping[KNOWN_MAPPINGS[normalized]] = i;
        }
    }

    return mapping;
}

module.exports = {
    truncateText,
    validateRemediationPlan,
    isValidDateString,
    formatPct,
    computeVCLStats,
    categorizeNonCompliant,
    rankHeavyHitters,
    computeForecastBurndown,
    matchByHostname,
    computeBulkDiff,
    mapColumnHeaders,
};
