<?php
/**
 * Site Scanner for BoostBot Knowledge Base
 *
 * Scans WordPress site content for indexing.
 *
 * @package BoostBot
 * @since 1.0.0
 */

// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Site_Scanner
 *
 * Discovers and scans site content (posts, pages, products).
 */
class BoostBot_Site_Scanner {

    /**
     * Scan the entire site.
     *
     * @return array Scan results with site_type and content counts.
     */
    public function scan_site(): array {
        BoostBot_Logger::info( 'Starting site scan' );

        $results = array(
            'site_type'         => $this->detect_site_type(),
            'posts'             => $this->scan_posts(),
            'pages'             => $this->scan_pages(),
            'products'          => $this->scan_products(),
            'categories'        => $this->scan_categories(),
            'custom_post_types' => $this->scan_custom_post_types(),
        );

        BoostBot_Logger::info( 'Site scan complete', array(
            'site_type' => $results['site_type'],
            'posts'     => count( $results['posts'] ),
            'pages'     => count( $results['pages'] ),
            'products'  => count( $results['products'] ),
        ) );

        return $results;
    }

    /**
     * Detect the type of site.
     *
     * @return string Site type (woocommerce, blog, corporate, general).
     */
    public function detect_site_type(): string {
        // Check for WooCommerce
        if ( class_exists( 'WooCommerce' ) ) {
            $product_count = wp_count_posts( 'product' );
            if ( isset( $product_count->publish ) && $product_count->publish > 0 ) {
                return 'woocommerce';
            }
        }

        $post_count = wp_count_posts( 'post' );
        $page_count = wp_count_posts( 'page' );

        $posts = isset( $post_count->publish ) ? $post_count->publish : 0;
        $pages = isset( $page_count->publish ) ? $page_count->publish : 0;

        // Blog has significantly more posts than pages
        if ( $posts > $pages * 3 && $posts > 10 ) {
            return 'blog';
        }

        // Corporate/business site has many pages
        if ( $pages > 10 ) {
            return 'corporate';
        }

        return 'general';
    }

    /**
     * Scan all published posts.
     *
     * @param int $limit Maximum posts to return (0 = all).
     * @return array Array of post data.
     */
    public function scan_posts( int $limit = 0 ): array {
        $args = array(
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => $limit > 0 ? $limit : -1,
            'orderby'        => 'date',
            'order'          => 'DESC',
            // Avoid loading full post objects (post_content can be huge).
            'fields'         => 'ids',
            'no_found_rows'  => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        );

        $post_ids = get_posts( $args );
        $results = array();

        foreach ( $post_ids as $post_id ) {
            $post_id = (int) $post_id;
            $results[] = array(
                'id'             => $post_id,
                'title'          => get_the_title( $post_id ),
                'url'            => urldecode( get_permalink( $post_id ) ),
                'date'           => get_post_field( 'post_date', $post_id ),
                'categories'     => wp_get_post_categories( $post_id, array( 'fields' => 'names' ) ),
                'tags'           => wp_get_post_tags( $post_id, array( 'fields' => 'names' ) ),
                // content_length is intentionally omitted/0 to keep scanning memory-safe.
                'content_length' => 0,
                'excerpt'        => get_post_field( 'post_excerpt', $post_id ),
            );
        }

        return $results;
    }

    /**
     * Scan all published pages.
     *
     * @param int $limit Maximum pages to return (0 = all).
     * @return array Array of page data.
     */
    public function scan_pages( int $limit = 0 ): array {
        $args = array(
            'post_type'      => 'page',
            'post_status'    => 'publish',
            'posts_per_page' => $limit > 0 ? $limit : -1,
            'orderby'        => 'menu_order',
            'order'          => 'ASC',
            // Avoid loading full post objects (post_content can be huge).
            'fields'         => 'ids',
            'no_found_rows'  => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        );

        $page_ids = get_posts( $args );
        $results = array();

        foreach ( $page_ids as $page_id ) {
            $page_id = (int) $page_id;
            $results[] = array(
                'id'             => $page_id,
                'title'          => get_the_title( $page_id ),
                'url'            => urldecode( get_permalink( $page_id ) ),
                'parent_id'      => (int) get_post_field( 'post_parent', $page_id ),
                'template'       => get_page_template_slug( $page_id ),
                // content_length is intentionally omitted/0 to keep scanning memory-safe.
                'content_length' => 0,
                'is_front_page'  => intval( get_option( 'page_on_front' ) ) === $page_id,
            );
        }

        return $results;
    }

