<?php

/**
 * Schema Output Manager
 *
 * Centralizes all schema output to prevent duplicates and conflicts.
 * 2025 Enhancement: Single consolidated output handler for all schema types.
 *
 * @package ProRank\SEO\Core\Schema
 * @since   2.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Core\Schema;

use ProRank\SEO\Core\Compatibility\CompetitorDetector;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


/**
 * Schema Output Manager class
 *
 * Singleton pattern to manage all schema output across the plugin.
 */


class SchemaOutputManager {

    /**
     * Singleton instance
     *
     * @var SchemaOutputManager|null
     */
    private static ?SchemaOutputManager $instance = null;

    /**
     * Registered schemas by priority
     *
     * @var array Array of arrays: priority => [schemas]
     */
    private array $schemas = [];

    /**
     * Schema type registry (prevents duplicates)
     *
     * @var array Array of @type => schema_data
     */
    private array $type_registry = [];

    /**
     * Whether output has been rendered
     *
     * @var bool
     */
    private bool $rendered = false;

    /**
     * Private constructor (singleton)
     */
    private function __construct() {
        // Hook into wp_head at priority 1 (before all modules)
        add_action('wp_head', [$this, 'output_consolidated_schemas'], 1);
    }

    /**
     * Get singleton instance
     *
     * @return SchemaOutputManager
     */
    public static function instance(): SchemaOutputManager {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Register a schema for output
     *
     * @param array|string $schema   Schema data (array or JSON string)
     * @param int         $priority  Priority (lower = earlier, default 10)
     * @param string      $source    Source module/component (for debugging)
     * @return bool True if registered, false if duplicate
     */
    public function register_schema($schema, int $priority = 10, string $source = 'unknown'): bool {
        // Parse schema if it's a string
        if (is_string($schema)) {
            $schema = json_decode($schema, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                prorank_log('[ProRank SEO] Invalid schema JSON from ' . $source . ': ' . json_last_error_msg());
                return false;
            }
        }

        if (!is_array($schema)) {
            prorank_log('[ProRank SEO] Invalid schema type from ' . $source);
            return false;
        }

        // Check for @type (required)
        $schema_type = $this->get_schema_type($schema);
        if (!$schema_type) {
            prorank_log('[ProRank SEO] Schema missing @type from ' . $source);
            return false;
        }

        // Check for duplicates (same @type)
        if (isset($this->type_registry[$schema_type])) {
            // Allow override from higher priority (lower number)
            $existing_priority = $this->type_registry[$schema_type]['priority'];
            if ($priority >= $existing_priority) {
                // Skip duplicate with same or lower priority
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    prorank_log(sprintf(
                        '[ProRank SEO] Duplicate schema type "%s" from %s (priority %d) - already registered at priority %d',
                        $schema_type,
                        $source,
                        $priority,
                        $existing_priority
                    ));
                }
                return false;
            }

            // Remove existing lower priority schema
            $this->remove_schema_from_priority($schema_type, $existing_priority);
        }

        // Add schema to priority queue
        if (!isset($this->schemas[$priority])) {
            $this->schemas[$priority] = [];
        }

        $this->schemas[$priority][] = [
            'schema' => $schema,
            'source' => $source,
            'type' => $schema_type,
        ];

        // Register in type registry
        $this->type_registry[$schema_type] = [
            'priority' => $priority,
            'source' => $source,
        ];

        return true;
    }

    /**
     * Get schema @type (handles nested types and arrays)
     *
     * @param array $schema Schema data
     * @return string|null Schema type or null
     */
    private function get_schema_type(array $schema): ?string {
        if (isset($schema['@type'])) {
            // Handle single type
            if (is_string($schema['@type'])) {
                return $schema['@type'];
            }

            // Handle array of types (take first)
            if (is_array($schema['@type']) && !empty($schema['@type'])) {
                return is_string($schema['@type'][0]) ? $schema['@type'][0] : null;
            }
        }

        // Handle @graph structures
        if (isset($schema['@graph']) && is_array($schema['@graph']) && !empty($schema['@graph'])) {
            $first = $schema['@graph'][0];
            if (isset($first['@type'])) {
                return is_string($first['@type']) ? $first['@type'] : null;
            }
        }

        return null;
    }

