<?php

/**
 * LinkCentral Content Filter
 *
 * This file contains functions to process and replace LinkCentral links in post content.
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
// Exit if accessed directly
class LinkCentral_Content_Filter {
    private $link_cache = array();

    /**
     * Initialize and setup the content filter.
     */
    public function init() {
        // Register the Elementor filter unconditionally — it only fires when Elementor renders,
        // so it is harmless when Elementor is inactive. This avoids a load-order race where
        // did_action('elementor/loaded') is still 0 during plugins_loaded at priority 10.
        add_filter( 'elementor/frontend/the_content', array($this, 'process_linkcentral_links'), 999 );
        // Add the WordPress content filter on the wp hook, but skip Elementor pages to
        // avoid double-processing. By the wp hook, all plugins are loaded so
        // class_exists('\Elementor\Plugin') reliably reflects whether Elementor is active.
        add_action( 'wp', function () {
            if ( !$this->is_elementor_page() ) {
                add_filter( 'the_content', array($this, 'process_linkcentral_links'), 999 );
            }
        } );
    }

    /**
     * Extract the LinkCentral link ID from a DOM anchor element.
     *
     * Supports both the current data-attribute format and the legacy #linkcentral-sync-to- href format.
     *
     * @param DOMElement $link The anchor element.
     * @return int|null The link ID, or null if not found.
     */
    private function get_link_id_from_element( $link ) {
        $link_id = $link->getAttribute( 'data-linkcentral-id-sync' );
        if ( $link_id ) {
            return absint( $link_id );
        }
        $href = $link->getAttribute( 'href' );
        if ( preg_match( '/#linkcentral-sync-to-(\\d+)/', $href, $matches ) ) {
            return absint( $matches[1] );
        }
        return null;
    }

    /**
     * Check if the current page is an Elementor page.
     *
     * @return bool Whether the page is an Elementor page.
     */
    private function is_elementor_page() {
        if ( !class_exists( '\\Elementor\\Plugin' ) ) {
            return false;
        }
        $document = \Elementor\Plugin::$instance->documents->get( get_the_ID() );
        return $document && $document->is_built_with_elementor();
    }

    /**
     * Process the content and replace LinkCentral links.
     *
     * @param string $content The post content.
     * @return string The processed content.
     */
    public function process_linkcentral_links( $content ) {
        if ( empty( $content ) || strpos( $content, 'data-linkcentral-id-sync' ) === false && strpos( $content, '#linkcentral-sync-to-' ) === false ) {
            return $content;
        }
        $dom = new DOMDocument();
        $previous_libxml = libxml_use_internal_errors( true );
        // Wrap content in a temporary container to ensure a single root element.
        // This prevents LIBXML_HTML_NOIMPLIED from incorrectly nesting sibling
        // elements when the content has multiple root elements (e.g., <h1> followed by <p>).
        $wrapped_content = '<div id="linkcentral-temp-wrapper">' . $content . '</div>';
        // mb_encode_numericentity + htmlentities round-trip preserves multibyte characters
        // that DOMDocument::loadHTML would otherwise mangle without an explicit charset declaration.
        $dom->loadHTML( mb_encode_numericentity( htmlspecialchars_decode( htmlentities(
            $wrapped_content,
            ENT_NOQUOTES,
            'UTF-8',
            false
        ), ENT_NOQUOTES ), [
            0x80,
            0x10ffff,
            0,
            ~0
        ], 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
        libxml_clear_errors();
        libxml_use_internal_errors( $previous_libxml );
        $xpath = new DOMXPath($dom);
        // Query both old and new format links
        $links = $xpath->query( "//a[@data-linkcentral-id-sync]|//a[starts-with(@href, '#linkcentral-sync-to-')]" );
        if ( $links->length === 0 ) {
            return $content;
        }
        $link_ids = array();
        foreach ( $links as $link ) {
            $link_id = $this->get_link_id_from_element( $link );
            if ( $link_id ) {
                $link_ids[] = $link_id;
            }
        }
        $this->preload_link_data( $link_ids );
        $link_attributes = new LinkCentral_Link_Attributes();
        foreach ( $links as $link ) {
            $link_id = $this->get_link_id_from_element( $link );
            $link_data = ( $link_id ? $this->link_cache[$link_id] ?? null : null );
            if ( $link_data ) {
                $this->update_link_element( $link, $link_data, $link_attributes );
            }
        }
        // Extract content from inside the temporary wrapper to preserve proper HTML structure
        $wrapper = $dom->getElementById( 'linkcentral-temp-wrapper' );
        if ( $wrapper ) {
            $new_content = '';
            foreach ( $wrapper->childNodes as $child ) {
                $new_content .= $dom->saveHTML( $child );
            }
            return ( $new_content ? $new_content : $content );
        }
        // Fallback to documentElement if wrapper not found (shouldn't happen)
        $new_content = $dom->saveHTML( $dom->documentElement );
        return ( $new_content ? $new_content : $content );
    }

    /**
     * Update the link element with the processed LinkCentral data.
     *
     * @param DOMElement $link     The link element to update.
     * @param array      $link_data The processed link data.
     * @param LinkCentral_Link_Attributes $link_attributes Helper for resolving per-link/global attributes.
     */
    private function update_link_element( $link, $link_data, $link_attributes ) {
        $new_href = linkcentral_get_link_url( $link_data['ID'], $link_data['post_name'], $link_data['disable_slug_prefix'] ?? null );
        $link->setAttribute( 'href', esc_url_raw( $new_href ) );
        $css_classes = $link_attributes->get_css_classes_from_data( $link_data );
        $rel_attributes = $link_attributes->get_rel_attributes_from_data( $link_data );
        // Preserve existing classes and append new ones
        $existing_classes = $link->getAttribute( 'class' );
        $existing_classes = preg_replace( '/\\blinkcentral-link\\b\\s*/', '', $existing_classes );
        $all_classes = trim( $existing_classes . ' ' . $css_classes );
        $all_classes = preg_replace( '/\\s+/', ' ', $all_classes );
        if ( !empty( $all_classes ) ) {
            $link->setAttribute( 'class', esc_attr( $all_classes ) );
        } else {
            $link->removeAttribute( 'class' );
        }
        // Set the rel attribute
        if ( !empty( $rel_attributes ) ) {
            $link->setAttribute( 'rel', implode( ' ', $rel_attributes ) );
        } else {
            $link->removeAttribute( 'rel' );
        }
        // Determine target attribute based on insertion-specific choice, existing HTML, per-link setting, and global default
        $per_insertion_newtab = $link->getAttribute( 'data-linkcentral-newtab' );
        // 'yes', 'no', or ''
        $link->removeAttribute( 'data-linkcentral-newtab' );
        if ( $per_insertion_newtab === 'no' ) {
            // 1) Explicit per-insertion "No" wins over everything
            $link->removeAttribute( 'target' );
        } elseif ( $per_insertion_newtab === 'yes' ) {
            // 2) Explicit per-insertion "Yes" wins over everything
            $link->setAttribute( 'target', '_blank' );
        } else {
            // 3) No explicit per-insertion override: respect any existing target from the editor (e.g., Elementor)
            $existing_target = $link->getAttribute( 'target' );
            if ( !empty( $existing_target ) ) {
                $link->setAttribute( 'target', esc_attr( $existing_target ) );
            } else {
                // 4) No existing target: fall back to per-link + global (with scope) logic
                $should_open_new_tab = $link_attributes->get_new_tab_from_data( $link_data );
                if ( $should_open_new_tab ) {
                    $link->setAttribute( 'target', '_blank' );
                } else {
                    $link->removeAttribute( 'target' );
                }
            }
        }
        // Remove the data-linkcentral-id-sync attribute only if it exists
        if ( $link->hasAttribute( 'data-linkcentral-id-sync' ) ) {
            $link->removeAttribute( 'data-linkcentral-id-sync' );
        }
    }

    /**
     * Preload link data for multiple link IDs.
     *
     * @param array $link_ids An array of link IDs to preload data for.
     */
    private function preload_link_data( $link_ids ) {
        $link_ids = array_unique( array_map( 'absint', $link_ids ) );
        $link_ids = array_diff( $link_ids, array_keys( $this->link_cache ) );
        if ( empty( $link_ids ) ) {
            return;
        }
        global $wpdb;
        $results = $wpdb->get_results( $wpdb->prepare( "\n            SELECT p.ID, p.post_name, \n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_destination_url' THEN pm.meta_value END) AS destination_url,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_css_classes_option' THEN pm.meta_value END) AS css_classes_option,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_custom_css_classes' THEN pm.meta_value END) AS custom_css_classes,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_new_tab' THEN pm.meta_value END) AS new_tab,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_nofollow' THEN pm.meta_value END) AS nofollow,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_sponsored' THEN pm.meta_value END) AS sponsored,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_parameter_forwarding' THEN pm.meta_value END) AS parameter_forwarding,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_disable_slug_prefix' THEN pm.meta_value END) AS disable_slug_prefix,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_other_attributes_option' THEN pm.meta_value END) AS other_attributes_option,\n                   MAX(CASE WHEN pm.meta_key = '_linkcentral_custom_other_attributes' THEN pm.meta_value END) AS custom_other_attributes\n            FROM {$wpdb->posts} p\n            LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id\n            WHERE p.ID IN (" . implode( ',', array_fill( 0, count( $link_ids ), '%d' ) ) . ")\n            AND p.post_type = 'linkcentral_link'\n            AND p.post_status = 'publish'\n            GROUP BY p.ID", $link_ids ), ARRAY_A );
        foreach ( $results as $result ) {
            $this->link_cache[$result['ID']] = $result;
        }
    }

}
