<?php
/**
 * Rank Booster Content Fixer
 *
 * Extensible registry/dispatcher for health fixes that edit real page content
 * (as opposed to render-time overrides). Each fix is handled by a registered
 * handler keyed on the Morningscore validator name.
 *
 * Shared pipeline for every content fix:
 *   1. Resolve the reported URL to a WP post.
 *   2. Detect the page builder.
 *   3. Snapshot + edit via the handler (handler owns builder specifics).
 *   4. Record a tracking row so counts / already-fixed / revert all work.
 *
 * Adding a future check = write a handler + register it below. No changes to
 * the processor / delete-handler / UI wiring are needed.
 */

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

class Rank_Booster_Content_Fixer {

    /**
     * Page builders whose heading storage we can edit safely.
     *
     * This is intentionally a positive list. Every content-fix validator must
     * pass it before an AI suggestion is requested, so unsupported builders
     * cannot consume credits.
     */
    const EDITABLE_CONTENT_EDITORS = array( 'classic', 'gutenberg', 'divi', 'wpbakery', 'elementor' );

    /** @var array<string,string> validator => handler class name */
    private static $handlers = array();

    /** @var array<string,string> validator => kind ('content' | 'image') */
    private static $kinds = array();

    private static $booted = false;

    public static function init() {
        if ( self::$booted ) {
            return;
        }
        require_once plugin_dir_path( __FILE__ ) . 'class-rank-booster-heading-io.php';
        require_once plugin_dir_path( __FILE__ ) . 'class-rank-booster-heading-fix.php';
        require_once plugin_dir_path( __FILE__ ) . 'class-rank-booster-image-fix.php';

        self::register_handler( 'long_h1_h2_tag', 'Rank_Booster_Heading_Fix' );
        self::register_handler( 'missing_h1_tag', 'Rank_Booster_Heading_Fix' );
        self::register_handler( 'large_images', 'Rank_Booster_Image_Fix', 'image' );

        self::$booted = true;
    }

    public static function register_handler( $validator, $handler_class, $kind = 'content' ) {
        self::$handlers[ $validator ] = $handler_class;
        self::$kinds[ $validator ]    = $kind;
    }

    public static function is_supported( $validator ) {
        return isset( self::$handlers[ $validator ] );
    }

    /**
     * Cheap, API-free check that a content fix can actually be applied to this
     * page — run BEFORE pulling the (credit-costing) AI fix, and crucially before
     * we ever try to edit content, so we never break unsupported page builders.
     *
     * Images: the file must be a local, writable image.
     * Headings: the page's editor must be on the editable positive list, then
     *           support the specific operation.
     *
     * @return true|WP_Error
     */
    public static function precheck( $validator, $page_url, $parameters = array() ) {
        self::init();

        if ( ! self::is_supported( $validator ) ) {
            return new WP_Error( 'unsupported_validator', 'Validator not supported: ' . $validator );
        }

        // Image fixes: verify a local, writable file (no editor concerns).
        if ( self::$kinds[ $validator ] === 'image' ) {
            $image_url = '';
            if ( is_array( $parameters ) ) {
                $image_url = ! empty( $parameters['uri'] ) ? (string) $parameters['uri']
                    : ( ! empty( $parameters['issue_identifier'] ) ? (string) $parameters['issue_identifier'] : '' );
            }
            return Rank_Booster_Image_Fix::precheck( $image_url );
        }

        // Content fixes: resolve the post and confirm its editor is supported for
        // this operation, so we never edit (or break) a builder we don't handle.
        $post_id = Rank_Booster::resolve_url_to_post( $page_url );
        if ( ! $post_id ) {
            return new WP_Error( 'post_not_found', 'URL does not resolve to a WordPress post.' );
        }

        $editor_type = Rank_Booster::detect_editor( $post_id );
        if ( ! self::is_editor_supported( $editor_type ) ) {
            self::log_unsupported_editor( $page_url, $post_id, $editor_type, $validator );
            return new WP_Error(
                'unsupported_editor',
                sprintf( 'Skipped page because the "%s" page builder is not supported for content fixes.', $editor_type )
            );
        }
        if ( ! Rank_Booster_Heading_Fix::is_editor_supported( $validator, $editor_type ) ) {
            return new WP_Error(
                'unsupported_editor',
                sprintf( 'editor "%s" not supported for this fix', $editor_type )
            );
        }

        return true;
    }