    /**
     * Remove schema from priority queue
     *
     * @param string $schema_type Schema type to remove
     * @param int   $priority     Priority level
     * @return void
     */
    private function remove_schema_from_priority(string $schema_type, int $priority): void {
        if (!isset($this->schemas[$priority])) {
            return;
        }

        foreach ($this->schemas[$priority] as $index => $item) {
            if ($item['type'] === $schema_type) {
                unset($this->schemas[$priority][$index]);
                break;
            }
        }

        // Remove empty priority array
        if (empty($this->schemas[$priority])) {
            unset($this->schemas[$priority]);
        }
    }

    /**
     * Output all registered schemas (consolidated)
     *
     * Called by wp_head hook at priority 1.
     *
     * @return void
     */
    public function output_consolidated_schemas(): void {
        if ($this->rendered) {
            return; // Already rendered
        }

        // In SEO conflict safe mode, skip ProRank schema output to avoid duplicates.
        $detector = new CompetitorDetector();
        if ($detector->should_enable_seo_safe_mode()) {
            $this->rendered = true;
            return;
        }

        // Allow modules to register their schemas
        do_action('prorank_seo_register_schemas', $this);

        // Check if we have any schemas
        if (empty($this->schemas)) {
            $this->rendered = true;
            return;
        }

        // Sort by priority (lower number = higher priority)
        ksort($this->schemas);

        // Collect all schemas
        $all_schemas = [];
        foreach ($this->schemas as $priority => $schemas_at_priority) {
            foreach ($schemas_at_priority as $item) {
                $all_schemas[] = $item['schema'];
            }
        }

        if (empty($all_schemas)) {
            $this->rendered = true;
            return;
        }

        // Output consolidated schema
        $this->output_schema_markup($all_schemas);
        $this->rendered = true;
    }

    /**
     * Output schema markup (single script tag for all schemas)
     *
     * @param array $schemas Array of schema objects
     * @return void
     */
    private function output_schema_markup(array $schemas): void {
        // Decide format based on schema count
        if (count($schemas) === 1) {
            // Single schema - output directly
            $output = wp_json_encode($schemas[0], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        } else {
            // Multiple schemas - use @graph
            $output = wp_json_encode([
                '@context' => 'https://schema.org',
                '@graph' => $schemas,
            ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        }

        if ($output === false) {
            prorank_log('[ProRank SEO] Failed to encode schemas: ' . json_last_error_msg());
            return;
        }

        // Output schema markup
        echo "\n";
        echo '<script type="application/ld+json">' . "\n";
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-LD output
        echo $output . "\n";
        echo '</script>' . "\n";
    }

    /**
     * Get all registered schemas (for debugging)
     *
     * @return array
     */
    public function get_registered_schemas(): array {
        $result = [];
        ksort($this->schemas);

        foreach ($this->schemas as $priority => $schemas_at_priority) {
            foreach ($schemas_at_priority as $item) {
                $result[] = [
                    'priority' => $priority,
                    'type' => $item['type'],
                    'source' => $item['source'],
                    'schema' => $item['schema'],
                ];
            }
        }

        return $result;
    }

    /**
     * Reset schemas (mainly for testing)
     *
     * @return void
     */
    public function reset(): void {
        $this->schemas = [];
        $this->type_registry = [];
        $this->rendered = false;
    }

    /**
     * Prevent cloning (singleton)
     */
    private function __clone() {}

    /**
     * Prevent unserialization (singleton)
     */
    public function __wakeup() {
        throw new \Exception('Cannot unserialize singleton');
    }
}
