<?php
/**
 * DBA Plagiarism Checker - Checks content uniqueness BEFORE publishing
 * Methods:
 *   1) Copyscape API (if key provided — premium, most reliable)
 *   2) Internal duplicate detection (compares against all existing DBA posts)
 *   3) Sentence-sampling via web search (free fallback — checks random sentences via Google)
 */
class DBA_Plagiarism {

    private static $instance = null;

    /** Minimum uniqueness score to pass (0–100). Below this = reject. */
    const MIN_UNIQUENESS = 85;

    /** Number of random sentences to sample for web-search method */
    const SAMPLE_SENTENCES = 5;

    /** Minimum sentence length (words) worth checking */
    const MIN_SENTENCE_WORDS = 8;

    /** Similarity threshold for internal fuzzy matching (0-1, 1 = identical) */
    const INTERNAL_SIMILARITY_THRESHOLD = 0.35;

    public static function get_instance() {
        if (null === self::$instance) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {}

    /* ──────────────────────────────────────────────
     *  PUBLIC API — called by Post Creator
     * ────────────────────────────────────────────── */

    /**
     * Run all enabled plagiarism checks.
     *
     * @param  string $content_html  The article HTML (with CTA).
     * @param  int    $exclude_id    Post ID to exclude from internal check (0 for new).
     * @return array  ['pass' => bool, 'score' => int 0-100, 'details' => string[], 'method' => string]
     */
    public function check($content_html, $exclude_id = 0) {
        $plain = $this->html_to_plain($content_html);

        // 1) Internal duplicate check (always runs — free, fast)
        $internal = $this->check_internal($plain, $exclude_id);
        if (!$internal['pass']) {
            return $internal;
        }

        // 2) Copyscape (if API key configured)
        $settings = get_option('dba_settings', array());
        $copyscape_user = isset($settings['copyscape_user']) ? $settings['copyscape_user'] : '';
        $copyscape_key  = isset($settings['copyscape_key'])  ? $settings['copyscape_key']  : '';

        if (!empty($copyscape_user) && !empty($copyscape_key)) {
            $cs = $this->check_copyscape($plain, $copyscape_user, $copyscape_key);
            return $cs;
        }

        // 3) Sentence-sampling web search (free fallback)
        $web = $this->check_web_sentences($plain);
        return $web;
    }

    /* ──────────────────────────────────────────────
     *  METHOD 1: Internal duplicate detection
     * ────────────────────────────────────────────── */

    /**
     * Compare against every existing DBA post using cosine-like similarity
     * on word-frequency vectors (shingling approach).
     */
    private function check_internal($plain_text, $exclude_id = 0) {
        $result = array(
            'pass'    => true,
            'score'   => 100,
            'details' => array(),
            'method'  => 'internal',
        );

        // Get all existing DBA posts
        $posts = get_posts(array(
            'post_type'      => 'post',
            'post_status'    => array('publish', 'draft', 'future', 'pending'),
            'posts_per_page' => 200,
            'meta_query'     => array(
                array('key' => '_dba_auto_created', 'compare' => 'EXISTS'),
            ),
            'fields' => 'ids',
        ));

        if (empty($posts)) {
            $result['details'][] = 'No existing posts to compare (first post).';
            return $result;
        }

        $new_shingles = $this->build_shingles($plain_text);
        if (empty($new_shingles)) {
            return $result;
        }

        $highest_sim = 0;
        $most_similar_id = 0;

        foreach ($posts as $pid) {
            if ($pid == $exclude_id) continue;

            $existing_content = get_post_field('post_content', $pid);
            if (empty($existing_content)) continue;

            $existing_plain = $this->html_to_plain($existing_content);
            $existing_shingles = $this->build_shingles($existing_plain);

            if (empty($existing_shingles)) continue;

            $sim = $this->jaccard_similarity($new_shingles, $existing_shingles);

            if ($sim > $highest_sim) {
                $highest_sim = $sim;
                $most_similar_id = $pid;
            }
        }

        $uniqueness = (int) round((1 - $highest_sim) * 100);
        $result['score'] = $uniqueness;

        if ($highest_sim >= self::INTERNAL_SIMILARITY_THRESHOLD) {
            $result['pass'] = false;
            $result['details'][] = sprintf(
                'Too similar to existing post #%d (%.1f%% overlap). Minimum uniqueness required: %d%%.',
                $most_similar_id,
                $highest_sim * 100,
                self::MIN_UNIQUENESS
            );
            error_log('DBA Plagiarism: FAILED internal check — ' . round($highest_sim * 100, 1) . '% similar to post #' . $most_similar_id);
        } else {
            $result['details'][] = sprintf(
                'Internal check passed. Highest similarity: %.1f%% (post #%d).',
                $highest_sim * 100,
                $most_similar_id
            );
        }

        return $result;
    }

    /**
     * Build word n-gram shingles (3-grams) from text.
     */
    private function build_shingles($text, $n = 3) {
        $words = preg_split('/\s+/', strtolower(trim($text)));
        $words = array_values(array_filter($words, function($w) {
            return mb_strlen($w) > 1;
        }));

        if (count($words) < $n) return array();

        $shingles = array();
        for ($i = 0; $i <= count($words) - $n; $i++) {
            $shingle = implode(' ', array_slice($words, $i, $n));
            $shingles[$shingle] = true;
        }
        return $shingles;
    }

    /**
     * Jaccard similarity between two shingle sets.
     */
    private function jaccard_similarity($set_a, $set_b) {
        $intersection = count(array_intersect_key($set_a, $set_b));
        $union = count($set_a) + count($set_b) - $intersection;
        return $union > 0 ? $intersection / $union : 0;
    }

    /* ──────────────────────────────────────────────
     *  METHOD 2: Copyscape API
     * ────────────────────────────────────────────── */

    /**
     * Check via Copyscape Premium API (csearch — text search).
     */
    private function check_copyscape($plain_text, $user, $api_key) {
        $result = array(
            'pass'    => true,
            'score'   => 100,
            'details' => array(),
            'method'  => 'copyscape',
        );

        // Copyscape text search endpoint
        $url = add_query_arg(array(
            'u' => $user,
            'o' => $api_key,
            'f' => 'json',
            'c' => '1',          // encoding flag
            'e' => 'UTF-8',
        ), 'https://www.copyscape.com/api/');

        $response = wp_remote_post($url, array(
            'timeout' => 60,
            'body'    => array('t' => substr($plain_text, 0, 25000)), // Copyscape limit
        ));

        if (is_wp_error($response)) {
            $result['details'][] = 'Copyscape API error: ' . $response->get_error_message();
            // Fail-open: allow publication but log warning
            error_log('DBA Plagiarism WARNING: Copyscape unavailable — ' . $response->get_error_message());
            return $result;
        }

        $code = wp_remote_retrieve_response_code($response);
        if ($code !== 200) {
            $result['details'][] = 'Copyscape returned HTTP ' . $code;
            return $result;
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);

        if (isset($body['error'])) {
            $result['details'][] = 'Copyscape error: ' . ($body['error'] ?? 'unknown');
            return $result;
        }

        $match_count = isset($body['count']) ? intval($body['count']) : 0;

        if ($match_count === 0) {
            $result['details'][] = 'Copyscape: 0 matches found. Content is unique.';
            return $result;
        }

        // Analyze matches
        $total_matched_words = 0;
        $total_words = str_word_count($plain_text);
        $flagged_urls = array();

        if (isset($body['result']) && is_array($body['result'])) {
            foreach ($body['result'] as $match) {
                $pct = isset($match['percentmatched']) ? floatval($match['percentmatched']) : 0;
                $matched_url = isset($match['url']) ? $match['url'] : 'unknown';
                $matched_words = isset($match['wordsmatched']) ? intval($match['wordsmatched']) : 0;

                $total_matched_words += $matched_words;
                $flagged_urls[] = $matched_url . ' (' . $pct . '%)';
            }
        }

        $uniqueness = $total_words > 0
            ? max(0, (int) round((1 - ($total_matched_words / $total_words)) * 100))
            : 100;

        $result['score'] = $uniqueness;

        if ($uniqueness < self::MIN_UNIQUENESS) {
            $result['pass'] = false;
            $result['details'][] = sprintf(
                'Copyscape: %d matches found. Uniqueness: %d%% (minimum: %d%%). Flagged: %s',
                $match_count,
                $uniqueness,
                self::MIN_UNIQUENESS,
                implode('; ', array_slice($flagged_urls, 0, 5))
            );
            error_log('DBA Plagiarism: FAILED Copyscape — uniqueness ' . $uniqueness . '% — ' . implode(', ', array_slice($flagged_urls, 0, 3)));
        } else {
            $result['details'][] = sprintf(
                'Copyscape: %d matches found but uniqueness is %d%% (above %d%% threshold). OK.',
                $match_count,
                $uniqueness,
                self::MIN_UNIQUENESS
            );
        }

        return $result;
    }

    /* ──────────────────────────────────────────────
     *  METHOD 3: Web sentence sampling (free fallback)
     * ────────────────────────────────────────────── */

    /**
     * Pick random sentences, search Google for exact matches.
     * If too many come back with results, flag as plagiarised.
     */
    private function check_web_sentences($plain_text) {
        $result = array(
            'pass'    => true,
            'score'   => 100,
            'details' => array(),
            'method'  => 'web_sentence_sampling',
        );

        // Split into sentences
        $sentences = preg_split('/(?<=[.!?])\s+/', $plain_text, -1, PREG_SPLIT_NO_EMPTY);

        // Filter: only keep meaningful sentences (8+ words)
        $candidates = array();
        foreach ($sentences as $s) {
            $s = trim($s);
            $wc = str_word_count($s);
            if ($wc >= self::MIN_SENTENCE_WORDS && $wc <= 40) {
                $candidates[] = $s;
            }
        }

        if (count($candidates) < 3) {
            $result['details'][] = 'Not enough long sentences to sample. Skipping web check.';
            return $result;
        }

        // Pick random sample
        $sample_count = min(self::SAMPLE_SENTENCES, count($candidates));
        $keys = array_rand($candidates, $sample_count);
        if (!is_array($keys)) $keys = array($keys);

        $samples = array();
        foreach ($keys as $k) {
            $samples[] = $candidates[$k];
        }

        $matches_found = 0;
        $checked = 0;

        foreach ($samples as $sentence) {
            // Trim sentence to ~10 words for search (exact phrase)
            $words = explode(' ', $sentence);
            $search_phrase = implode(' ', array_slice($words, 0, 10));

            // Use WP HTTP to search (Google Custom Search or fallback)
            $found = $this->search_exact_phrase($search_phrase);

            $checked++;
            if ($found) {
                $matches_found++;
                $result['details'][] = 'Sentence match found: "' . wp_trim_words($sentence, 10, '…') . '"';
            }

            // Rate limit
            usleep(500000);
        }

        // Score: if 3+ out of 5 sentences found online, likely plagiarized
        $match_ratio = $checked > 0 ? $matches_found / $checked : 0;
        $uniqueness = (int) round((1 - $match_ratio) * 100);
        $result['score'] = $uniqueness;

        if ($match_ratio > 0.4) {
            // More than 40% of sampled sentences found verbatim
            $result['pass'] = false;
            $result['details'][] = sprintf(
                'Web sampling: %d/%d sentences found online (%.0f%%). Content may not be original.',
                $matches_found,
                $checked,
                $match_ratio * 100
            );
            error_log('DBA Plagiarism: FAILED web sampling — ' . $matches_found . '/' . $checked . ' sentences found');
        } else {
            $result['details'][] = sprintf(
                'Web sampling: %d/%d sentences found online (%.0f%%). Below threshold — OK.',
                $matches_found,
                $checked,
                $match_ratio * 100
            );
        }

        return $result;
    }

    /**
     * Search for an exact phrase using Google Custom Search API or WordPress HTTP.
     * Returns true if an exact match is found.
     */
    private function search_exact_phrase($phrase) {
        $settings = get_option('dba_settings', array());

        // Method A: Google Custom Search API (if configured)
        $google_api_key = isset($settings['google_search_key']) ? $settings['google_search_key'] : '';
        $google_cx      = isset($settings['google_search_cx'])  ? $settings['google_search_cx']  : '';

        if (!empty($google_api_key) && !empty($google_cx)) {
            return $this->google_custom_search($phrase, $google_api_key, $google_cx);
        }

        // Method B: Simple HEAD request to Google (less reliable, may hit captcha)
        // We'll use a conservative approach - search via Bing or skip
        // For production, Copyscape or Google Custom Search is recommended
        return false; // Skip if no search API configured
    }

    /**
     * Google Custom Search API exact-phrase check.
     */
    private function google_custom_search($phrase, $api_key, $cx) {
        $query = '"' . $phrase . '"'; // Exact phrase

        $url = add_query_arg(array(
            'key'  => $api_key,
            'cx'   => $cx,
            'q'    => $query,
            'num'  => 3,
        ), 'https://www.googleapis.com/customsearch/v1');

        $response = wp_remote_get($url, array('timeout' => 15));

        if (is_wp_error($response)) return false;

        $body = json_decode(wp_remote_retrieve_body($response), true);
        $total = isset($body['searchInformation']['totalResults']) ? intval($body['searchInformation']['totalResults']) : 0;

        // If exact phrase appears on other sites, flag it
        return $total > 0;
    }

    /* ──────────────────────────────────────────────
     *  UTILITIES
     * ────────────────────────────────────────────── */

    /**
     * Strip HTML → clean plain text.
     */
    private function html_to_plain($html) {
        // Remove CTA block first (it's boilerplate, ignore for plagiarism)
        $html = preg_replace(
            '/<p>\s*Damalion supports investors.*?contact your Damalion experts now\s*<\/a>\.\s*<\/p>/si',
            '', $html
        );

        // Remove GeoBot CTA blocks (all languages: FR/IT/ES/PT)
        $html = preg_replace(
            '/<div class="damalion-cta"[^>]*>.*?<\/div>/si',
            '', $html
        );

        // Remove any CTA paragraph containing "Damalion accompagne" or "Damalion acompaña" etc.
        $html = preg_replace(
            '/<p>\s*Damalion\s+(accompagne|accompagna|acompaña|acompanha|helps|supports).*?<\/p>/si',
            '', $html
        );

        // Remove "Related technical notes" section (boilerplate links)
        $html = preg_replace(
            '/<h3>\s*Related technical notes\s*<\/h3>\s*<ul>[\s\S]*?<\/ul>/si',
            '', $html
        );

        // Strip tags
        $text = wp_strip_all_tags($html);

        // Normalize whitespace
        $text = preg_replace('/\s+/', ' ', trim($text));

        // Remove common stopwords for better comparison
        // (we keep them for sentence sampling but remove for shingling)

        return $text;
    }
}