    /**
     * The DB override_type used for tracking a validator's fixes.
     * Image fixes are keyed on the image file (like alt tags), not the post.
     */
    public static function override_type_for( $validator ) {
        return ( isset( self::$kinds[ $validator ] ) && self::$kinds[ $validator ] === 'image' )
            ? 'image_optimize'
            : 'content_fix';
    }

    /**
     * Apply a content fix.
     *
     * @param string $validator   Morningscore validator (e.g. long_h1_h2_tag).
     * @param string $page_url     Reported page URL.
     * @param array  $parameters   Validator parameters from the API.
     * @param string $solution     Chosen replacement text.
     * @param string $mode         'auto' or 'manual' (informational).
     * @return array|WP_Error
     */
    public static function apply( $validator, $page_url, $parameters, $solution, $mode = 'auto' ) {
        self::init();

        if ( ! self::is_supported( $validator ) ) {
            return new WP_Error( 'unsupported_validator', 'Validator not supported for content fixes: ' . $validator );
        }

        // Image fixes act on a file, not a post — separate pipeline.
        if ( self::$kinds[ $validator ] === 'image' ) {
            return self::apply_image( $validator, $page_url, $parameters, $solution );
        }

        $post_id = Rank_Booster::resolve_url_to_post( $page_url );
        if ( ! $post_id ) {
            return new WP_Error( 'post_not_found', 'Could not resolve URL to a valid post: ' . $page_url );
        }

        $editor_type = Rank_Booster::detect_editor( $post_id );
        if ( ! self::is_editor_supported( $editor_type ) ) {
            return new WP_Error(
                'unsupported_editor',
                sprintf( 'Skipped page because the "%s" page builder is not supported for content fixes.', $editor_type )
            );
        }
        $handler     = self::make_handler( $validator );

        $context = array(
            'post_id'     => $post_id,
            'editor_type' => $editor_type,
            'page_url'    => $page_url,
            'parameters'  => $parameters,
            'solution'    => $solution,
            'validator'   => $validator,
            'mode'        => $mode,
        );

        // Preserve the live version in WP revision history before editing.
        wp_save_post_revision( $post_id );

        $result = $handler->apply( $context );
        if ( is_wp_error( $result ) ) {
            return $result;
        }

        // Track the fix so counts, already-fixed detection and revert work.
        // We store the applied heading as the override value (shown in the
        // Fixes view); the pre-edit snapshot for revert lives in post meta.
        //
        // A page can have multiple over-long headings, each a separate issue.
        // For long_h1_h2_tag we key the row on the specific heading (via a
        // synthetic object_id) so fixing one heading doesn't mark the others as
        // fixed. Other content fixes (missing_h1_tag) stay page-level (object_id 0).
        $object_id = 0;
        if ( $validator === 'long_h1_h2_tag' ) {
            $heading   = isset( $parameters['tag'] ) ? (string) $parameters['tag'] : '';
            $object_id = rank_heading_identity( $heading );
        }

        Rank_Processor::update_seo_data(
            $post_id,
            $object_id,
            'content_fix',
            $solution,
            $validator,
            null,
            null,
            null,
            $page_url
        );

        return array(
            'success'  => true,
            'post_id'  => $post_id,
            'post_url' => get_permalink( $post_id ),
        );
    }

    /**
     * Whether this page builder has a content storage path we can edit.
     */
    private static function is_editor_supported( $editor_type ) {
        return in_array( $editor_type, self::EDITABLE_CONTENT_EDITORS, true );
    }

