<?php

/**
 * Controlador REST de los ajustes fiscales del panel + el configurador de
 * IVA español en un clic.
 *
 * @package Entrepo
 */
namespace Entrepo\Rest;

use Entrepo\Core\Plan;
use Entrepo\Fiscal\ConfiguradorIva;
use Entrepo\Fiscal\NifValidator;
use Entrepo\WooCommerce\VisibilidadHooks;
defined( 'ABSPATH' ) || exit;
/**
 * Recurso REST `ajustes` (`entrepo/v1`).
 *
 * Sigue el patrón-casa: `registrar_rutas()` declara las rutas y `puede()` es
 * el `permission_callback` común (capability, nunca nonce a mano).
 */
final class AjustesController {
    /** Nombre de la option donde vive `entrepo_ajustes_fiscales` (autoload no: se lee poco, solo en el panel y en `ViesClient`/`FiscalHooks`). */
    private const OPTION = 'entrepo_ajustes_fiscales';

    /** Declara las rutas del recurso bajo `entrepo/v1`. */
    public function registrar_rutas() : void {
        register_rest_route( 'entrepo/v1', '/ajustes', array(array(
            'methods'             => 'GET',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'obtener_endpoint'),
        ), array(
            'methods'             => 'POST',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'guardar_endpoint'),
        )) );
        register_rest_route( 'entrepo/v1', '/ajustes/configurar-iva', array(
            'methods'             => 'POST',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'configurar_iva_endpoint'),
        ) );
        // Endpoint PROPIO (no dentro del POST /ajustes general): la
        // visibilidad B2B (F4-1) vive en su propia option
        // (`entrepo_ajustes_visibilidad`, ver `VisibilidadHooks`), separada de
        // `entrepo_ajustes_fiscales` — igual que `configurar-iva` ya es una
        // sub-ruta propia del mismo controlador para una acción/ajuste con su
        // propio contrato, en vez de sobrecargar el POST general con un tercer
        // payload sin relación fiscal.
        register_rest_route( 'entrepo/v1', '/ajustes/visibilidad', array(
            'methods'             => 'POST',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'guardar_visibilidad_endpoint'),
        ) );
    }

    /**
     * Gate de TODOS los endpoints del recurso: los ajustes fiscales (NIF de
     * la tienda, interruptores de reverse charge/recargo) y el configurador
     * de impuestos son configuración comercial de la tienda, no pública.
     *
     * @return bool
     */
    public function puede() : bool {
        return current_user_can( 'manage_woocommerce' );
    }

    /**
     * Ajustes guardados + diagnóstico del IVA español, para pintar la vista
     * completa con una sola petición.
     *
     * @param \WP_REST_Request $req Petición (sin parámetros; la firma la exige el callback REST).
     * @return \WP_REST_Response `{ajustes, diagnostico, aviso_sin_nif_tienda, visibilidad}`.
     */
    public function obtener_endpoint( \WP_REST_Request $req ) : \WP_REST_Response {
        // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Firma del callback REST.
        $ajustes = self::opciones_actuales();
        return new \WP_REST_Response(array(
            'ajustes'              => $ajustes,
            'diagnostico'          => ConfiguradorIva::diagnostico(),
            'aviso_sin_nif_tienda' => self::aviso_sin_nif_tienda( $ajustes ),
            'visibilidad'          => VisibilidadHooks::modo_actual(),
        ), 200);
    }

    /**
     * Guarda los ajustes fiscales saneados.
     *
     * `nif_tienda` se guarda normalizado (mayúsculas, sin espacios/guiones/
     * puntos — misma limpieza que `NifValidator::normalizar()`, pero SIN
     * quitar el prefijo de país: a diferencia del NIF de un cliente, el de la
     * propia tienda se muestra tal cual en el panel, y `ViesClient` ya lo
     * vuelve a normalizar en el momento de usarlo) y se valida de FORMATO (no
     * VIES: la tienda no se valida a sí misma contra el servicio, solo se
     * comprueba que el NIF que va a viajar como `requester` en `ViesClient`
     * tenga una forma correcta para el país base de la tienda —
     * `woocommerce_default_country`). Un NIF vacío es válido (el requester es
     * opcional, ver `ViesClient::datos_requester()`); solo se valida si el
     * admin ha escrito algo.
     *
     * Gate Pro (fix IMPORTANT, lote de revisión de rama): activar
     * `reverse_charge_activo` o `recargo_activo` (ponerlos a `true`) exige
     * `Plan::es_pro()` — sin este guard, una instalación free podía guardar
     * cualquiera de los dos toggles igualmente, aunque `FiscalHooks` ya exija
     * `Plan::es_pro()` internamente para aplicar cualquier efecto real
     * (`calcular_condiciones()`/`calcular_recargo()`): la option quedaría en
     * un estado "activado" que nunca surte efecto, una configuración muerta.
     * La sección de VISIBILIDAD (modo, F4-1) sigue FREE e intacta —
     * `guardar_visibilidad_endpoint()` no toca este gate, es una option
     * distinta. `false` (o ausente) en ambos toggles nunca dispara el gate:
     * apagar un régimen fiscal, o guardar solo `nif_tienda`, siempre es libre.
     *
     * @param \WP_REST_Request $req Petición con `nif_tienda`, `reverse_charge_activo`, `recargo_activo`.
     * @return \WP_REST_Response|\WP_Error Los ajustes guardados + diagnóstico (200), `WP_Error` 400 si `nif_tienda` no tiene un formato válido, o 403 `entrepo_requiere_pro` si se intenta activar `reverse_charge_activo`/`recargo_activo` sin plan Pro.
     */
    public function guardar_endpoint( \WP_REST_Request $req ) {
        $nif_tienda_bruto = sanitize_text_field( (string) $req->get_param( 'nif_tienda' ) );
        $nif_tienda = strtoupper( (string) preg_replace( '/[\\s\\-.]+/', '', trim( $nif_tienda_bruto ) ) );
        // Fix MINOR (lote de revisión de rama): `rest_sanitize_boolean()` en
        // vez de un cast `(bool)` a mano, por consistencia con
        // `ClientesController::aprobar_endpoint()` (mismo saneo que usa el
        // propio core de WP-REST para parámetros `boolean`: acepta
        // `true`/`false`/`"1"`/`"0"`/etc., no solo valores ya-boolean).
        $reverse_charge_activo = rest_sanitize_boolean( self::escalar_para_rest_bool( $req->get_param( 'reverse_charge_activo' ) ) );
        $recargo_activo = rest_sanitize_boolean( self::escalar_para_rest_bool( $req->get_param( 'recargo_activo' ) ) );
        $ajustes = array(
            'nif_tienda'            => $nif_tienda,
            'reverse_charge_activo' => $reverse_charge_activo,
            'recargo_activo'        => $recargo_activo,
        );
        update_option( self::OPTION, $ajustes, false );
        return new \WP_REST_Response(array(
            'ajustes'              => $ajustes,
            'diagnostico'          => ConfiguradorIva::diagnostico(),
            'aviso_sin_nif_tienda' => self::aviso_sin_nif_tienda( $ajustes ),
        ), 200);
    }

    /**
     * Ejecuta el configurador de IVA español en un clic
     * (`ConfiguradorIva::configurar_espana()`). La UI ya pidió confirmación
     * explícita antes de llamar a este endpoint (ver `Ajustes.js`) — aquí no
     * hay una segunda confirmación de servidor porque la operación es
     * idempotente (una segunda llamada por error no duplica nada).
     *
     * @param \WP_REST_Request $req Petición (sin parámetros; la firma la exige el callback REST).
     * @return \WP_REST_Response `{creadas, existentes, calc_taxes_activado}`.
     */
    public function configurar_iva_endpoint( \WP_REST_Request $req ) : \WP_REST_Response {
        // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Firma del callback REST.
        return new \WP_REST_Response(ConfiguradorIva::configurar_espana(), 200);
    }

    /**
     * Guarda el modo de visibilidad B2B (F4-1) en su propia option
     * (`VisibilidadHooks::OPTION`), saneado con la allowlist de
     * `VisibilidadHooks::MODOS_VALIDOS` — un modo que no sea exactamente uno
     * de los 3 conocidos es un `WP_Error` 400, NADA se guarda.
     *
     * @param \WP_REST_Request $req Petición con `modo`.
     * @return \WP_REST_Response|\WP_Error `{visibilidad}` (200), o `WP_Error` 400 si `modo` no es uno de los 3 válidos.
     */
    public function guardar_visibilidad_endpoint( \WP_REST_Request $req ) {
        $modo = sanitize_text_field( (string) $req->get_param( 'modo' ) );
        if ( !in_array( $modo, VisibilidadHooks::MODOS_VALIDOS, true ) ) {
            return new \WP_Error('entrepo_modo_visibilidad_invalido', __( 'The visibility mode is not valid.', 'entrepo' ), array(
                'status' => 400,
            ));
        }
        update_option( VisibilidadHooks::OPTION, array(
            'modo' => $modo,
        ), false );
        return new \WP_REST_Response(array(
            'visibilidad' => $modo,
        ), 200);
    }

    /**
     * Ajustes guardados con sus valores por defecto (option todavía sin
     * crear, o con una forma inesperada) — nunca un WP_Error ni un aviso, la
     * primera visita a la vista Ajustes debe pintar el formulario vacío.
     *
     * @return array{nif_tienda:string,reverse_charge_activo:bool,recargo_activo:bool}
     */
    private static function opciones_actuales() : array {
        $guardado = get_option( self::OPTION, array() );
        if ( !is_array( $guardado ) ) {
            $guardado = array();
        }
        return array(
            'nif_tienda'            => (string) ($guardado['nif_tienda'] ?? ''),
            'reverse_charge_activo' => (bool) ($guardado['reverse_charge_activo'] ?? false),
            'recargo_activo'        => (bool) ($guardado['recargo_activo'] ?? false),
        );
    }

    /**
     * Fix IMPORTANT (lote de revisión de rama): `true` si el reverse charge
     * está activo pero la tienda no tiene su propio NIF-IVA configurado. Sin
     * `nif_tienda`, `ViesClient` no manda `requester` en la consulta VIES
     * (`datos_requester()` lo omite si está vacío), así que las validaciones
     * que sí resultan válidas llegan SIN `requestIdentifier` — es decir, sin
     * la prueba de consulta que exige la normativa ante inspección para
     * sostener la exención. Es solo informativo: no bloquea el toggle, esa
     * decisión sigue siendo del admin (ver `Ajustes.js`).
     *
     * @param array{nif_tienda:string,reverse_charge_activo:bool,recargo_activo:bool} $ajustes Ajustes ya saneados/leídos (nunca el crudo del `$_POST`).
     * @return bool
     */
    private static function aviso_sin_nif_tienda( array $ajustes ) : bool {
        return !empty( $ajustes['reverse_charge_activo'] ) && '' === $ajustes['nif_tienda'];
    }

    /**
     * Adapta el valor crudo de un parámetro REST para `rest_sanitize_boolean()`
     * (PHPStan / F5-Task 3): `WP_REST_Request::get_param()` devuelve `mixed`,
     * pero el stub de WordPress declara `rest_sanitize_boolean()` con una
     * plantilla `bool|string|int` — PHPStan no puede resolverla contra
     * `mixed`. El propio cuerpo de `rest_sanitize_boolean()` SOLO trata de
     * forma especial los valores `bool`/`string` (parsea `"false"`, `"0"`,
     * etc.); para cualquier OTRO tipo (incluido `null` si el parámetro no
     * vino) hace un `(bool)` cast plano — así que pre-castear aquí a `bool`
     * cualquier valor que NO sea ya `bool`/`string` no cambia el resultado en
     * ningún caso, solo estrecha el tipo para el analizador.
     *
     * @param mixed $valor Valor crudo de `WP_REST_Request::get_param()`.
     * @return bool|string
     */
    private static function escalar_para_rest_bool( $valor ) {
        return ( is_string( $valor ) || is_bool( $valor ) ? $valor : (bool) $valor );
    }

}