    /**
     * Scan WooCommerce products.
     *
     * @param int $limit Maximum products to return (0 = all).
     * @return array Array of product data.
     */
    public function scan_products( int $limit = 0 ): array {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return array();
        }

        $args = array(
            'status' => 'publish',
            'limit'  => $limit > 0 ? $limit : -1,
            'return' => 'ids',
        );

        $product_ids = wc_get_products( $args );
        $results     = array();

        foreach ( $product_ids as $product_id ) {
            $product = wc_get_product( $product_id );
            if ( ! $product ) {
                continue;
            }

            $results[] = array(
                'id'           => $product_id,
                'title'        => $product->get_name(),
                'url'          => urldecode( $product->get_permalink() ),
                'price'        => $product->get_price(),
                'regular_price'=> $product->get_regular_price(),
                'sale_price'   => $product->get_sale_price(),
                'categories'   => wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'names' ) ),
                'sku'          => $product->get_sku(),
                'stock_status' => $product->get_stock_status(),
                'type'         => $product->get_type(),
                'short_desc'   => $product->get_short_description(),
            );
        }

        return $results;
    }

    /**
     * Week 30: Get content sizes for resilient indexing (SQL-only, memory-safe).
     *
     * Returns size info for published content without loading full post objects.
     *
     * Shape:
     * [
     *   ['id'=>123,'type'=>'page','content_size'=>12345,'elementor_size'=>0,'recommended_level'=>4],
     *   ...
     * ]
     *
     * @since 1.13.0
     * @param array $content_types Allowed: post|page|product
     * @return array<int,array<string,int|string>>
     */
    public function get_content_sizes( array $content_types = array( 'post', 'page', 'product' ) ): array {
        global $wpdb;

        $allowed = array( 'post', 'page', 'product' );
        $types   = array_values( array_intersect( $allowed, array_map( 'strval', $content_types ) ) );
        // If the requested types don't intersect the allowed set, index NOTHING rather than
        // silently expanding to the whole site (which would spend BoostCoin the admin never
        // requested — e.g. an unsupported 'custom' type reaching this method).
        if ( empty( $types ) ) {
            return array();
        }

        // Build placeholders for IN clause.
        $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );

        // SQL-only sizes:
        // - content_size: LENGTH(post_content)
        // - elementor_size: LENGTH(_elementor_data meta) (0 when not present)
        //
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $sql = "
            SELECT
                p.ID as id,
                p.post_type as type,
                LENGTH(p.post_content) as content_size,
                COALESCE((
                    SELECT LENGTH(pm.meta_value)
                    FROM {$wpdb->postmeta} pm
                    WHERE pm.post_id = p.ID AND pm.meta_key = '_elementor_data'
                    LIMIT 1
                ), 0) as elementor_size
            FROM {$wpdb->posts} p
            WHERE p.post_status = 'publish'
              AND p.post_type IN ({$placeholders})
            ORDER BY p.ID ASC
        ";

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $rows = $wpdb->get_results( $wpdb->prepare( $sql, $types ), ARRAY_A );
        if ( ! is_array( $rows ) ) {
            return array();
        }

        $out = array();
        foreach ( $rows as $r ) {
            $id            = (int) ( $r['id'] ?? 0 );
            $type          = (string) ( $r['type'] ?? '' );
            $content_size  = (int) ( $r['content_size'] ?? 0 );
            $elementor_size= (int) ( $r['elementor_size'] ?? 0 );

            if ( ! $id || '' === $type ) {
                continue;
            }

            $out[] = array(
                'id'                => $id,
                'type'              => $type,
                'content_size'       => max( 0, $content_size ),
                'elementor_size'     => max( 0, $elementor_size ),
                'recommended_level'  => $this->recommend_extraction_level( $type, $content_size, $elementor_size ),
            );
        }

        return $out;
    }

    /**
     * Recommend an extraction level (1..5) based on content size signals.
     *
     * @since 1.13.0
     * @param string $type
     * @param int    $content_size
     * @param int    $elementor_size
     * @return int
     */
    private function recommend_extraction_level( string $type, int $content_size, int $elementor_size ): int {
        // Conservative defaults: prefer safer extraction on very large content.
        // These thresholds can be tuned later via settings/filters.
        $size = max( $content_size, $elementor_size );

        // Huge builder data tends to OOM with full rendering.
        if ( $elementor_size > 1000000 ) { // 1MB+
            return 2;
        }
        if ( $elementor_size > 300000 ) { // ~300KB+
            return 3;
        }

        if ( $size > 2000000 ) { // 2MB+
            return 1;
        }
        if ( $size > 1000000 ) { // 1MB+
            return 2;
        }
        if ( $size > 300000 ) { // 300KB+
            return 3;
        }
        if ( $size > 120000 ) { // 120KB+
            return 4;
        }

        return 5;
    }

    /**
     * Scan categories.
     *
     * @return array Array of category data.
     */
    public function scan_categories(): array {
        $results = array();

        // Post categories
        $post_cats = get_categories( array(
            'hide_empty' => false,
        ) );

        foreach ( $post_cats as $cat ) {
            $results[] = array(
                'id'          => $cat->term_id,
                'name'        => $cat->name,
                'slug'        => $cat->slug,
                'description' => $cat->description,
                'count'       => $cat->count,
                'type'        => 'category',
                'url'         => get_category_link( $cat->term_id ),
            );
        }

        // Product categories (if WooCommerce)
        if ( class_exists( 'WooCommerce' ) ) {
            $product_cats = get_terms( array(
                'taxonomy'   => 'product_cat',
                'hide_empty' => false,
            ) );

            if ( ! is_wp_error( $product_cats ) ) {
                foreach ( $product_cats as $cat ) {
                    $results[] = array(
                        'id'          => $cat->term_id,
                        'name'        => $cat->name,
                        'slug'        => $cat->slug,
                        'description' => $cat->description,
                        'count'       => $cat->count,
                        'type'        => 'product_cat',
                        'url'         => get_term_link( $cat ),
                    );
                }
            }
        }

        return $results;
    }

    /**
     * Scan custom post types.
     *
     * @return array Array of CPT data.
     */
    public function scan_custom_post_types(): array {
        $results = array();

        // Get all public custom post types (exclude built-in)
        $post_types = get_post_types( array(
            'public'   => true,
            '_builtin' => false,
        ), 'objects' );

        // Exclude WooCommerce types
        $exclude = array( 'product', 'shop_order', 'shop_coupon' );

        foreach ( $post_types as $post_type ) {
            if ( in_array( $post_type->name, $exclude, true ) ) {
                continue;
            }

            $count = wp_count_posts( $post_type->name );

            $results[] = array(
                'name'       => $post_type->name,
                'label'      => $post_type->label,
                'count'      => isset( $count->publish ) ? $count->publish : 0,
                'has_archive'=> $post_type->has_archive,
            );
        }

        return $results;
    }

    /**
     * Get content for indexing.
     *
     * @param int    $content_id   Content ID.
     * @param string $content_type Content type (post, page, product).
     * @return array|null Content data for indexing or null if not found.
     */
    public function get_content_for_indexing( int $content_id, string $content_type ): ?array {
        switch ( $content_type ) {
            case 'post':
            case 'page':
                $post = get_post( $content_id );
                if ( ! $post || $post->post_status !== 'publish' ) {
                    return null;
                }

                $content = $this->clean_content( $post->post_content );

                return array(
                    'id'         => $content_id,
                    'type'       => $content_type,
                    'title'      => $post->post_title,
                    'content'    => $content,
                    'excerpt'    => $post->post_excerpt ?: wp_trim_words( $content, 50 ),
                    'url'        => urldecode( get_permalink( $content_id ) ),
                    'categories' => $content_type === 'post'
                        ? wp_get_post_categories( $content_id, array( 'fields' => 'names' ) )
                        : array(),
                    'date'       => $post->post_date,
                    'author'     => get_the_author_meta( 'display_name', $post->post_author ),
                );

            case 'product':
                if ( ! class_exists( 'WooCommerce' ) ) {
                    return null;
                }

                $product = wc_get_product( $content_id );
                if ( ! $product ) {
                    return null;
                }

                return array(
                    'id'          => $content_id,
                    'type'        => 'product',
                    'title'       => $product->get_name(),
                    'content'     => $this->clean_content( $product->get_description() ),
                    'excerpt'     => $product->get_short_description(),
                    'url'         => urldecode( $product->get_permalink() ),
                    'price'       => $product->get_price(),
                    'categories'  => wp_get_post_terms( $content_id, 'product_cat', array( 'fields' => 'names' ) ),
                    'sku'         => $product->get_sku(),
                    'stock'       => $product->get_stock_status(),
                    'attributes'  => $this->get_product_attributes( $product ),
                );

            default:
                return null;
        }
    }

    /**
     * Clean content for processing.
     *
     * Removes shortcodes, HTML, and cleans whitespace.
     *
     * @param string $content Raw content.
     * @return string Cleaned content.
     */
    public function clean_content( string $content ): string {
        // Remove shortcodes
        $content = strip_shortcodes( $content );

        // Remove HTML tags
        $content = wp_strip_all_tags( $content );

        // Decode HTML entities
        $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );

        // Clean excessive whitespace
        $content = preg_replace( '/\s+/', ' ', $content );

        return trim( $content );
    }

    /**
     * Get product attributes as array.
     *
     * @param WC_Product $product The product.
     * @return array Attributes array.
     */
    private function get_product_attributes( $product ): array {
        $attributes = array();
        $raw_attrs  = $product->get_attributes();

        foreach ( $raw_attrs as $attr ) {
            if ( is_a( $attr, 'WC_Product_Attribute' ) ) {
                $name    = wc_attribute_label( $attr->get_name() );
                $options = $attr->get_options();

                if ( $attr->is_taxonomy() ) {
                    $terms = array();
                    foreach ( $options as $term_id ) {
                        $term = get_term( $term_id );
                        if ( $term && ! is_wp_error( $term ) ) {
                            $terms[] = $term->name;
                        }
                    }
                    $options = $terms;
                }

                $attributes[ $name ] = implode( ', ', $options );
            }
        }

        return $attributes;
    }

    /**
     * Get counts for preview.
     *
     * @return array Content counts.
     */
    public function get_content_counts(): array {
        $post_count    = wp_count_posts( 'post' );
        $page_count    = wp_count_posts( 'page' );
        $product_count = class_exists( 'WooCommerce' ) ? wp_count_posts( 'product' ) : null;

        return array(
            'posts'    => isset( $post_count->publish ) ? (int) $post_count->publish : 0,
            'pages'    => isset( $page_count->publish ) ? (int) $page_count->publish : 0,
            'products' => $product_count && isset( $product_count->publish ) ? (int) $product_count->publish : 0,
        );
    }

    /**
     * Estimate indexing time and cost.
     *
     * @param array $counts Content counts.
     * @return array Estimates.
     */
    public function estimate_indexing( array $counts ): array {
        $total_items = ( $counts['posts'] ?? 0 ) + ( $counts['pages'] ?? 0 ) + ( $counts['products'] ?? 0 );

        // Estimate ~10 items per minute (conservative)
        $estimated_minutes = ceil( $total_items / 10 );

        // Rough cost estimate: ~$0.001 per item for summarization
        $estimated_cost = $total_items * 0.001;

        return array(
            'total_items'       => $total_items,
            'estimated_minutes' => $estimated_minutes,
            'estimated_cost'    => round( $estimated_cost, 2 ),
        );
    }

    /**
     * Restrict a SQL identifier to [A-Za-z0-9_] characters.
     *
     * DDL cannot use $wpdb->prepare() for identifiers, so any identifier
     * that ends up interpolated into a query first passes through this
     * allow-list.
     *
     * @param string $identifier Raw identifier.
     * @return string Identifier with disallowed characters removed.
     */
    private static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}








