<?php
/**
 * DBA_SEO_Sniper_Crawler — Serper.dev SERP API + competitor page analysis
 *
 * Phase 1: Fetches Google top 20 results via Serper.dev API (1 API call = 20 results).
 * Phase 2: Fetches each competitor page and extracts structural SEO data.
 *
 * API budget:
 *   - Free tier: 2,500 calls
 *   - Weekly rank check: 1 call per keyword
 *   - Manual analyze: 1 call per keyword
 *   - 10 keywords × 52 weeks = 520 calls/year
 *
 * @since 2.16.0
 */
if ( ! defined( 'ABSPATH' ) ) exit;

class DBA_SEO_Sniper_Crawler {

    const SERPER_ENDPOINT = 'https://google.serper.dev/search';
    const USAGE_KEY       = 'dba_sniper_serper_usage';  // { total, month, monthly[] }
    const API_KEY_OPTION  = 'dba_sniper_serper_key';

    /**
     * Get or set the Serper API key.
     */
    public static function get_api_key() {
        $key = get_option( self::API_KEY_OPTION, '' );
        // Fallback to hardcoded key if not set in DB
        if ( empty( $key ) ) {
            $key = 'cb782cca137211b655eeec3adaf438cb3140ff4a';
        }
        return $key;
    }

    public static function set_api_key( $key ) {
        update_option( self::API_KEY_OPTION, sanitize_text_field( $key ), false );
    }

    /**
     * Track API usage.
     */
    private static function track_usage() {
        $usage = get_option( self::USAGE_KEY, array(
            'total'   => 0,
            'monthly' => array(),
        ) );

        $usage['total']++;
        $month = date( 'Y-m' );
        if ( ! isset( $usage['monthly'][ $month ] ) ) $usage['monthly'][ $month ] = 0;
        $usage['monthly'][ $month ]++;

        // Keep only last 12 months
        $usage['monthly'] = array_slice( $usage['monthly'], -12, 12, true );

        update_option( self::USAGE_KEY, $usage, false );

        return $usage['total'];
    }

    /**
     * Get usage stats.
     */
    public static function get_usage() {
        return get_option( self::USAGE_KEY, array(
            'total'   => 0,
            'monthly' => array(),
        ) );
    }

    /**
     * Crawl Google top 20 results for a keyword via Serper.dev.
     *
     * 1 API call = up to 20 results. No scraping, no CAPTCHA, no consent page.
     *
     * @param string $keyword   The target keyword.
     * @param string $language  ISO language code.
     * @return array|WP_Error   Array of SERP results or error.
     */
    public static function crawl_serp( $keyword, $language = 'en' ) {
        $api_key = self::get_api_key();
        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', 'Serper API key not configured. Go to SEO Sniper settings.' );
        }

        $gl = self::lang_to_country( $language );

        $body = array(
            'q'   => $keyword,
            'gl'  => $gl,
            'hl'  => $language,
            'num' => 20,
        );

        $response = wp_remote_post( self::SERPER_ENDPOINT, array(
            'timeout' => 20,
            'headers' => array(
                'X-API-KEY'    => $api_key,
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode( $body ),
        ) );

        if ( is_wp_error( $response ) ) {
            error_log( 'DBA Sniper: Serper API request failed — ' . $response->get_error_message() );
            return new WP_Error( 'serper_error', 'Serper API request failed: ' . $response->get_error_message() );
        }

        $code = wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code === 401 || $code === 403 ) {
            return new WP_Error( 'auth_error', 'Serper API key invalid or expired. Check your key.' );
        }

        if ( $code === 429 ) {
            return new WP_Error( 'rate_limited', 'Serper API rate limited. You may have hit the free tier limit (2,500 calls).' );
        }

        if ( $code !== 200 ) {
            $msg = $data['message'] ?? 'HTTP ' . $code;
            error_log( 'DBA Sniper: Serper API error — ' . $msg );
            return new WP_Error( 'serper_http_error', 'Serper API: ' . $msg );
        }

