<?php

namespace WPDeveloper\BetterDocsPro\REST;

use WP_Error;
use WP_REST_Request;
use WPDeveloper\BetterDocs\Core\BaseAPI;

class InstantAnswer extends BaseAPI {
    public function register() {
        $this->post( '/ask', [$this, 'ask'] );
        $this->post( '/feedback', [$this, 'feedback'] );
    }

    public function sanitize( $post_data = [] ) {
        if ( ! empty( $post_data ) ) {
            $sanitized_data = [];
            foreach ( $post_data as $key => $data ) {
                if ( $key === 'email' ) {
                    $sanitized_data[$key] = sanitize_email( $data );
                } else {
                    $sanitized_data[$key] = esc_html( stripslashes( $data ) );
                }
            }

            return $sanitized_data;
        }
        return [];
    }

    public function ready_subject( $sanitized_data, $ask_subject ) {
        $ask_subject = ! empty( $ask_subject ) ? $ask_subject : '[ia_subject]';

        $_subject_data = '';
        if ( isset( $sanitized_data['subject'] ) ) {
            $_subject_data = $sanitized_data['subject'];
        }
        $subject = str_replace( '[ia_subject]', $_subject_data, $ask_subject );

        $_email_data = '';
        if ( isset( $sanitized_data['email'] ) ) {
            $_email_data = $sanitized_data['email'];
        }
        $subject = str_replace( '[ia_email]', $_email_data, $subject );

        $_name_data = '';
        if ( isset( $sanitized_data['name'] ) ) {
            $_name_data = $sanitized_data['name'];
        }
        $subject = str_replace( '[ia_name]', $_name_data, $subject );

        return $subject;
    }

    /**
     * Verify a reCAPTCHA v3 token against Google's siteverify endpoint.
     * Returns true when verification passes (or is disabled). Returns a
     * WP_Error for missing/invalid/low-score tokens so the REST response
     * surfaces the rejection with an appropriate HTTP status.
     *
     * @return true|WP_Error
     */
    protected function verify_recaptcha( WP_REST_Request $request ) {
        $enabled    = (bool) $this->settings->get( 'enable_recaptcha', false );
        $site_key   = trim( (string) $this->settings->get( 'recaptcha_site_key', '' ) );
        $secret_key = trim( (string) $this->settings->get( 'recaptcha_secret_key', '' ) );

        if ( ! $enabled ) {
            return true;
        }

        // Fail closed: if the toggle is on but keys are missing (e.g. reached via a
        // direct DB write or migration that bypasses the settings-save validator),
        // reject rather than silently letting every request through.
        if ( $site_key === '' || $secret_key === '' ) {
            return new WP_Error(
                'recaptcha_misconfigured',
                __( 'Verification is currently unavailable. Please try again later.', 'betterdocs-pro' ),
                [ 'status' => 503 ]
            );
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- public REST /ask endpoint; abuse protection is reCAPTCHA + per-IP rate limiting + honeypot, not a nonce (anonymous visitors have none). Token is sanitized and then verified with Google.
        $token = isset( $_POST['recaptcha_token'] ) ? sanitize_text_field( wp_unslash( $_POST['recaptcha_token'] ) ) : '';
        if ( $token === '' ) {
            return new WP_Error(
                'recaptcha_failed',
                __( 'Verification failed. Please try again.', 'betterdocs-pro' ),
                [ 'status' => 400 ]
            );
        }

        $response = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', [
            'timeout' => 5,
            'body'    => [
                'secret'   => $secret_key,
                'response' => $token,
                'remoteip' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''
            ]
        ] );

