<?php
/**
 * Base Endpoint for BoostBot REST API.
 *
 * @package BoostBot
 */

// Prevent direct access to the file.
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Abstract BoostBot_Base_Endpoint class.
 *
 * Provides common functionality for all REST API endpoints.
 *
 * @since 1.0.0
 */
abstract class BoostBot_Base_Endpoint
{
    /**
     * The namespace for the REST API.
     *
     * @since 1.0.0
     * @access protected
     * @var string $namespace
     */
    protected $namespace = 'boostbot/v1';

    /**
     * The base path for this endpoint.
     *
     * @since 1.0.0
     * @access protected
     * @var string $rest_base
     */
    protected $rest_base = '';

    /**
     * Register the routes for this endpoint.
     *
     * @since 1.0.0
     */
    abstract public function register();

    /**
     * Helper to register a REST route.
     *
     * @since 1.0.0
     * @param string          $route      The route path (appended to rest_base).
     * @param string|array    $methods    HTTP methods (GET, POST, PUT, DELETE).
     * @param callable        $callback   The callback function.
     * @param callable        $permission Permission callback.
     * @param array           $args       Optional. Arguments for the route.
     */
    protected function register_route($route, $methods, $callback, $permission, $args = [])
    {
        $full_route = '/' . $this->rest_base . $route;

        register_rest_route(
            $this->namespace,
            $full_route,
            [
                'methods'             => $methods,
                'callback'            => $callback,
                'permission_callback' => $permission,
                'args'                => $args,
            ]
        );
    }

    // =========================================================================
    // Permission Callbacks
    // =========================================================================

    /**
     * Allow public access (no authentication required).
     *
     * @since 1.0.0
     * @return bool Always returns true.
     */
    public function public_access()
    {
        return true;
    }

    /**
     * Require admin access (manage_options capability) OR a valid backend management key.
     *
     * This allows both:
     * 1. Logged-in WordPress admins (standard admin panel use)
     * 2. The backend service (remote support via X-BoostBot-Management-Key header)
     *
     * @since 1.0.0
     * @param WP_REST_Request|null $request The REST request object (auto-passed by WP REST API).
     * @return bool True if user has admin access or valid management key.
     */
    public function admin_access( $request = null )
    {
        // Standard WordPress admin check.
        if ( current_user_can( 'manage_options' ) ) {
            return true;
        }

        // Backend management key check (for remote support).
        if ( $request instanceof WP_REST_Request ) {
            return $this->verify_management_key( $request );
        }

        return false;
    }

    /**
     * Verify the backend management key from the request header.
     *
     * The key is stored as a SHA-256 hash in WP options. The backend sends
     * the plaintext key in the X-BoostBot-Management-Key header.
     *
     * @since 1.18.0
     * @param WP_REST_Request $request The REST request object.
     * @return bool True if the management key is valid.
     */
    protected function verify_management_key( $request )
    {
        $key = $request->get_header( 'X-BoostBot-Management-Key' );
        if ( empty( $key ) ) {
            return false;
        }

        $stored_hash = get_option( 'boostbot_management_key_hash', '' );
        if ( empty( $stored_hash ) ) {
            return false;
        }

        // Timing-safe comparison of SHA-256 hashes.
        $incoming_hash = hash( 'sha256', $key );
        return hash_equals( $stored_hash, $incoming_hash );
    }

    /**
     * Require editor access (edit_posts capability).
     *
     * @since 1.0.0
     * @return bool True if user has editor access, false otherwise.
     */
    public function editor_access()
    {
        return current_user_can('edit_posts');
    }

    /**
     * Verify nonce for public endpoints.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return bool|WP_Error True if valid, WP_Error otherwise.
     */
    public function nonce_check($request)
    {
        $nonce = $request->get_header('X-WP-Nonce');

        if (!wp_verify_nonce($nonce, 'wp_rest')) {
            return new WP_Error(
                'boostbot_invalid_nonce',
                __('Invalid security token. Please refresh the page and try again.', 'boostbot'),
                ['status' => 403]
            );
        }

        return true;
    }

