<?php
/**
 * AJAX Handlers for Schema Save and Update
 *
 * @package CustomSchemaSchemafy
 * @since 1.1.85
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Read a POST field as a string, or '' when the request omits it.
 *
 * Every handler in this file verifies the nonce and the capability before
 * calling this. What it adds is that a request missing a field fails the
 * "Missing required fields" check below instead of first emitting an
 * "Undefined array key" warning on PHP 8.
 *
 * Sanitizing is left to the caller because it depends on the field:
 * sanitize_text_field() for names and types, esc_url_raw() for URLs, and for
 * schema_data a json_decode() plus schemafy_sanitize_json_recursive() — running
 * sanitize_text_field() over raw JSON-LD would corrupt it.
 *
 * @param string $key POST key.
 * @return string
 */
function schemafy_post_string($key) {
    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by every calling handler before this runs
    if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
        return '';
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified by caller; value sanitized at each call site with the field-appropriate function
    return (string) wp_unslash($_POST[$key]);
}

/**
 * Read a POST field as a row ID: a positive integer, or 0 for anything else.
 *
 * Deliberately rejects rather than coerces. absint() would turn a posted "-5"
 * into 5 and act on a real row the caller never named; the callers only test
 * for 0, so an out-of-range value has to arrive as 0 to be refused.
 *
 * @param string $key POST key.
 * @return int Positive row ID, or 0 when absent or not a positive integer.
 */
function schemafy_post_int($key) {
    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by every calling handler before this runs
    if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
        return 0;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by caller; cast to int below
    $value = (int) $_POST[$key];

    return $value > 0 ? $value : 0;
}

/**
 * Check whether a JSON-LD node has a specific @type.
 *
 * @param object $node JSON-LD node.
 * @return bool
 */
function schemafy_schema_node_has_specific_type($node) {
    if (!is_object($node) || !isset($node->{'@type'})) {
        return false;
    }

    $generic_types = array('Thing', 'CreativeWork', 'WebPage', 'WebSite', 'LocalBusiness', 'Organization');
    $raw_type = $node->{'@type'};
    $types = is_array($raw_type) ? $raw_type : array($raw_type);

    foreach ($types as $type) {
        if (!is_scalar($type)) {
            continue;
        }

        $type = trim((string) $type);
        $type = preg_replace('/^https?:\/\/schema\.org\//i', '', $type);

        if ($type !== '' && !in_array($type, $generic_types, true)) {
            return true;
        }
    }

    return false;
}

/**
 * Find the best JSON-LD node to inspect for @type.
 *
 * @param mixed $decoded Decoded JSON data
 * @return object|null
 */
function schemafy_find_decoded_schema_node($decoded) {
    if (is_object($decoded) && isset($decoded->{'@type'})) {
        return $decoded;
    }

    $nodes = array();

    if (is_object($decoded) && isset($decoded->{'@graph'}) && is_array($decoded->{'@graph'})) {
        $nodes = $decoded->{'@graph'};
    } elseif (is_array($decoded)) {
        $nodes = $decoded;
    }

    $first_typed_node = null;

    foreach ($nodes as $node) {
        if (is_object($node) && isset($node->{'@type'})) {
            if ($first_typed_node === null) {
                $first_typed_node = $node;
            }

            if (schemafy_schema_node_has_specific_type($node)) {
                return $node;
            }
        }
    }

    if ($first_typed_node !== null) {
        return $first_typed_node;
    }

    return is_object($decoded) ? $decoded : null;
}

/**
 * Recursively sanitize a decoded JSON structure before it is stored.
 *
 * Walks arrays/objects and runs every string leaf through sanitize_text_field(),
 * sanitizes array keys too, and passes numbers/booleans/null through unchanged.
 * Use this on any json_decode() result that originated from request input so the
 * payload is cleaned — json_decode() itself performs NO sanitization.
 *
 * @param mixed $data Decoded JSON value (array, scalar, or null).
 * @return mixed Sanitized value of the same shape.
 */