        if ( is_wp_error( $response ) ) {
            return new WP_Error(
                'recaptcha_unreachable',
                __( 'Could not reach the verification service. Please try again later.', 'betterdocs-pro' ),
                [ 'status' => 503 ]
            );
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $body ) || empty( $body['success'] ) ) {
            return new WP_Error(
                'recaptcha_failed',
                __( 'Verification failed. Please try again.', 'betterdocs-pro' ),
                [ 'status' => 400 ]
            );
        }

        if ( isset( $body['action'] ) && $body['action'] !== 'betterdocs_ask' ) {
            return new WP_Error(
                'recaptcha_failed',
                __( 'Verification failed. Please try again.', 'betterdocs-pro' ),
                [ 'status' => 400 ]
            );
        }

        $threshold = (float) $this->settings->get( 'recaptcha_score_threshold', 0.5 );
        $score     = isset( $body['score'] ) ? (float) $body['score'] : 0.0;
        if ( $score < $threshold ) {
            return new WP_Error(
                'recaptcha_failed',
                __( 'Verification failed. Please try again.', 'betterdocs-pro' ),
                [ 'status' => 400 ]
            );
        }

        return true;
    }

    /**
     * Throttle anonymous Ask submissions per IP using a transient counter.
     * The /ask route is intentionally public, so this is the only volume brake
     * when reCAPTCHA is disabled. Returns true when within the limit, or a
     * WP_Error (429) once the limit is exceeded.
     *
     * @return true|WP_Error
     */
    protected function check_rate_limit() {
        $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
        if ( $ip === '' ) {
            return true;
        }

        $key   = 'betterdocs_ia_ask_' . md5( $ip );
        $count = (int) get_transient( $key );
        if ( $count >= 5 ) {
            return new WP_Error(
                'ask_rate_limited',
                __( 'Too many requests. Please try again in a few minutes.', 'betterdocs-pro' ),
                [ 'status' => 429 ]
            );
        }

        set_transient( $key, $count + 1, 10 * MINUTE_IN_SECONDS );
        return true;
    }

    /**
     * This method is responsible for sending emails to site admin from ask form in IA.
     * @param mixed $request
     * @return mixed
     */
    public function ask( WP_REST_Request $request ) {
        // Honeypot: a hidden field real users never fill. Silently accept (and drop)
        // bot submissions so the trap is not revealed.
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- public REST /ask endpoint; bot honeypot check, no nonce by design (see verify_recaptcha()/check_rate_limit()).
        if ( ! empty( $_POST['betterdocs_hp'] ) ) {
            return true;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- public REST /ask endpoint; payload is sanitized in sanitize(). Abuse protection is reCAPTCHA + per-IP rate limiting + honeypot, not a nonce.
        $sanitized_data = $this->sanitize( $_POST );
        if ( empty( $sanitized_data ) || empty( $sanitized_data['email'] ) || ! is_email( $sanitized_data['email'] ) ) {
            return new WP_Error(
                'ask_invalid',
                __( 'Please provide a valid email address.', 'betterdocs-pro' ),
                [ 'status' => 400 ]
            );
        }

        $rate_limit_check = $this->check_rate_limit();
        if ( is_wp_error( $rate_limit_check ) ) {
            return $rate_limit_check;
        }

        $recaptcha_check = $this->verify_recaptcha( $request );
        if ( is_wp_error( $recaptcha_check ) ) {
            return $recaptcha_check;
        }

        $ask_subject = $this->settings->get( 'ask_subject', '[ia_subject]' );
        $to          = $this->settings->get( 'ask_email', get_bloginfo( 'admin_email' ) );

        $subject = html_entity_decode( $this->ready_subject( $sanitized_data, $ask_subject ), ENT_QUOTES, 'UTF-8' );
        if ( isset( $sanitized_data['subject'] ) ) {
            $sanitized_data['subject'] = $subject;
        }

        $files = $request->get_file_params();
        // Enforce the admin "File Upload" toggle server-side; without this the
        // unauthenticated endpoint accepts uploads even when the feature is OFF.
        $file_upload_enabled = wp_validate_boolean( $this->settings->get( 'chat_tab_file_upload_switch', true ) );
        if ( $file_upload_enabled && ! empty( $files['file'] ) ) {
            if ( ! function_exists( 'wp_handle_upload' ) ) {
                require_once ABSPATH . 'wp-admin/includes/file.php';
            }

            // Mirror the client-side allowlist (png/gif/jpg/jpeg/pdf) and 5 MB size
            // limit on the server, and cap the file count, so the client controls
            // cannot be bypassed by a direct request.
            $allowed_mimes = [
                'jpg|jpeg' => 'image/jpeg',
                'gif'      => 'image/gif',
                'png'      => 'image/png',
                'pdf'      => 'application/pdf'
            ];
            $max_file_size = 5 * MB_IN_BYTES;
            $max_files     = 10;

            $upload_overrides = [
                'test_form'                => false,
                'mimes'                    => $allowed_mimes,
                // Store under a random, unguessable name so private attachments
                // don't get predictable public URLs.
                'unique_filename_callback' => [$this, 'ia_unique_filename']
            ];
            $new_files  = $files['file'];
            $movedFiles = [];
            $processed  = 0;
            foreach ( $new_files['name'] as $key => $value ) {
                if ( empty( $new_files['name'][$key] ) ) {
                    continue;
                }
                if ( ++$processed > $max_files ) {
                    break;
                }
                if ( (int) $new_files['size'][$key] > $max_file_size ) {
                    continue;
                }
                $file = [
                    'name'     => $new_files['name'][$key],
                    'type'     => $new_files['type'][$key],
                    'tmp_name' => $new_files['tmp_name'][$key],
                    'error'    => $new_files['error'][$key],
                    'size'     => $new_files['size'][$key]
                ];
                $movedFile = wp_handle_upload( $file, $upload_overrides );
                if ( ! isset( $movedFile['error'] ) ) {
                    $movedFiles[] = $movedFile;
                }
            }
            if ( ! empty( $movedFiles ) ) {
                $sanitized_data['files'] = $movedFiles;
            }
        }
        $body    = $this->mail_body( $sanitized_data );
        $name    = isset( $sanitized_data['name'] ) ? html_entity_decode( $sanitized_data['name'], ENT_QUOTES, 'UTF-8' ) : '';
        // Strip CR/LF so a crafted name cannot inject extra mail headers (e.g. Bcc:).
        $name    = trim( preg_replace( '/[\r\n]+/', ' ', $name ) );
        $from    = $sanitized_data['email'];
        if ( ! is_email( $from ) ) {
            $from = $to;
        }
        $headers = ['Content-Type: text/html; charset=UTF-8', "From: $name <$from>", 'Reply-To: ' . $from];
        if ( wp_mail( $to, $subject, $body, $headers ) ) {
            return true;
        }
        return new WP_Error(
            'ask_mail_failed',
            __( 'Your message could not be sent. Please try again later.', 'betterdocs-pro' ),
            [ 'status' => 500 ]
        );
    }

    /**
     * Generate a random, unguessable filename for IA Ask-form uploads while
     * preserving the original extension, so private attachments can't be reached
     * via predictable /wp-content/uploads/YYYY/MM/<original-name> URLs.
     *
     * @param string $dir
     * @param string $name
     * @param string $ext
     * @return string
     */
    public function ia_unique_filename( $dir, $name, $ext ) {
        return wp_generate_password( 20, false ) . $ext;
    }

    public function mail_body( $data ) {
        if ( empty( $data ) ) {
            return '';
        }

        ob_start();
        betterdocs_pro()->views->get( 'admin/email/ia', ['all_data' => $data] );
        return ob_get_clean();
    }

    /**
     * Save Global Feedback
     * @param WP_REST_Request $request
     * @return bool
     */
    public function feedback( WP_REST_Request $request ) {
        $feelings        = isset( $request['feelings'] ) ? $request['feelings'] : 'happy';
        $allowed_feelings = ['happy', 'normal', 'sad'];

        if ( ! in_array( $feelings, $allowed_feelings, true ) ) {
            return new WP_Error(
                'betterdocs_invalid_feelings',
                __( 'Invalid feedback value.', 'betterdocs-pro' ),
                ['status' => 400]
            );
        }

        $feedback = get_option( '_betterdocs_feelings', [] );

        $feedback[$feelings] = ( isset( $feedback[$feelings] ) ? intval( $feedback[$feelings] ) : 0 ) + 1;
        if ( update_option( '_betterdocs_feelings', $feedback, 'no' ) ) {
            return true;
        }
        return false;
    }
}