    /**
     * Check rate limiting for the request.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @param string          $type    The rate limit type (chat, api).
     * @return bool|WP_Error True if allowed, WP_Error if rate limited.
     */
    public function rate_limit_check($request, $type = 'chat')
    {
        $identifier = $this->get_visitor_identifier($request);

        if (class_exists('BoostBot_Rate_Limiter') && BoostBot_Rate_Limiter::is_limited($identifier, $type)) {
            return new WP_Error(
                'boostbot_rate_limited',
                $this->get_rate_limit_message($request),
                ['status' => 429]
            );
        }

        return true;
    }

    /**
     * Combined permission check: rate limit + optional nonce.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return bool|WP_Error True if all checks pass.
     */
    public function public_with_rate_limit($request)
    {
        // Check rate limiting
        $rate_check = $this->rate_limit_check($request);
        if (is_wp_error($rate_check)) {
            return $rate_check;
        }

        return true;
    }

    /**
     * Combined permission check for public read endpoints using the lighter API bucket.
     *
     * Use this for passive bootstrap/config reads that should not consume the same
     * burst budget as active chat traffic.
     *
     * @param WP_REST_Request $request The REST request object.
     * @return bool|WP_Error True if all checks pass.
     */
    public function public_with_api_rate_limit($request)
    {
        $rate_check = $this->rate_limit_check($request, 'api');
        if (is_wp_error($rate_check)) {
            return $rate_check;
        }

        return true;
    }

    /**
     * Combined permission check for public user-triggered commerce actions.
     *
     * Use this for cart mutations, coupon application, and bundle flows. These
     * actions should be less strict than chat bursts but tighter than passive
     * read-only bootstrap endpoints.
     *
     * @param WP_REST_Request $request The REST request object.
     * @return bool|WP_Error True if all checks pass.
     */
    public function public_with_commerce_rate_limit($request)
    {
        $rate_check = $this->rate_limit_check($request, 'commerce');
        if (is_wp_error($rate_check)) {
            return $rate_check;
        }

        return true;
    }

    /**
     * Combined permission check: rate limit + nonce.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return bool|WP_Error True if all checks pass.
     */
    public function public_with_validation($request)
    {
        // Check rate limiting
        $rate_check = $this->rate_limit_check($request);
        if (is_wp_error($rate_check)) {
            return $rate_check;
        }

        // Nonce check is optional for REST API when using cookie auth
        // The REST API automatically validates logged-in users
        return true;
    }

    // =========================================================================
    // Response Helpers
    // =========================================================================

    /**
     * Return a success response.
     *
     * @since 1.0.0
     * @param mixed $data The response data.
     * @param int   $code Optional. HTTP status code. Default 200.
     * @return WP_REST_Response The response object.
     */
    protected function success($data, $code = 200)
    {
        return new WP_REST_Response(
            [
                'success' => true,
                'data'    => $data,
            ],
            $code
        );
    }

    /**
     * Return an error response.
     *
     * @since 1.0.0
     * @param string $message The error message.
     * @param int    $code    Optional. HTTP status code. Default 400.
     * @param mixed  $details Optional. Additional error details.
     * @return WP_Error The error object.
     */
    protected function error($message, $code = 400, $details = null)
    {
        $data = ['status' => $code];

        if ($details !== null) {
            $data['details'] = $details;
        }

        return new WP_Error(
            'boostbot_error',
            $message,
            $data
        );
    }

    /**
     * Return a validation error response.
     *
     * @since 1.0.0
     * @param array $errors Array of validation errors.
     * @return WP_Error The error object.
     */
    protected function validation_error($errors)
    {
        return new WP_Error(
            'boostbot_validation_error',
            __('Validation failed', 'boostbot'),
            [
                'status' => 400,
                'errors' => $errors,
            ]
        );
    }

    /**
     * Return a not found error response.
     *
     * @since 1.0.0
     * @param string $resource The resource type that wasn't found.
     * @return WP_Error The error object.
     */
    protected function not_found($resource = 'Resource')
    {
        return new WP_Error(
            'boostbot_not_found',
            /* translators: %s: resource type name */
            sprintf(__('%s not found', 'boostbot'), $resource),
            ['status' => 404]
        );
    }