function schemafy_sanitize_json_recursive($data) {
    if (is_array($data)) {
        $clean = array();
        foreach ($data as $key => $value) {
            $clean_key = is_string($key) ? sanitize_text_field($key) : $key;
            $clean[$clean_key] = schemafy_sanitize_json_recursive($value);
        }
        return $clean;
    }

    if (is_string($data)) {
        return sanitize_text_field($data);
    }

    // Integers, floats, booleans and null are inherently safe.
    return $data;
}

/**
 * Extract a storage-safe schema_type from decoded JSON-LD.
 *
 * Schema.org allows @type to be an array. The database column expects a string,
 * so prefer the most specific non-generic type while preserving the original
 * JSON-LD untouched in schema_data.
 *
 * @param object $decoded Decoded JSON object
 * @param string $fallback Existing/supplied schema type
 * @return string
 */
function schemafy_extract_schema_type_from_decoded($decoded, $fallback = '') {
    $decoded = schemafy_find_decoded_schema_node($decoded);

    if (!is_object($decoded) || !isset($decoded->{'@type'})) {
        return sanitize_text_field($fallback);
    }

    $raw_type = $decoded->{'@type'};
    $types = is_array($raw_type) ? $raw_type : array($raw_type);
    $generic_types = array('Thing', 'CreativeWork', 'WebPage', 'WebSite', 'LocalBusiness', 'Organization');
    $selected = '';

    foreach ($types as $type) {
        if (!is_scalar($type)) {
            continue;
        }
        $type = trim((string) $type);
        $type = preg_replace('/^https?:\/\/schema\.org\//i', '', $type);
        if ($type === '') {
            continue;
        }
        $selected = $type;
        if (!in_array($type, $generic_types, true)) {
            break;
        }
    }

    return sanitize_text_field($selected ?: $fallback);
}

/**
 * AJAX Handler: Save new schema
 */
function schemafy_ajax_save_schema() {
    // Check permissions
    if (!current_user_can('manage_options')) {
        wp_send_json_error(__('Unauthorized', 'schemafy'));
    }

    // Verify nonce
    if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
        wp_send_json_error(__('Invalid nonce', 'schemafy'));
    }

    // Sanitize and validate input
    $schema_name = sanitize_text_field(schemafy_post_string('schema_name'));
    $url = esc_url_raw(schemafy_post_string('url'));

    // Canonize URL to HTTPS (unless localhost/dev environment)
    if (!preg_match('/^https?:\/\/(localhost|127\.0\.0\.1|.*\.local|.*\.test)/i', $url)) {
        $url = preg_replace('/^http:/', 'https:', $url);
    }

    $schema_type = sanitize_text_field(schemafy_post_string('schema_type'));
    $schema_data = schemafy_post_string('schema_data');
    $meta_title = sanitize_text_field(schemafy_post_string('meta_title'));
    $meta_description = sanitize_text_field(schemafy_post_string('meta_description'));

    // Basic validation
    if (empty($schema_name) || empty($url) || empty($schema_data)) {
        wp_send_json_error(__('Missing required fields', 'schemafy'));
    }

    // JSON validation and sanitization
    $decoded = json_decode($schema_data, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        wp_send_json_error(__('Invalid JSON format', 'schemafy'));
    }
    $decoded = schemafy_sanitize_json_recursive($decoded);
    $schema_data = wp_json_encode($decoded);

    // Extract @type from the sanitized JSON (object form; supports @type arrays)
    $schema_type = schemafy_extract_schema_type_from_decoded(json_decode($schema_data), $schema_type);

    // Save to database
    global $schemafy_core_instance;
    $db = $schemafy_core_instance->get_database();

    $data = array(
        'schema_name' => $schema_name,
        'url' => $url,
        'schema_type' => $schema_type,
        'schema_data' => $schema_data,
        'meta_title' => $meta_title,
        'meta_description' => $meta_description,
        'is_active' => 1,
        'created_at' => current_time('mysql'),
        'created_by' => get_current_user_id(),
        'updated_at' => current_time('mysql')
    );

    $result = $db->create_schema($data);

    if ($result) {
        if (class_exists('SCHEMAFY_Schema_Injector')) {
            SCHEMAFY_Schema_Injector::clear_all_schema_cache();
        }
        wp_send_json_success(array('id' => $result, 'message' => __('Schema saved successfully', 'schemafy')));
    } else {
        wp_send_json_error(__('Database error: Could not save schema', 'schemafy'));
    }
}
add_action('wp_ajax_schemafy_save_schema', 'schemafy_ajax_save_schema');

