<?php

declare(strict_types=1);

/**
 * HowTo Schema Service
 *
 * Single source of truth for building and rendering HowTo JSON-LD. Used by
 * the Premium TargetedHowToSchemaModule and any future producers (Gutenberg
 * block render path, Elementor widget) that should emit consistent HowTo
 * schema across the site.
 *
 * Provides a request-level emitted flag so multiple producers on the same
 * page don't duplicate HowTo schema in the rendered HTML.
 *
 * @package ProRank\SEO\Core\Services
 * @since 1.1.4
 */

namespace ProRank\SEO\Core\Services;

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


class HowToSchemaService {

    /**
     * Request-level flag — true once any HowTo schema has been emitted on this request.
     *
     * @var bool
     */
    private static bool $emitted = false;

    /**
     * Normalize a raw step into ['name' => ..., 'text' => ..., 'url' => ...]
     * keeping only entries with a non-empty name. Accepts ['name','text','url']
     * or ['title','description','anchor'] shapes — both appear in the codebase.
     *
     * @param array $steps Raw step entries.
     * @return array Normalized steps (possibly empty).
     */
    public static function normalize_steps( array $steps ): array {
        $out = [];

        foreach ( $steps as $step ) {
            if ( ! is_array( $step ) ) {
                continue;
            }

            $name = trim( (string) ( $step['name'] ?? $step['title'] ?? '' ) );
            $text = trim( (string) ( $step['text'] ?? $step['description'] ?? '' ) );
            $url = trim( (string) ( $step['url'] ?? $step['anchor'] ?? '' ) );

            if ( $name === '' ) {
                continue;
            }

            $entry = [ 'name' => $name ];
            if ( $text !== '' ) {
                $entry['text'] = $text;
            }
            if ( $url !== '' ) {
                $entry['url'] = $url;
            }

            $out[] = $entry;
        }

        return $out;
    }

    /**
     * Build the HowTo JSON-LD array.
     *
     * Schema text fields are stripped of HTML; schema.org HowToStep `text`
     * accepts plain text. Caller is responsible for any input-time
     * sanitization beyond what's already in normalize_steps().
     *
     * @param array $config Config with keys:
     *     - name        (string, required) HowTo headline
     *     - description (string, optional) Short description
     *     - totalTime   (string, optional) ISO 8601 duration like "PT15M"
     *     - image       (string|array, optional) URL or ImageObject array
     *     - steps       (array, required)  Step entries (see normalize_steps)
     *     - mainEntityOfPage (string, optional) URL anchored to the page
     * @return array|null HowTo schema array, or null when invalid.
     */
    public static function build_schema( array $config ): ?array {
        $name = trim( (string) ( $config['name'] ?? '' ) );
        if ( $name === '' ) {
            return null;
        }

        $steps = self::normalize_steps( is_array( $config['steps'] ?? null ) ? $config['steps'] : [] );
        if ( count( $steps ) < 2 ) {
            // HowTo with fewer than two steps is not a procedure — skip rather
            // than emit a schema Google would reject as invalid.
            return null;
        }

        $main_entity = [];
        foreach ( $steps as $step ) {
            $entry = [
                '@type' => 'HowToStep',
                'name' => wp_strip_all_tags( $step['name'] ),
            ];
            if ( ! empty( $step['text'] ) ) {
                $entry['text'] = wp_strip_all_tags( $step['text'] );
            }
            if ( ! empty( $step['url'] ) ) {
                $entry['url'] = esc_url_raw( $step['url'] );
            }
            $main_entity[] = $entry;
        }

        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'HowTo',
            'name' => wp_strip_all_tags( $name ),
            'step' => $main_entity,
        ];

        $description = trim( (string) ( $config['description'] ?? '' ) );
        if ( $description !== '' ) {
            $schema['description'] = wp_strip_all_tags( $description );
        }

        $total_time = trim( (string) ( $config['totalTime'] ?? '' ) );
        if ( $total_time !== '' ) {
            $schema['totalTime'] = $total_time;
        }

        $image = $config['image'] ?? null;
        if ( is_string( $image ) && $image !== '' ) {
            $schema['image'] = [
                '@type' => 'ImageObject',
                'url' => esc_url_raw( $image ),
            ];
        } elseif ( is_array( $image ) && ! empty( $image['url'] ) ) {
            $schema['image'] = [
                '@type' => 'ImageObject',
                'url' => esc_url_raw( (string) $image['url'] ),
            ];
            if ( ! empty( $image['width'] ) ) {
                $schema['image']['width'] = (int) $image['width'];
            }
            if ( ! empty( $image['height'] ) ) {
                $schema['image']['height'] = (int) $image['height'];
            }
        }

        $main_url = trim( (string) ( $config['mainEntityOfPage'] ?? '' ) );
        if ( $main_url !== '' ) {
            $schema['mainEntityOfPage'] = [
                '@type' => 'WebPage',
                '@id' => esc_url_raw( $main_url ),
            ];
        }

        return $schema;
    }

    /**
     * Render a complete <script type="application/ld+json"> tag.
     *
     * Returns empty string when the schema can't be built (missing name,
     * fewer than 2 steps) so callers can safely echo it.
     *
     * @param array $config See build_schema().
     * @param string $class_attr Optional class attribute for the script tag.
     * @return string Script tag HTML, or '' when there's nothing to emit.
     */
    public static function render_script( array $config, string $class_attr = '' ): string {
        $schema = self::build_schema( $config );

        if ( $schema === null ) {
            return '';
        }

        $class = $class_attr !== ''
            ? ' class="' . esc_attr( $class_attr ) . '"'
            : '';

        return '<script type="application/ld+json"' . $class . '>'
            . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
            . '</script>';
    }

    /**
     * Mark HowTo schema as emitted for this request.
     *
     * @return void
     */
    public static function mark_emitted(): void {
        self::$emitted = true;
    }

    /**
     * Whether any HowTo schema has been emitted this request.
     *
     * @return bool
     */
    public static function was_emitted(): bool {
        return self::$emitted;
    }

    /**
     * Reset the emitted flag — exposed for tests, not used in production code.
     *
     * @return void
     */
    public static function reset_for_tests(): void {
        self::$emitted = false;
    }
}