    /**
     * Return an unauthorized error response.
     *
     * @since 1.0.0
     * @return WP_Error The error object.
     */
    protected function unauthorized()
    {
        return new WP_Error(
            'boostbot_unauthorized',
            __('You do not have permission to perform this action.', 'boostbot'),
            ['status' => 403]
        );
    }

    // =========================================================================
    // Utility Methods
    // =========================================================================

    /**
     * Get a unique identifier for the visitor.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return string The visitor identifier (MD5 hash).
     */
    protected function get_visitor_identifier($request)
    {
        // Use REMOTE_ADDR as the primary identifier (cannot be spoofed).
        // Only fall back to forwarded headers when behind a known reverse-proxy
        // (configured via WordPress TRUSTED_PROXY or similar).
        $ip = '';

        if (isset($_SERVER['REMOTE_ADDR'])) {
            $ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
        }

        // SECURITY: key abuse limiters on REMOTE_ADDR only. The User-Agent header is
        // fully client-controlled, so mixing it into the identifier let an attacker rotate
        // the UA per request to get a fresh rate/message-limit bucket and bypass all
        // throttling (message limiter, burst cooldown, rate limiter, spam monitor).
        return md5($ip);
    }

    /**
     * Get pagination parameters from request.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @param int             $default_per_page Default items per page.
     * @param int             $max_per_page Maximum items per page.
     * @return array Pagination parameters.
     */
    protected function get_pagination_params($request, $default_per_page = 20, $max_per_page = 100)
    {
        $page = absint($request->get_param('page')) ?: 1;
        $per_page = absint($request->get_param('per_page')) ?: $default_per_page;
        $per_page = min($per_page, $max_per_page);

        return [
            'page'     => $page,
            'per_page' => $per_page,
            'offset'   => ($page - 1) * $per_page,
        ];
    }

    /**
     * Build pagination response data.
     *
     * @since 1.0.0
     * @param int $page Current page.
     * @param int $per_page Items per page.
     * @param int $total Total items.
     * @return array Pagination data.
     */
    protected function build_pagination_response($page, $per_page, $total)
    {
        return [
            'page'        => $page,
            'per_page'    => $per_page,
            'total'       => $total,
            'total_pages' => ceil($total / $per_page),
        ];
    }

    /**
     * Get sorting parameters from request.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @param string          $default_orderby Default order by field.
     * @param array           $allowed_fields Allowed fields to sort by.
     * @return array Sorting parameters.
     */
    protected function get_sorting_params($request, $default_orderby = 'created_at', $allowed_fields = [])
    {
        $orderby = sanitize_text_field($request->get_param('orderby')) ?: $default_orderby;
        $order = strtoupper(sanitize_text_field($request->get_param('order')) ?: 'DESC');

        // Validate orderby field
        if (!empty($allowed_fields) && !in_array($orderby, $allowed_fields, true)) {
            $orderby = $default_orderby;
        }

        // Validate order direction
        if (!in_array($order, ['ASC', 'DESC'], true)) {
            $order = 'DESC';
        }

        return [
            'orderby' => $orderby,
            'order'   => $order,
        ];
    }

    /**
     * Extract a sanitized language code from the request.
     *
     * Looks for a `language` query/body parameter and sanitizes it via
     * BoostBot_Language. Returns '' if not provided or invalid.
     *
     * @param WP_REST_Request $request
     * @return string Two-letter language code or ''.
     */
    protected function get_request_language( $request ) {
        $raw = $request->get_param( 'language' );
        if ( empty( $raw ) ) {
            return '';
        }
        if ( class_exists( 'BoostBot_Language' ) ) {
            return BoostBot_Language::sanitize( $raw );
        }
        return sanitize_text_field( $raw );
    }

    /**
     * Return a localized rate-limit error string for the current request.
     *
     * @param WP_REST_Request|null $request Request object when available.
     * @return string
     */
    protected function get_rate_limit_message( $request = null ) {
        $language = '';
        if ( $request instanceof WP_REST_Request ) {
            $language = $this->get_request_language( $request );
        }

        return 'he' === $language
            ? 'יותר מדי בקשות. אנא המתינו רגע ונסו שוב.'
            : __( 'Too many requests. Please wait a moment and try again.', 'boostbot' );
    }
}