/**
 * AJAX Handler: Save schema to multiple URLs at once
 * Creates one independent schema record per URL
 */
function schemafy_ajax_save_schema_bulk() {
    // Check permissions
    if (!current_user_can('manage_options')) {
        wp_send_json_error(__('Unauthorized', 'schemafy'));
    }

    // Verify nonce
    if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
        wp_send_json_error(__('Invalid nonce', 'schemafy'));
    }

    // Get URLs array
    $urls = ( isset($_POST['urls']) && is_array($_POST['urls']) ) ? array_map('sanitize_text_field', wp_unslash($_POST['urls'])) : array();
    if (!is_array($urls) || empty($urls)) {
        wp_send_json_error(__('No URLs provided', 'schemafy'));
    }

    // Limit to 50 URLs per batch
    if (count($urls) > 50) {
        wp_send_json_error(__('Maximum 50 URLs per batch', 'schemafy'));
    }

    // Sanitize common fields
    $schema_name = sanitize_text_field(schemafy_post_string('schema_name'));
    $schema_type = sanitize_text_field(schemafy_post_string('schema_type'));
    $schema_data = schemafy_post_string('schema_data');
    $meta_title = sanitize_text_field(schemafy_post_string('meta_title'));
    $meta_description = sanitize_text_field(schemafy_post_string('meta_description'));

    if (empty($schema_name) || empty($schema_data)) {
        wp_send_json_error(__('Missing required fields', 'schemafy'));
    }

    // JSON validation and sanitization (once)
    $decoded = json_decode($schema_data, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        wp_send_json_error(__('Invalid JSON format', 'schemafy'));
    }
    $decoded = schemafy_sanitize_json_recursive($decoded);
    $schema_data = wp_json_encode($decoded);

    // Extract @type from the sanitized JSON (object form; supports @type arrays)
    $schema_type = schemafy_extract_schema_type_from_decoded(json_decode($schema_data), $schema_type);

    global $schemafy_core_instance;
    $db = $schemafy_core_instance->get_database();

    $created_ids = array();
    $failed_urls = array();
    $now = current_time('mysql');
    $user_id = get_current_user_id();

    foreach ($urls as $raw_url) {
        $url = esc_url_raw(sanitize_text_field($raw_url));

        if (empty($url)) {
            continue;
        }

        // Canonize URL to HTTPS (unless localhost/dev)
        if (!preg_match('/^https?:\/\/(localhost|127\.0\.0\.1|.*\.local|.*\.test)/i', $url)) {
            $url = preg_replace('/^http:/', 'https:', $url);
        }

        $data = array(
            'schema_name' => $schema_name,
            'url' => $url,
            'schema_type' => $schema_type,
            'schema_data' => $schema_data,
            'meta_title' => $meta_title,
            'meta_description' => $meta_description,
            'is_active' => 1,
            'created_at' => $now,
            'created_by' => $user_id,
            'updated_at' => $now
        );

        $result = $db->create_schema($data);

        if ($result) {
            $created_ids[] = $result;
        } else {
            $failed_urls[] = $url;
        }
    }

    if (!empty($created_ids)) {
        if (class_exists('SCHEMAFY_Schema_Injector')) {
            SCHEMAFY_Schema_Injector::clear_all_schema_cache();
        }
        $count = count($created_ids);
        wp_send_json_success(array(
            'ids' => $created_ids,
            'count' => $count,
            'failed' => $failed_urls,
            'message' => sprintf(
                // translators: %d is a value inserted at runtime.
                _n('Schema saved to %d URL successfully', 'Schema saved to %d URLs successfully', $count, 'schemafy'),
                $count
            )
        ));
    } else {
        wp_send_json_error(__('Failed to save schema to any URL', 'schemafy'));
    }
}
add_action('wp_ajax_schemafy_save_schema_bulk', 'schemafy_ajax_save_schema_bulk');

