/**
 * Validation Module
 * Handles: JSON validation, schema structure validation, bulk validation
 */
(function($) {
    'use strict';

    /**
     * The JSON-LD nodes a document actually contains.
     *
     * A document is one of three shapes, the same three the save handler walks
     * in schemafy_find_decoded_schema_node(): a single node, an @graph
     * container, or a bare array of nodes. Only the first used to be
     * recognised, so every @graph document — the shape Yoast, Rank Math and
     * most markup copied off a page use — and every array reported "Missing
     * @type" no matter what was actually inside it.
     *
     * @param {*} doc Parsed JSON.
     * @return {Array} Nodes to check.
     */
    function jsonLdNodes(doc) {
        if (Array.isArray(doc)) {
            return doc;
        }

        if (doc && typeof doc === 'object') {
            return Array.isArray(doc['@graph']) ? doc['@graph'] : [doc];
        }

        return [];
    }

    /**
     * Whether an @context points at schema.org, in any form JSON-LD allows:
     * a string, an array, or a context object such as {"@vocab": "..."}.
     *
     * Only the string form used to be handled, with .includes() called
     * straight on the value — on an object or array that throws, and the
     * exception surfaced to the user as a bogus "Invalid JSON" syntax error.
     *
     * @param {*} context The @context value.
     * @return {boolean}
     */
    function contextMentionsSchemaOrg(context) {
        if (typeof context === 'string') {
            return context.indexOf('schema.org') !== -1;
        }

        if (Array.isArray(context)) {
            return context.some(contextMentionsSchemaOrg);
        }

        if (context && typeof context === 'object') {
            return Object.keys(context).some(function(key) {
                return contextMentionsSchemaOrg(context[key]);
            });
        }

        return false;
    }

    /**
     * Required properties and length limits per schema type, published by PHP
     * from the Smart Builder's field configs. Absent for any type this build
     * ships no config for, in which case only structure gets checked.
     *
     * @return {Object}
     */
    function schemaRules() {
        return (typeof schemafy_ajax !== 'undefined' && schemafy_ajax.schema_rules)
            ? schemafy_ajax.schema_rules
            : {};
    }

    /**
     * Read a value at a schemaPath, which may be dotted ("brand.name").
     *
     * @param {Object} node The JSON-LD node.
     * @param {string} path Property path.
     * @return {*} The value, or undefined.
     */
    function valueAtPath(node, path) {
        return path.split('.').reduce(function(current, key) {
            return (current && typeof current === 'object') ? current[key] : undefined;
        }, node);
    }

    /**
     * Whether a value counts as absent to a search engine.
     *
     * A property present but empty is worth exactly as much as one that is
     * missing, so both are reported the same way. Whitespace-only strings and
     * empty arrays and objects all qualify.
     *
     * @param {*} value Value to test.
     * @return {boolean}
     */
    function isBlank(value) {
        if (value === undefined || value === null) {
            return true;
        }
        if (typeof value === 'string') {
            return value.trim() === '';
        }
        if (Array.isArray(value)) {
            return value.length === 0;
        }
        if (typeof value === 'object') {
            return Object.keys(value).length === 0;
        }

        return false;
    }

    /**
     * Nested objects that carry no @type, which search engines cannot
     * interpret as anything. Reported as warnings, since a plain value object
     * is occasionally legitimate.
     *
     * Keys beginning with "@" are skipped: @context in particular is an object
     * by design and never carries a type.
     *
     * @param {*}      value  Value to walk.
     * @param {string} prefix Property path walked so far.
     * @param {Array}  found  Accumulator.
     * @return {Array} Property paths of untyped nested objects.
     */
    function untypedNestedObjects(value, prefix, found) {
        found = found || [];

        if (Array.isArray(value)) {
            value.forEach(function(item, i) {
                untypedNestedObjects(item, prefix + '[' + i + ']', found);
            });
            return found;
        }

        if (!value || typeof value !== 'object') {
            return found;
        }

        if (prefix !== '' && !value['@type']) {
            found.push(prefix);
        }

        Object.keys(value).forEach(function(key) {
            if (key.charAt(0) === '@') {
                return;
            }
            untypedNestedObjects(value[key], prefix === '' ? key : prefix + '.' + key, found);
        });

        return found;
    }

    /**
     * Everything wrong with one JSON-LD node.
     *
     * This is the single check both the Manual JSON tab and the bulk validator
     * run. They used to disagree: the bulk one carried a hand-written list that
     * knew three types, and the manual one checked no properties at all.
     *
     * @param {Object} node  A JSON-LD node.
     * @param {string} label How to name it in messages.
     * @return {{errors: Array, warnings: Array, typeKnown: boolean}}
     */
    function nodeIssues(node, label) {
        const errors = [];
        const warnings = [];
        const prefix = label ? label + ': ' : '';

        if (!node || typeof node !== 'object' || Array.isArray(node)) {
            return { errors: [prefix + 'not a JSON object'], warnings: warnings, typeKnown: false };
        }

        const type = node['@type'];

        if (isBlank(type)) {
            return { errors: [prefix + 'missing @type'], warnings: warnings, typeKnown: false };
        }

        // @type may be an array; the rules of any listed type apply.
        const types = Array.isArray(type) ? type : [type];
        const rules = schemaRules();
        const known = types.filter(function(t) { return rules[t]; });

        known.forEach(function(t) {
            const rule = rules[t] || {};

            (rule.required || []).forEach(function(path) {
                if (isBlank(valueAtPath(node, path))) {
                    errors.push(prefix + t + ' is missing required property "' + path + '"');
                }
            });

            Object.keys(rule.maxLength || {}).forEach(function(path) {
                const value = valueAtPath(node, path);
                if (typeof value === 'string' && value.length > rule.maxLength[path]) {
                    warnings.push(
                        prefix + '"' + path + '" is ' + value.length +
                        ' characters, over the ' + rule.maxLength[path] + ' recommended for ' + t
                    );
                }
            });
        });

        untypedNestedObjects(node, '', []).forEach(function(path) {
            warnings.push(prefix + '"' + path + '" is an object with no @type');
        });

        return { errors: errors, warnings: warnings, typeKnown: known.length > 0 };
    }

    /**
     * Render findings one per line, capped so a badly broken document reports
     * a readable summary instead of a wall of text.
     *
     * @param {Array}  items Findings.
     * @param {number} max   How many to spell out.
     * @return {string}
     */
    function asBulletList(items, max) {
        max = max || 6;
        const shown = items.slice(0, max).map(function(item) { return '• ' + item; });
        const hidden = items.length - shown.length;

        if (hidden > 0) {
            shown.push('• …and ' + hidden + ' more');
        }

        return shown.join('\n');
    }

    window.SchemafyValidation = {
        /**
         * Validate JSON string
         */
        validateJson: function(selector) {
            const $textarea = $(selector);
            const value = $textarea.val();

            try {
                JSON.parse(value);
                window.SchemafyUtilities.showNotification('JSON is valid!', 'success');
                return true;
            } catch(e) {
                window.SchemafyUtilities.showNotification('Invalid JSON: ' + e.message, 'error');
                return false;
            }
        },

        /**
         * Auto-validate JSON and update status indicator
         */
        autoValidateJson: function(editorSelector, statusSelector) {
            const value = $(editorSelector).val();
            const $status = $(statusSelector);

            if (!value) {
                $status.removeClass('valid invalid');
                $status.html('<span class="dashicons dashicons-info"></span> Paste JSON to validate');
                return;
            }

            try {
                JSON.parse(value);
                $status.removeClass('invalid').addClass('valid');
                $status.html('<span class="dashicons dashicons-yes"></span> Valid JSON');
            } catch(e) {
                $status.removeClass('valid').addClass('invalid');
                let errMsg = e.message || 'syntax error';
                const posMatch = errMsg.match(/position (\d+)/);
                if (posMatch) {
                    const pos = parseInt(posMatch[1]);
                    const lines = value.substring(0, pos).split('\n');
                    errMsg = 'Line ' + lines.length + ': ' + errMsg;
                }
                $status.html('<span class="dashicons dashicons-dismiss"></span> Invalid JSON — ' + errMsg);
            }
        },

        /**
         * Enhanced JSON validation with detailed errors
         */
        validateJsonEnhanced: function() {
            const $textarea = $('#schemafy-manual-json');
            let value = $textarea.val();

            if (!value.trim()) {
                window.SchemafyUtilities.showNotification('Please enter JSON to validate', 'warning');
                return false;
            }

            // Auto-strip <script type="application/ld+json"> wrapper if present
            if (window.SchemafyEditor && typeof window.SchemafyEditor.stripScriptWrapper === 'function') {
                const clean = window.SchemafyEditor.stripScriptWrapper(value);
                if (clean !== value) {
                    value = clean;
                    $textarea.val(value).trigger('input');
                }
            }

            try {
                const parsed = JSON.parse(value);

                // Check for required schema.org properties
                const errors = [];
                const warnings = [];
                const nodes = jsonLdNodes(parsed);

                // @context belongs to the document for a single node or an
                // @graph, but to each node individually in a bare array.
                const contexts = Array.isArray(parsed)
                    ? nodes.map(function(node) { return node && node['@context']; })
                    : [parsed && parsed['@context']];

                if (!contexts.length || contexts.some(function(c) { return !c; })) {
                    errors.push('Missing @context property');
                } else if (!contexts.every(contextMentionsSchemaOrg)) {
                    warnings.push('@context should reference schema.org');
                }

                if (!nodes.length) {
                    errors.push('No schema found — expected an object, an array of objects, or an @graph');
                }

                // Check each node's actual properties against what its type
                // requires, not just that a type is present.
                let unknownTypes = 0;

                nodes.forEach(function(node, i) {
                    const label = nodes.length > 1 ? 'Schema ' + (i + 1) : '';
                    const issues = nodeIssues(node, label);

                    Array.prototype.push.apply(errors, issues.errors);
                    Array.prototype.push.apply(warnings, issues.warnings);

                    if (!issues.typeKnown && issues.errors.length === 0) {
                        unknownTypes++;
                    }
                });

                if (unknownTypes > 0 && errors.length === 0) {
                    warnings.push(
                        unknownTypes === nodes.length
                            ? 'This build has no field definition for that type, so only structure was checked'
                            : unknownTypes + ' schemas have a type this build has no field definition for, so only their structure was checked'
                    );
                }

                if (errors.length > 0) {
                    window.SchemafyUtilities.showNotification(
                        errors.length + (errors.length === 1 ? ' problem found:' : ' problems found:') +
                        '\n' + asBulletList(errors.concat(warnings)),
                        'error'
                    );
                    return false;
                }

                if (warnings.length > 0) {
                    window.SchemafyUtilities.showNotification(
                        'Valid, with ' + warnings.length + (warnings.length === 1 ? ' suggestion:' : ' suggestions:') +
                        '\n' + asBulletList(warnings),
                        'warning'
                    );
                    return true;
                }

                window.SchemafyUtilities.showNotification(
                    nodes.length > 1
                        ? 'All ' + nodes.length + ' schemas are valid and complete.'
                        : 'Valid — every property this type requires is present.',
                    'success'
                );
                return true;

            } catch(e) {
                const errorDetails = this.detectSyntaxErrors(value, e);
                window.SchemafyUtilities.showNotification('Invalid JSON: ' + errorDetails, 'error');
                return false;
            }
        },

        /**
         * Detect and explain JSON syntax errors
         */
        detectSyntaxErrors: function(value, error) {
            const lines = value.split('\n');
            let errorMessage = error.message;

            // Try to extract line number from error
            const lineMatch = error.message.match(/position (\d+)/);
            if (lineMatch) {
                const position = parseInt(lineMatch[1]);
                let charCount = 0;
                let lineNumber = 1;

                for (let i = 0; i < lines.length; i++) {
                    charCount += lines[i].length + 1;
                    if (charCount >= position) {
                        lineNumber = i + 1;
                        break;
                    }
                }

                errorMessage = `Line ${lineNumber}: ${error.message}`;
            }

            // Common error patterns
            if (value.includes("'")) {
                errorMessage += ' (Tip: Use double quotes instead of single quotes)';
            }

            if (/,\s*[}\]]/.test(value)) {
                errorMessage += ' (Tip: Remove trailing commas)';
            }

            return errorMessage;
        },

        /**
         * Validate schema structure
         */
        validateSchemaStructure: function(schemaData) {
            const errors = [];

            try {
                const data = typeof schemaData === 'string' ? JSON.parse(schemaData) : schemaData;
                const nodes = jsonLdNodes(data);

                if (!Array.isArray(data) && !data['@context']) {
                    errors.push('Missing @context');
                }

                if (!nodes.length) {
                    errors.push('No schema found');
                    return errors;
                }

                // Same per-node check the Manual JSON tab runs. This used to be
                // a separate hand-written list that knew only Product, Article
                // and FAQPage, so every other type passed unchecked.
                nodes.forEach(function(node, i) {
                    const label = nodes.length > 1 ? 'Schema ' + (i + 1) : '';
                    Array.prototype.push.apply(errors, nodeIssues(node, label).errors);
                });

            } catch (e) {
                errors.push('Invalid JSON format');
            }

            return errors;
        },

        /**
         * Bulk validate multiple schemas
         */
        bulkValidateSchemas: function(ids) {
            if (!window.SchemafyBulk || !window.SchemafyBulk.allBulkSchemas) {
                window.SchemafyUtilities.showNotification('No schemas available', 'warning');
                return;
            }

            window.SchemafyUtilities.showNotification(`Validating ${ids.length} schema(s)...`, 'info');

            const selectedSchemas = window.SchemafyBulk.allBulkSchemas.filter(schema => ids.includes(parseInt(schema.id)));
            const results = [];

            selectedSchemas.forEach(schema => {
                try {
                    const data = JSON.parse(schema.schema_data);
                    const isValid = data['@context'] && data['@type'];

                    results.push({
                        id: schema.id,
                        name: schema.schema_name,
                        url: schema.url,
                        valid: isValid,
                        errors: isValid ? [] : ['Missing @context or @type']
                    });
                } catch (e) {
                    results.push({
                        id: schema.id,
                        name: schema.schema_name,
                        url: schema.url,
                        valid: false,
                        errors: ['Invalid JSON format']
                    });
                }
            });

            this.showValidationResults(results);
        },

        /**
         * Validate all schemas
         */
        validateAllSchemas: function() {
            if (!window.SchemafyBulk || !window.SchemafyBulk.allBulkSchemas || window.SchemafyBulk.allBulkSchemas.length === 0) {
                window.SchemafyUtilities.showNotification('No schemas to validate', 'warning');
                return;
            }

            const ids = window.SchemafyBulk.allBulkSchemas.map(s => parseInt(s.id));
            this.bulkValidateSchemas(ids);
        },

        /**
         * Show validation results modal
         */
        showValidationResults: function(results) {
            const validCount = results.filter(r => r.valid).length;
            const invalidCount = results.length - validCount;

            // Schema names, URLs and the property names quoted in each finding
            // all come from stored content, so they are escaped before going
            // anywhere near this markup.
            const esc = window.SchemafyUtilities.escapeHtml;

            let resultsHtml = '';
            results.forEach(result => {
                const statusIcon = result.valid
                    ? '<span class="dashicons dashicons-yes-alt" style="color: #0d9488;"></span>'
                    : '<span class="dashicons dashicons-dismiss" style="color: #3730a3;"></span>';

                const errors = result.errors.length > 0
                    ? `<div style="color: #3730a3; font-size: 12px; margin-top: 4px;">${esc(result.errors.join(', '))}</div>`
                    : '';

                resultsHtml += `
                    <div style="padding: 12px; border-bottom: 1px solid var(--schemafy-gray-200); display: flex; gap: 12px;">
                        ${statusIcon}
                        <div style="flex: 1;">
                            <div style="font-weight: 600;">${esc(result.name)}</div>
                            <div style="font-size: 12px; color: var(--schemafy-gray-600);">${esc(result.url)}</div>
                            ${errors}
                        </div>
                    </div>
                `;
            });

            const modalHtml = `
                <div class="schemafy-modal-overlay" id="schemafy-validation-modal">
                    <div class="schemafy-modal-v2" style="max-width: 800px;">
                        <div class="schemafy-modal-header-v2">
                            <h3><span class="dashicons dashicons-yes-alt"></span> Validation Results</h3>
                            <button class="schemafy-modal-close-v2">&times;</button>
                        </div>
                        <div class="schemafy-modal-body-v2">
                            <div style="display: flex; gap: 20px; margin-bottom: 20px;">
                                <div style="flex: 1; padding: 16px; background: #ede9fe; border-radius: 8px; text-align: center;">
                                    <div style="font-size: 32px; font-weight: 700; color: #0d9488;">${validCount}</div>
                                    <div style="font-size: 13px; color: #0d9488; font-weight: 600;">Valid</div>
                                </div>
                                <div style="flex: 1; padding: 16px; background: #f5f3ff; border-radius: 8px; text-align: center;">
                                    <div style="font-size: 32px; font-weight: 700; color: #3730a3;">${invalidCount}</div>
                                    <div style="font-size: 13px; color: #3730a3; font-weight: 600;">Invalid</div>
                                </div>
                            </div>

                            <div style="max-height: 400px; overflow-y: auto; border: 1px solid var(--schemafy-gray-300); border-radius: 8px;">
                                ${resultsHtml}
                            </div>
                        </div>
                        <div class="schemafy-modal-footer-v2">
                            <button class="schemafy-btn-primary" id="schemafy-validation-close">Close</button>
                        </div>
                    </div>
                </div>
            `;

            $('body').append(modalHtml);

            $('#schemafy-validation-modal .schemafy-modal-close-v2, #schemafy-validation-close').on('click', () => {
                $('#schemafy-validation-modal').remove();
            });
        }
    };

    // Exposed so scripts/test-jsonld-nodes.js can check these against the real
    // implementation instead of a copy of it.
    window.SchemafyValidation.jsonLdNodes = jsonLdNodes;
    window.SchemafyValidation.contextMentionsSchemaOrg = contextMentionsSchemaOrg;
    window.SchemafyValidation.nodeIssues = nodeIssues;
    window.SchemafyValidation.valueAtPath = valueAtPath;
    window.SchemafyValidation.isBlank = isBlank;

})(jQuery);
