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

namespace NimblyMail\SMTP\Rest\Network;

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 NetworkSettingsController
 *
 * Handles GET/POST of network-wide plugin settings. Super admins editing
 * network defaults and per-field locks use this endpoint; sub-site admins
 * continue to use /settings.
 *
 * @since 1.0.0
 * @since 1.0.4 update_settings() now records the network-level
 *              rotation-configured marker when a save touches that group.
 * @since 1.0.5 update_settings() now enforces the same Developer Sandbox
 *              coherence check the site-level SettingsController enforces.
 */
class NetworkSettingsController extends AbstractNetworkController {

    /**
     * Register the REST routes.
     *
     * @since 1.0.0
     *
     * @return void
     */
    protected function do_register_routes(): void {
        register_rest_route(
            self::NAMESPACE,
            '/network/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' ),
                ),
            )
        );
    }

    /**
     * Get the network-context settings schema along with the locks tree.
     *
     * @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( 'network' ),
                'locks' => Options::init()->get_locks(),
            )
        );
    }

    /**
     * Update network defaults and locks.
     *
     * Request body shape:
     *   {
     *     "settings": { "group": { "key": value, ... }, ... },
     *     "locks":    { "group": { "key": bool,  ... }, ... }
     *   }
     *
     * Either key may be omitted. Unknown groups/fields are rejected with a
     * 400 error. Encrypted fields arriving masked are skipped to avoid
     * overwriting stored ciphertext.
     *
     * @since 1.0.0
     * @since 1.0.4 Records the network-level rotation-configured marker when
     *              the persisted settings touch the rotation group.
     * @since 1.0.5 Now enforces the same Developer Sandbox coherence check
     *              the site-level endpoint already enforces, resolved
     *              against the network-effective values, so a network-wide
     *              save can no longer leave every site's sandbox in an
     *              incoherent (redirect mode, no safe address) state.
     *
     * @param WP_REST_Request $request The API request.
     *
     * @return WP_REST_Response|WP_Error
     */
    public function update_settings( WP_REST_Request $request ) {
        $body = $request->get_json_params();

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

        $settings = isset( $body['settings'] ) && is_array( $body['settings'] ) ? $body['settings'] : array();
        $locks    = isset( $body['locks'] ) && is_array( $body['locks'] ) ? $body['locks'] : array();

        $whitelist = $this->build_field_whitelist( 'network' );

        $validated_settings = $this->validate_against_schema( $settings, $whitelist );
        if ( is_wp_error( $validated_settings ) ) {
            return $validated_settings;
        }

        $validated_locks = $this->validate_locks_against_schema( $locks, $whitelist );
        if ( is_wp_error( $validated_locks ) ) {
            return $validated_locks;
        }

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

        // Network-effective resolution differs from site-effective: there is
        // no site-override layer to consult, only constant → stored network
        // value → plugin default (see get_network_metadata()).
        $sandbox_effective = array(
            'enabled'      => Options::init()->get_network_metadata( 'sandbox', 'enabled' )['value'],
            'mode'         => Options::init()->get_network_metadata( 'sandbox', 'mode' )['value'],
            'safe_address' => Options::init()->get_network_metadata( 'sandbox', 'safe_address' )['value'],
        );

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

        // Fire the same pre-save filter as the single-site SettingsController so
        // Pro's SettingsLock (and any other add-on hooking the filter) can
        // strip locked groups before persistence on network-level saves too.
        $validated_settings = apply_filters( 'nimblymail_settings_pre_save', $validated_settings, $request );

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

        $options = Options::init();

        if ( ! empty( $validated_settings ) ) {
            $options->set( $validated_settings, 'network' );
            Rotation::maybe_mark_network_configured( $validated_settings );
        }

        if ( ! empty( $validated_locks ) ) {
            $options->set_locks( $validated_locks );
        }

        $response_data = array(
            'message' => __( 'Network settings saved successfully.', 'nimblymail-smtp' ),
            'tabs'    => SettingsProvider::get_instance()->get_tabs( 'network' ),
            'locks'   => $options->get_locks(),
        );

        // Same response filter the single-site endpoint fires so add-ons can
        // attach warnings (e.g. SettingsLock reports stripped Pro groups) and
        // override the success message.
        $filtered = apply_filters( 'nimblymail_settings_save_response', $response_data, $validated_settings, $request );
        if ( is_array( $filtered ) ) {
            $response_data = $filtered;
        }

        return new WP_REST_Response( $response_data );
    }

    /**
     * Validate the locks payload against the schema whitelist.
     *
     * Lock entries for fields defined by PHP constants are silently dropped
     * (constants are already effectively locked). Unknown groups or fields
     * fail the whole request with a 400.
     *
     * @since 1.0.0
     *
     * @param array<string, array<string, mixed>>                         $data   Incoming locks data.
     * @param array<string, array{fields: string[], encrypted: string[]}> $schema Whitelist schema.
     *
     * @return array<string, array<string, bool>>|WP_Error Validated locks or error.
     */
    private function validate_locks_against_schema( array $data, array $schema ) {
        $validated = array();

        foreach ( $data as $group => $fields ) {
            if ( ! is_array( $fields ) ) {
                continue;
            }

            if ( ! isset( $schema[ $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( (string) $group )
                    ),
                    array( 'status' => 400 )
                );
            }

            $valid_fields = $schema[ $group ]['fields'];

            foreach ( $fields as $field_id => $locked ) {
                if ( ! in_array( $field_id, $valid_fields, true ) ) {
                    return new WP_Error(
                        'rest_unknown_field',
                        sprintf(
                            /* translators: %1$s: the field ID, %2$s: the settings group name. */
                            __( 'Unknown field "%1$s" in group "%2$s".', 'nimblymail-smtp' ),
                            sanitize_text_field( (string) $field_id ),
                            sanitize_text_field( (string) $group )
                        ),
                        array( 'status' => 400 )
                    );
                }

                if ( Options::init()->is_const_defined( $group, $field_id ) ) {
                    continue;
                }

                if ( ! isset( $validated[ $group ] ) ) {
                    $validated[ $group ] = array();
                }

                $validated[ $group ][ $field_id ] = (bool) $locked;
            }
        }

        return $validated;
    }
}