        // Track this call
        $total = self::track_usage();
        error_log( 'DBA Sniper: Serper API call #' . $total . ' for "' . $keyword . '" (' . $language . ')' );

        // Parse organic results
        $organic = $data['organic'] ?? array();

        if ( empty( $organic ) ) {
            return new WP_Error( 'no_results', 'No results found for "' . $keyword . '".' );
        }

        $results = array();
        foreach ( $organic as $idx => $item ) {
            $url    = $item['link'] ?? '';
            $domain = wp_parse_url( $url, PHP_URL_HOST );
            $domain = preg_replace( '/^www\./', '', $domain ?? '' );

            $results[] = array(
                'position' => $item['position'] ?? ( $idx + 1 ),
                'title'    => $item['title'] ?? '',
                'url'      => $url,
                'domain'   => $domain,
                'snippet'  => $item['snippet'] ?? '',
            );
        }

        error_log( 'DBA Sniper: Got ' . count( $results ) . ' organic results for "' . $keyword . '"' );

        // ── Capture Related Searches + People Also Ask ──
        $related  = array();
        $paa      = array();

        if ( ! empty( $data['relatedSearches'] ) ) {
            foreach ( $data['relatedSearches'] as $rs ) {
                $q = $rs['query'] ?? '';
                if ( ! empty( $q ) ) $related[] = $q;
            }
        }

        if ( ! empty( $data['peopleAlsoAsk'] ) ) {
            foreach ( $data['peopleAlsoAsk'] as $pa ) {
                $q = $pa['question'] ?? '';
                if ( ! empty( $q ) ) $paa[] = $q;
            }
        }