    /**
     * Persist the credit-saving skip reason in the existing gated debug log.
     */
    private static function log_unsupported_editor( $page_url, $post_id, $editor_type, $validator ) {
        if ( class_exists( 'Rank_Processor' ) ) {
            Rank_Processor::add_debug_log(
                sprintf(
                    '[ContentFix] Skipped page before AI request: url=%s post=%d editor=%s validator=%s (unsupported page builder).',
                    $page_url,
                    $post_id,
                    $editor_type,
                    $validator
                ),
                'info'
            );
        }
    }

    /**
     * Apply an image optimization fix. The offending image URL comes from the
     * validator parameters; the optimized URL is the chosen solution.
     */
    private static function apply_image( $validator, $page_url, $parameters, $solution ) {
        $image_url = '';
        if ( is_array( $parameters ) ) {
            if ( ! empty( $parameters['uri'] ) ) {
                $image_url = (string) $parameters['uri'];
            } elseif ( ! empty( $parameters['issue_identifier'] ) ) {
                $image_url = (string) $parameters['issue_identifier'];
            }
        }
        if ( $image_url === '' ) {
            return new WP_Error( 'no_image_url', 'The issue did not include an image URL to optimize.' );
        }

        // The full API content (sizeBefore, sizeAfter, etc.) may be stashed on
        // the parameters by the AJAX handler so we can include API-reported
        // sizes in the error message + dedicated log.
        $api_content = ( is_array( $parameters ) && isset( $parameters['_api_content'] ) && is_array( $parameters['_api_content'] ) )
            ? $parameters['_api_content']
            : null;

        $handler = self::make_handler( $validator );
        $result  = $handler->apply( array(
            'image_url'     => $image_url,
            'optimized_url' => $solution,
            'parameters'    => $parameters,
            'page_url'      => $page_url,
            'validator'     => $validator,
            'api_content'   => $api_content,
        ) );
        if ( is_wp_error( $result ) ) {
            return $result;
        }

        // Track the fix keyed on the image file (mirrors alt tags): image_url is
        // stored so counts + already-fixed detection work per image, and the
        // reported page URL provides context. object_id 0 → synthesized from the
        // image filename by update_seo_data.
        $post_id = Rank_Booster::resolve_url_to_post( $page_url );
        $stored  = Rank_Processor::update_seo_data(
            $post_id,
            0,
            'image_optimize',
            $image_url,
            $validator,
            null,
            null,
            $image_url,
            $page_url
        );

        if ( ! $stored ) {
            Rank_Processor::add_debug_log(
                '[ImageFix] tracking row NOT stored for ' . $image_url
                . ' (post_id=' . (int) $post_id . ', page=' . $page_url . ')'
            );
        } else {
            Rank_Processor::add_debug_log( '[ImageFix] tracking row stored for ' . $image_url );
        }

        return array(
            'success'   => true,
            'image_url' => $image_url,
        );
    }

    /**
     * Revert a previously applied content fix (restores the pre-edit snapshot).
     *
     * @return bool
     */
    public static function revert( $post_id, $validator, $page_url = '', $image_url = '' ) {
        self::init();

        if ( ! self::is_supported( $validator ) ) {
            return false;
        }

        // Image fixes revert by restoring the file backup (keyed on image URL).
        if ( self::$kinds[ $validator ] === 'image' ) {
            $handler = self::make_handler( $validator );
            $result  = $handler->revert( array(
                'image_url' => $image_url,
                'validator' => $validator,
            ) );
            return ! is_wp_error( $result );
        }

        $post_id = (int) $post_id;
        if ( ! $post_id ) {
            return false;
        }

        $editor_type = Rank_Booster::detect_editor( $post_id );
        $handler     = self::make_handler( $validator );

        wp_save_post_revision( $post_id );

        $result = $handler->revert( array(
            'post_id'     => $post_id,
            'editor_type' => $editor_type,
            'page_url'    => $page_url,
            'validator'   => $validator,
        ) );

        return ! is_wp_error( $result );
    }

    private static function make_handler( $validator ) {
        $class = self::$handlers[ $validator ];
        return new $class();
    }
}
