<?php

/**
 * Controlador REST de clientes B2B pendientes de aprobación.
 *
 * @package Entrepo
 */
namespace Entrepo\Rest;

use Entrepo\Core\Plan;
use Entrepo\Cuenta\RegistroB2B;
use Entrepo\Fiscal\ViesJob;
use Entrepo\Grupos\GrupoRepository;
defined( 'ABSPATH' ) || exit;
/**
 * Recurso REST `clientes/pendientes` (`entrepo/v1`).
 *
 * La búsqueda ligera de clientes (`GET /clientes/buscar`, el typeahead del
 * Simulador) ya vive en `SimuladorController` desde F3-5 y se queda ahí: es
 * un recurso distinto (cualquier cliente, no solo pendientes) que nació junto
 * al simulador y moverlo aquí solo por compartir el prefijo `clientes/` no
 * aporta nada — se documenta la decisión, no se repite el endpoint.
 *
 * Sigue el patrón-casa (ver `GruposController`): `registrar_rutas()` declara
 * las rutas y `puede()` es el `permission_callback` común (capability, nunca
 * nonce a mano).
 */
final class ClientesController {
    /** Declara las rutas del recurso bajo `entrepo/v1`. */
    public function registrar_rutas() : void {
        register_rest_route( 'entrepo/v1', '/clientes/pendientes', array(
            'methods'             => 'GET',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'pendientes_endpoint'),
        ) );
        register_rest_route( 'entrepo/v1', '/clientes/(?P<id>\\d+)/aprobar', array(
            'methods'             => 'POST',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'aprobar_endpoint'),
        ) );
        register_rest_route( 'entrepo/v1', '/clientes/(?P<id>\\d+)/vies', array(
            'methods'             => 'POST',
            'permission_callback' => array($this, 'puede'),
            'callback'            => array($this, 'vies_endpoint'),
        ) );
    }

    /**
     * Gate de todos los endpoints del recurso: los datos B2B (razón social,
     * NIF/IVA) y la aprobación de clientes son información comercial de la
     * tienda, no pública.
     *
     * @return bool
     */
    public function puede() : bool {
        return current_user_can( 'manage_woocommerce' );
    }

    /**
     * Listado de clientes B2B pendientes de aprobación para la tabla
     * DataViews del panel.
     *
     * @param \WP_REST_Request $req Petición (sin parámetros todavía; la firma la exige el callback REST).
     * @return \WP_REST_Response `{items, total, pages}` + cabeceras de paginación.
     */
    public function pendientes_endpoint( \WP_REST_Request $req ) : \WP_REST_Response {
        // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Firma del callback REST; se usará cuando el listado pagine/filtre.
        $items = ( new RegistroB2B() )->pendientes();
        $total = count( $items );
        $data = array(
            'items' => $items,
            'total' => $total,
            'pages' => ( $total > 0 ? 1 : 0 ),
        );
        $response = new \WP_REST_Response($data, 200);
        $response->header( 'X-WP-Total', (string) $total );
        $response->header( 'X-WP-TotalPages', (string) $data['pages'] );
        return $response;
    }

    /**
     * Aprueba a un cliente pendiente asignándole un grupo.
     *
     * `RegistroB2B::aprobar()` ya es idempotente (ver su docblock): no
     * reasigna grupo a un cliente que ya no está pendiente. Aquí eso se
     * traduce en `aprobado:false` con status 200 (no es un error de la
     * petición, es un aviso de estado — la UI lo trata como advertencia, no
     * como fallo), reservando `WP_Error` para los dos casos que sí son un
     * error del cliente de la API: usuario inexistente (404) y grupo
     * inválido (400).
     *
     * `recargo` (F2-6, opcional en el body — ausente equivale a `false`) es el
     * mismo juicio del admin que documenta `RegistroB2B::aprobar()`: se sanea
     * con `rest_sanitize_boolean()` (acepta `true`/`false`/`"1"`/`"0"`/etc., el
     * mismo saneo que usa el propio core de WP-REST para parámetros `boolean`)
     * y se pasa tal cual, sin validar aquí que el cliente sea de país ES — esa
     * responsabilidad es de la UI (ver `RegistroB2B::aprobar()`).
     *
     * Gate Pro (fix CRITICAL, lote de revisión de rama): `recargo=true` exige
     * `Plan::es_pro()`. La suite fiscal completa (reverse charge, VIES,
     * recargo) sigue siendo Pro — es el modelo de negocio, ver `Plan::MATRIZ`
     * — y `FiscalHooks::calcular_recargo()` ya exige `Plan::es_pro()` para
     * aplicar NINGÚN efecto en el carrito/checkout. Sin este gate aquí, una
     * instalación free podía persistir la meta `entrepo_recargo` igualmente
     * (el checkbox de `Clientes.js` no tenía guard propio) y quedarse con una
     * configuración MUERTA — un admin que cree honestamente haber activado el
     * recargo de un cliente, sin que nunca surta efecto y sin ningún aviso.
     * Eso es justo lo que «free que sirve de verdad» prohíbe: nunca en
     * silencio. Por eso, en vez de aceptar la petición y persistir un flag
     * inerte, `!Plan::es_pro()` con `recargo=true` es un `WP_Error` 403 — la
     * misma respuesta que ya usa `vies_endpoint()` para su propio gate Pro
     * (ver su docblock). `recargo=false` (o ausente) nunca lo dispara: no hay
     * nada Pro que gatear en una aprobación sin recargo.
     *
     * @param \WP_REST_Request $req Petición con el parámetro de ruta `id` y el body `grupo`, `recargo`.
     * @return \WP_REST_Response|\WP_Error `{aprobado:bool}` (200), `WP_Error` 404 si el usuario no existe, 400 si `grupo` no es un grupo gestionado por `GrupoRepository`, o 403 `entrepo_requiere_pro` si `recargo=true` y `!Plan::es_pro()`.
     */
    public function aprobar_endpoint( \WP_REST_Request $req ) {
        $user_id = (int) $req->get_param( 'id' );
        if ( !get_userdata( $user_id ) ) {
            return new \WP_Error('entrepo_usuario_no_encontrado', __( 'The user does not exist.', 'entrepo' ), array(
                'status' => 404,
            ));
        }
        $grupo = sanitize_key( (string) $req->get_param( 'grupo' ) );
        if ( !( new GrupoRepository() )->es_grupo( $grupo ) ) {
            return new \WP_Error('entrepo_grupo_invalido', __( 'The specified group is not valid.', 'entrepo' ), array(
                'status' => 400,
            ));
        }
        $recargo = rest_sanitize_boolean( self::escalar_para_rest_bool( $req->get_param( 'recargo' ) ) );
        $aprobado = ( new RegistroB2B() )->aprobar( $user_id, $grupo, $recargo );
        return new \WP_REST_Response(array(
            'aprobado' => $aprobado,
        ), 200);
    }

    /**
     * Revalida a mano el NIF-IVA de un cliente contra VIES (acción "Revalidar
     * VIES" del panel): vuelve a encolar la validación desde el intento 1,
     * igual que el disparo automático de `RegistroB2B::al_registrar()` —
     * útil tanto para un estado `error` (reintentos agotados) como para
     * refrescar un `valido`/`invalido` que pudo cambiar en VIES desde
     * entonces.
     *
     * `ViesJob::encolar()` devuelve `false` cuando ya hay una cadena viva
     * para ese cliente (guard anti-duplicado, ver su docblock) — eso se
     * propaga tal cual en `encolado`. NO es un error de la petición (sigue
     * siendo un 200): es un aviso de que ya hay una validación en curso, y el
     * panel lo muestra como tal en vez de encolar una segunda cadena en
     * paralelo.
     *
     * Gate Pro (F4-2, defensa en profundidad — ver `Plan::MATRIZ`): revalidar
     * VIES es una feature Pro (aunque `ViesJob::encolar()` ya trae su propio
     * guard `Plan::es_pro()` internamente y devolvería `false` igualmente,
     * este endpoint responde 403 en vez de un `{encolado:false}` engañoso —
     * son dos motivos distintos de "no se encoló" y el panel necesita
     * distinguirlos).
     *
     * @param \WP_REST_Request $req Petición con el parámetro de ruta `id`.
     * @return \WP_REST_Response|\WP_Error `{encolado:bool}` (200; `false` si ya había una validación en curso), `WP_Error` 403 `entrepo_requiere_pro` si `!Plan::es_pro()`, o 404 si el usuario no existe.
     */
    public function vies_endpoint( \WP_REST_Request $req ) {
        $user_id = (int) $req->get_param( 'id' );
        if ( !get_userdata( $user_id ) ) {
            return new \WP_Error('entrepo_usuario_no_encontrado', __( 'The user does not exist.', 'entrepo' ), array(
                'status' => 404,
            ));
        }
        $encolado = false;
        return new \WP_REST_Response(array(
            'encolado' => $encolado,
        ), 200);
    }

    /**
     * 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 );
    }

}
