<?php
/**
 * REST API Settings Controller.
 *
 * @package NimblyMail\SMTP
 */

namespace NimblyMail\SMTP\Rest;

use NimblyMail\SMTP\Log\Rotation;
use NimblyMail\SMTP\Options;
use NimblyMail\SMTP\SettingsProvider;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Class SettingsController
 *
 * Handles GET/POST of plugin settings and the per-section reset action
 * via REST.
 *
 * @since 1.0.0
 * @since 1.0.4 update_settings() now records the first time an admin saves
 *              the rotation group, so log rotation can tell "explicitly
 *              turned off" apart from "never configured"; reset_group()
 *              clears that record when the group is reset instead of saved.
 * @since 1.0.5 validate_sandbox_settings() moved to AbstractController so
 *              NetworkSettingsController can share it.
 */
class SettingsController extends AbstractController {

    /**
     * Register the REST routes.
     *
     * @since 1.0.0
     */
    public function register_routes(): void {
        register_rest_route(
            self::NAMESPACE,
            '/settings',
            array(
                array(
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => array( $this, 'get_settings' ),
                    'permission_callback' => array( $this, 'permissions_check' ),
                ),
                array(
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => array( $this, 'update_settings' ),
                    'permission_callback' => array( $this, 'permissions_check' ),
                ),
            )
        );

        register_rest_route(
            self::NAMESPACE,
            '/settings/reset',
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'reset_group' ),
                'permission_callback' => array( $this, 'permissions_check' ),
            )
        );
    }

    /**
     * Get the complete settings schema and current values.
     *
     * @since 1.0.0
     *
     * @return WP_REST_Response
     */
    public function get_settings(): WP_REST_Response {
        return new WP_REST_Response(
            array(
                'tabs' => SettingsProvider::get_instance()->get_tabs(),
            )
        );
    }

    /**
     * Update settings on the current site.
     *
     * Validates incoming data against the schema whitelist and enforces
     * network locks (returns 403 with the list of rejected fields).
     *
     * @since 1.0.0
     * @since 1.0.4 Records the rotation-configured marker when the payload
     *              actually persisted touches the rotation group.
     * @since 1.0.5 Builds the site-effective sandbox values locally and
     *              passes them to the now-shared validate_sandbox_settings()
     *              on AbstractController.
     *
     * @param WP_REST_Request $request The API request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function update_settings( WP_REST_Request $request ) {
        $data = $request->get_json_params();

        if ( ! is_array( $data ) ) {
            return new WP_Error( 'rest_invalid_param', __( 'Invalid data provided.', 'nimblymail-smtp' ), array( 'status' => 400 ) );
        }

        $schema    = $this->build_field_whitelist();
        $validated = $this->validate_against_schema( $data, $schema, true );
        if ( is_wp_error( $validated ) ) {
            return $validated;
        }

        if ( ! empty( $validated['mail']['from_email'] ) && ! is_email( $validated['mail']['from_email'] ) ) {
            return new WP_Error(
                'rest_invalid_email',
                __( 'Please enter a valid From Email address.', 'nimblymail-smtp' ),
                array( 'status' => 400 )
            );
        }

        $sandbox_effective = array(
            'enabled'      => Options::init()->get( 'sandbox', 'enabled' ),
            'mode'         => Options::init()->get( 'sandbox', 'mode' ),
            'safe_address' => Options::init()->get( 'sandbox', 'safe_address' ),
        );

        $sandbox_error = $this->validate_sandbox_settings( $validated, $sandbox_effective );
        if ( is_wp_error( $sandbox_error ) ) {
            return $sandbox_error;
        }

        /**
         * Filters the validated settings payload immediately before persistence.
         *
         * Late-stage hook for add-ons (e.g. NimblyMail Pro's SettingsLock,
         * which strips Pro-owned groups when the license has elapsed its
         * grace period). Callbacks may return:
         *
         *   - The same array, optionally mutated, to continue with the save.
         *   - A WP_Error to short-circuit the save with a structured error.
         *
         * Runs AFTER schema whitelist + per-field validation, so the array
         * shape is guaranteed by SettingsController. Runs BEFORE
         * Options::set(), so callbacks can still strip groups defensively.
         *
         * @since 1.0.0
         *
         * @param array<string, array<string, mixed>>|\WP_Error $validated Validated settings payload, keyed by option group.
         * @param \WP_REST_Request                              $request   Current REST request.
         */
        $validated = apply_filters( 'nimblymail_settings_pre_save', $validated, $request );

        if ( is_wp_error( $validated ) ) {
            return $validated;
        }

        Options::init()->set( $validated );
        Rotation::maybe_mark_configured( $validated );

        $response_data = array(
            'message' => __( 'Settings saved successfully.', 'nimblymail-smtp' ),
            'tabs'    => SettingsProvider::get_instance()->get_tabs(),
        );

        /**
         * Filters the success response payload sent back to the client.
         *
         * Used by add-ons that need to surface notices the original request
         * did not carry — e.g. NimblyMail Pro's SettingsLock appending a
         * `warnings` array describing fields that were silently stripped
         * under license lockdown. The frontend treats any item in
         * `response.warnings` as a non-success notice and overrides the
         * `message` field accordingly.
         *
         * Callbacks should preserve the keys they don't touch and merge
         * into the existing `warnings` array rather than replacing it,
         * so multiple add-ons can attach their own notices.
         *
         * @since 1.0.0
         *
         * @param array<string, mixed>                 $response_data Default success payload (`message`, `tabs`).
         * @param array<string, array<string, mixed>>  $validated     The payload actually saved (post pre_save filtering).
         * @param \WP_REST_Request                     $request       Current REST request.
         */
        $filtered = apply_filters( 'nimblymail_settings_save_response', $response_data, $validated, $request );
        if ( is_array( $filtered ) ) {
            $response_data = $filtered;
        }
        // A non-array return (callback misbehaved by returning WP_Error /
        // string / null) is silently discarded — the response keeps the
        // server's canonical {message, tabs} shape so the frontend tab
        // renderer doesn't crash on undefined data.tabs.

        return new WP_REST_Response( $response_data );
    }

    /**
     * Reset a single settings group back to network defaults.
     *
     * Clears every key the site has overridden for the given group so
     * resolution falls back to the network default (or plugin default on
     * single-site installs). Rate-limited to 10 resets per minute per user
     * to prevent an authenticated admin from hammering the endpoint.
     *
     * @since 1.0.0
     * @since 1.0.4 Resetting the rotation group also clears its
     *              rotation-configured marker, so a site left with no
     *              rotation choice falls back to the default retention floor
     *              instead of silently ending up with neither.
     *
     * @param WP_REST_Request $request The API request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function reset_group( WP_REST_Request $request ) {
        $rate_error = $this->check_rate_limit( 'reset_rate', 10, 60 );
        if ( is_wp_error( $rate_error ) ) {
            return $rate_error;
        }

        $body  = $request->get_json_params();
        $group = is_array( $body ) && isset( $body['group'] ) ? (string) $body['group'] : '';

        if ( '' === $group ) {
            return new WP_Error(
                'rest_missing_group',
                __( 'The "group" parameter is required.', 'nimblymail-smtp' ),
                array( 'status' => 400 )
            );
        }

        if ( ! Options::group_exists( $group ) ) {
            return new WP_Error(
                'rest_unknown_group',
                sprintf(
                    /* translators: %s: the unknown settings group name. */
                    __( 'Unknown settings group: %s', 'nimblymail-smtp' ),
                    sanitize_text_field( $group )
                ),
                array( 'status' => 400 )
            );
        }

        Options::init()->reset_group( $group );

        // Order matters: the marker must only be cleared after the group is
        // actually gone. Rotation::is_configured() self-heals a missing
        // marker from whatever 'rotation' data is still stored, so clearing
        // the marker first would leave a window where a rotation run could
        // find the old data, re-backfill the marker, and leave the floor
        // permanently suppressed with nothing configured.
        if ( 'rotation' === $group ) {
            Rotation::clear_configured_marker();
        }

        return new WP_REST_Response(
            array(
                'message' => __( 'Section reset to defaults.', 'nimblymail-smtp' ),
                'tabs'    => SettingsProvider::get_instance()->get_tabs(),
            )
        );
    }
}