/**
 * AJAX Handler: Update existing schema
 */
function schemafy_ajax_update_schema() {
    // Check permissions
    if (!current_user_can('manage_options')) {
        wp_send_json_error(__('Unauthorized', 'schemafy'));
    }

    // Verify nonce
    if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
        wp_send_json_error(__('Invalid nonce', 'schemafy'));
    }

    $id = schemafy_post_int('id');
    if (!$id) {
        wp_send_json_error(__('Invalid ID', 'schemafy'));
    }

    // Sanitize
    $schema_name = sanitize_text_field(schemafy_post_string('schema_name'));
    $url = esc_url_raw(schemafy_post_string('url'));

    // Canonize URL to HTTPS (unless localhost/dev environment)
    if (!preg_match('/^https?:\/\/(localhost|127\.0\.0\.1|.*\.local|.*\.test)/i', $url)) {
        $url = preg_replace('/^http:/', 'https:', $url);
    }

    $schema_type = sanitize_text_field(schemafy_post_string('schema_type'));
    $schema_data = schemafy_post_string('schema_data');
    $meta_title = sanitize_text_field(schemafy_post_string('meta_title'));
    $meta_description = sanitize_text_field(schemafy_post_string('meta_description'));

    // JSON validation and sanitization
    if (!empty($schema_data)) {
        $decoded = json_decode($schema_data, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            wp_send_json_error(__('Invalid JSON format', 'schemafy'));
        }
        $decoded = schemafy_sanitize_json_recursive($decoded);
        $schema_data = wp_json_encode($decoded);

        // Extract @type from the sanitized JSON (object form; supports @type arrays)
        $schema_type = schemafy_extract_schema_type_from_decoded(json_decode($schema_data), $schema_type);
    }

    // Update database
    global $schemafy_core_instance;
    $db = $schemafy_core_instance->get_database();

    $data = array(
        'schema_name' => $schema_name,
        'url' => $url,
        'schema_type' => $schema_type,
        'schema_data' => $schema_data,
        'meta_title' => $meta_title,
        'meta_description' => $meta_description,
        'updated_at' => current_time('mysql')
    );

    $result = $db->update_schema($id, $data);

    if ($result !== false) {
        if (class_exists('SCHEMAFY_Schema_Injector')) {
            SCHEMAFY_Schema_Injector::clear_all_schema_cache();
        }
        wp_send_json_success(array('message' => __('Schema updated successfully', 'schemafy')));
    } else {
        wp_send_json_error(__('Database error: Could not update schema', 'schemafy'));
    }
}
add_action('wp_ajax_schemafy_update_schema', 'schemafy_ajax_update_schema');

/**
 * AJAX Handler: Get schema by ID for editing
 */
function schemafy_ajax_get_schema() {
    // Check permissions
    if (!current_user_can('manage_options')) {
        wp_send_json_error(__('Unauthorized', 'schemafy'));
    }

    // Verify nonce
    if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'schemafy_nonce')) {
        wp_send_json_error(__('Invalid nonce', 'schemafy'));
    }

    $id = schemafy_post_int('id');
    if (!$id) {
        wp_send_json_error(__('Invalid schema ID', 'schemafy'));
    }

    global $schemafy_core_instance;
    $db = $schemafy_core_instance->get_database();

    $schema = $db->get_schema($id);

    if ($schema) {
        wp_send_json_success($schema);
    } else {
        wp_send_json_error(__('Schema not found', 'schemafy'));
    }
}
add_action('wp_ajax_schemafy_get_schema', 'schemafy_ajax_get_schema');