        return array(
            'organic'          => $results,
            'related_searches' => $related,
            'people_also_ask'  => $paa,
            'keyword'          => $keyword,
            'language'         => $language,
        );
    }

    /**
     * Fetch and parse a competitor page — extract structural SEO data.
     *
     * @param string $url       The competitor URL.
     * @param string $keyword   The target keyword for density analysis.
     * @return array|null       Parsed data or null on failure.
     */
    public static function analyze_page( $url, $keyword ) {
        // Skip PDFs, images, and known problematic extensions
        $skip_ext = array( '.pdf', '.jpg', '.png', '.gif', '.doc', '.xls' );
        foreach ( $skip_ext as $ext ) {
            if ( stripos( $url, $ext ) !== false ) return null;
        }

        $response = wp_remote_get( $url, array(
            'timeout'    => 15,
            'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
            'sslverify'  => false,
        ) );

        if ( is_wp_error( $response ) ) {
            error_log( 'DBA Sniper: Failed to fetch ' . $url . ' — ' . $response->get_error_message() );
            return null;
        }

        $code = wp_remote_retrieve_response_code( $response );
        if ( $code !== 200 ) return null;

        $html = wp_remote_retrieve_body( $response );
        if ( empty( $html ) || strlen( $html ) < 500 ) return null;

        return self::parse_html( $html, $keyword, $url );
    }

    /**
     * Parse HTML into structured SEO data.
     */
    public static function parse_html( $html, $keyword, $url ) {
        $data = array(
            'url'              => $url,
            'word_count'       => 0,
            'headings'         => array( 'h1' => array(), 'h2' => array(), 'h3' => array() ),
            'keyword_count'    => 0,
            'keyword_density'  => 0,
            'keyword_in_title' => false,
            'keyword_in_h1'    => false,
            'keyword_in_h2'    => false,
            'keyword_in_first_p' => false,
            'meta_description' => '',
            'meta_desc_length' => 0,
            'has_faq'          => false,
            'has_table'        => false,
            'has_schema'       => false,
            'image_count'      => 0,
            'internal_links'   => 0,
            'external_links'   => 0,
        );

        libxml_use_internal_errors( true );
        $doc = new DOMDocument();
        @$doc->loadHTML( '<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR );
        libxml_clear_errors();

        $xpath = new DOMXPath( $doc );

        // Title tag
        $titles = $doc->getElementsByTagName( 'title' );
        $page_title = $titles->length > 0 ? $titles->item(0)->textContent : '';
        $data['keyword_in_title'] = ( stripos( $page_title, $keyword ) !== false );

        // Meta description
        $metas = $xpath->query( '//meta[@name="description"]' );
        if ( $metas->length > 0 ) {
            $data['meta_description'] = $metas->item(0)->getAttribute( 'content' );
            $data['meta_desc_length'] = mb_strlen( $data['meta_description'] );
        }

        // Extract main content
        $content_node = $xpath->query( '//article' );
        if ( $content_node->length === 0 ) $content_node = $xpath->query( '//main' );
        if ( $content_node->length === 0 ) $content_node = $xpath->query( '//body' );

        $content_html = '';
        if ( $content_node->length > 0 ) {
            $content_html = $doc->saveHTML( $content_node->item(0) );
        }

        $content_html = preg_replace( '/<(script|style|nav|header|footer|aside)\b[^>]*>.*?<\/\1>/si', '', $content_html );

        $plain_text = wp_strip_all_tags( $content_html );
        $plain_text = preg_replace( '/\s+/', ' ', trim( $plain_text ) );

        $data['word_count'] = str_word_count( $plain_text );

        // Keyword density
        $kw_lower = mb_strtolower( $keyword );
        $data['keyword_count'] = mb_substr_count( mb_strtolower( $plain_text ), $kw_lower );
        $total_words = $data['word_count'];
        $kw_words = str_word_count( $keyword );
        $data['keyword_density'] = $total_words > 0
            ? round( ( $data['keyword_count'] * $kw_words / $total_words ) * 100, 2 )
            : 0;

        // Headings
        foreach ( array( 'h1', 'h2', 'h3' ) as $tag ) {
            $nodes = $doc->getElementsByTagName( $tag );
            for ( $i = 0; $i < $nodes->length; $i++ ) {
                $text = trim( $nodes->item( $i )->textContent );
                if ( ! empty( $text ) ) {
                    $data['headings'][ $tag ][] = $text;
                }
            }
        }

        foreach ( $data['headings']['h1'] as $h ) {
            if ( stripos( $h, $keyword ) !== false ) { $data['keyword_in_h1'] = true; break; }
        }
        foreach ( $data['headings']['h2'] as $h ) {
            if ( stripos( $h, $keyword ) !== false ) { $data['keyword_in_h2'] = true; break; }
        }

        $first_p = $xpath->query( '//article//p[1] | //main//p[1] | //body//p[1]' );
        if ( $first_p->length > 0 ) {
            $data['keyword_in_first_p'] = ( stripos( $first_p->item(0)->textContent, $keyword ) !== false );
        }

        $data['has_faq'] = (
            stripos( $html, 'FAQPage' ) !== false ||
            stripos( $html, 'faq' ) !== false ||
            preg_match( '/<(dt|details|summary)\b/i', $html )
        );

        $tables = $doc->getElementsByTagName( 'table' );
        $data['has_table'] = ( $tables->length > 0 );

        $data['has_schema'] = ( stripos( $html, 'application/ld+json' ) !== false );

        $imgs = $doc->getElementsByTagName( 'img' );
        $data['image_count'] = $imgs->length;

        $domain = wp_parse_url( $url, PHP_URL_HOST );
        $links  = $doc->getElementsByTagName( 'a' );
        for ( $i = 0; $i < min( $links->length, 200 ); $i++ ) {
            $href = $links->item( $i )->getAttribute( 'href' );
            if ( empty( $href ) || $href[0] === '#' ) continue;
            if ( stripos( $href, $domain ) !== false || $href[0] === '/' ) {
                $data['internal_links']++;
            } else {
                $data['external_links']++;
            }
        }

        return $data;
    }

    /**
     * Map language code to Google country code.
     */
    private static function lang_to_country( $lang ) {
        $map = array(
            'en' => 'gb', 'fr' => 'fr', 'de' => 'de', 'it' => 'it',
            'es' => 'es', 'pt' => 'pt', 'nl' => 'nl', 'lu' => 'lu',
        );
        return $map[ $lang ] ?? 'lu';
    }
}
