<?php
/**
 * Spellmatic_Dictionary
 *
 * Manages the site-level custom dictionary — a list of words that Harper should
 * silently skip during linting (e.g. brand names, proper nouns, industry terms).
 *
 * Storage: wp_options key 'spellmatic_dictionary' → JSON-encoded array of
 * lowercase strings, deduplicated, sorted A–Z.
 *
 * REST endpoints (all require manage_options capability):
 *   GET  /spellmatic/v1/dictionary          → { words: string[] }
 *   POST /spellmatic/v1/dictionary/add      → { word: string }  → { words: string[] }
 *   POST /spellmatic/v1/dictionary/remove   → { word: string }  → { words: string[] }
 */

defined( 'ABSPATH' ) || exit;

class Spellmatic_Dictionary {

    /** wp_options key for the dictionary. */
    const OPTION_KEY = 'spellmatic_dictionary';

    // =========================================================================
    // REST handlers
    // =========================================================================

    /**
     * Return the full dictionary list.
     * REST: GET /spellmatic/v1/dictionary
     */
    public static function get_dictionary( WP_REST_Request $request ): WP_REST_Response {
        return new WP_REST_Response( [ 'words' => self::get_words() ], 200 );
    }

    /**
     * Add a word (or comma-separated list of words) to the dictionary.
     * REST: POST /spellmatic/v1/dictionary/add
     *
     * Body: { "word": "Elementor" }
     *    or { "word": "Avada, Divi, WPBakery" }
     */
    public static function add_word( WP_REST_Request $request ): WP_REST_Response {
        $raw = sanitize_text_field( $request->get_param( 'word' ) ?? '' );
        if ( '' === $raw ) {
            return new WP_REST_Response( [ 'error' => 'No word provided.' ], 400 );
        }

        // Support comma-separated batch adds.
        $candidates = array_map( 'trim', explode( ',', $raw ) );
        $added      = 0;
        $words      = self::get_words();

        foreach ( $candidates as $candidate ) {
            if ( '' === $candidate ) continue;

            // Sanitise: strip anything that isn't a letter, digit, apostrophe, or hyphen.
            $clean = preg_replace( '/[^\p{L}\p{N}\'\-]/u', '', $candidate );
            if ( '' === $clean ) continue;

            $lower = mb_strtolower( $clean );
            if ( ! in_array( $lower, $words, true ) ) {
                $words[] = $lower;
                $added++;
            }
        }

        if ( $added > 0 ) {
            sort( $words );
            self::save_words( $words );
        }

        // Dismiss all open scan results whose original word matches any added word.
        $dismissed = 0;
        if ( $added > 0 ) {
            global $wpdb;
            $table = $wpdb->prefix . 'spellmatic_results';
            foreach ( $candidates as $candidate ) {
                $clean = preg_replace( '/[^\p{L}\p{N}\'\-]/u', '', trim( $candidate ) );
                if ( '' === $clean ) continue;
                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is safe ($wpdb->prefix + hardcoded string).
                $dismissed += (int) $wpdb->query( $wpdb->prepare(
                    "UPDATE {$table} SET status = %s WHERE LOWER(original) = %s AND status = %s",
                    'dismissed',
                    mb_strtolower( $clean ),
                    'open'
                ) );
                // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            }
        }

        return new WP_REST_Response( [ 'words' => $words, 'added' => $added, 'dismissed' => $dismissed ], 200 );
    }

    /**
     * Remove a word from the dictionary.
     * REST: POST /spellmatic/v1/dictionary/remove
     *
     * Body: { "word": "elementor" }
     */
    public static function remove_word( WP_REST_Request $request ): WP_REST_Response {
        $raw = sanitize_text_field( $request->get_param( 'word' ) ?? '' );
        if ( '' === $raw ) {
            return new WP_REST_Response( [ 'error' => 'No word provided.' ], 400 );
        }

        $lower = mb_strtolower( trim( $raw ) );
        $words = self::get_words();
        $new   = array_values( array_filter( $words, fn( $w ) => $w !== $lower ) );

        if ( count( $new ) !== count( $words ) ) {
            self::save_words( $new );
        }

        return new WP_REST_Response( [ 'words' => $new ], 200 );
    }

    // =========================================================================
    // Internal helpers
    // =========================================================================

    /**
     * Return the current dictionary as an array of lowercase strings.
     *
     * @return string[]
     */
    public static function get_words(): array {
        $raw = get_option( self::OPTION_KEY, '[]' );
        $arr = json_decode( $raw, true );
        return is_array( $arr ) ? $arr : [];
    }

    /**
     * Persist the dictionary.
     *
     * @param string[] $words
     */
    private static function save_words( array $words ): void {
        update_option( self::OPTION_KEY, wp_json_encode( array_values( $words ) ), false );
    }
}