<?php

/**
 * Registro de empresa B2B + aprobación por un admin.
 *
 * Un cliente que se registra como empresa (checkbox/formulario con los 3
 * campos B2B) entra en el rol `ROL_PENDIENTE` en vez de `customer`. Un admin
 * lo aprueba desde la lista de Usuarios de WP asignándole un grupo de
 * `GrupoRepository` — a partir de ahí el resto del plugin (precios por rol,
 * `PrecioHooks`) lo trata como cualquier miembro de ese grupo, sin cambios.
 *
 * No hay página de admin propia en esta fase: la aprobación vive en la lista
 * nativa de Usuarios de WordPress (columna + acción por fila), lo mínimo que
 * cierra el flujo sin construir UI nueva.
 *
 * @package Entrepo
 */
namespace Entrepo\Cuenta;

use Entrepo\Fiscal\NifValidator;
use Entrepo\Fiscal\ViesJob;
use Entrepo\Grupos\GrupoRepository;
defined( 'ABSPATH' ) || exit;
/**
 * Registro de empresa B2B y su aprobación por un admin.
 *
 * Gestiona el ciclo completo: pinta los 3 campos B2B en registro/checkout,
 * asigna `ROL_PENDIENTE` al completarlo, añade la columna + acción de
 * aprobación en la lista de Usuarios de WP, y el handler que reasigna al
 * usuario aprobado al grupo (`GrupoRepository`) elegido por el admin.
 */
final class RegistroB2B {
    /** Rol asignado al registrarse como empresa, hasta que un admin aprueba. */
    public const ROL_PENDIENTE = 'entrepo_pendiente';

    /** Nombre del campo del nonce en el formulario de registro/checkout. */
    private const CAMPO_NONCE_REGISTRO = 'entrepo_registro_nonce';

    /** Acción del nonce del formulario de registro/checkout. */
    private const ACCION_NONCE_REGISTRO = 'entrepo_registro';

    /** Acción base del nonce del handler de aprobación (se sufija con user_id + grupo). */
    private const ACCION_NONCE_APROBAR = 'entrepo_aprobar_usuario';

    /** Meta donde se guarda la fecha (`current_time('mysql')`) de aprobación. */
    private const META_APROBADO_EN = 'entrepo_aprobado_en';

    /** Slug de la columna añadida a la lista de Usuarios de WP. */
    private const COLUMNA_USUARIOS = 'entrepo_b2b';

    /**
     * Definición de los 3 campos B2B. La clave de cada campo ES el nombre del
     * `<input>`/`<select>` en el formulario Y la user meta donde se guarda —
     * así el pintado, el guardado y el verificador comparten un único nombre.
     *
     * @return array<string,array{label:string,type:string,required:bool,options?:array<string,string>}>
     */
    public function campos() : array {
        return array(
            'entrepo_razon_social' => array(
                'label'    => __( 'Company name', 'entrepo' ),
                'type'     => 'text',
                'required' => true,
            ),
            'entrepo_nif_iva'      => array(
                'label'    => __( 'Tax ID / VAT', 'entrepo' ),
                'type'     => 'text',
                'required' => true,
            ),
            'entrepo_pais'         => array(
                'label'    => __( 'Country', 'entrepo' ),
                'type'     => 'select',
                'required' => true,
                'options'  => ( function_exists( 'WC' ) ? WC()->countries->get_countries() : array() ),
            ),
        );
    }

    /**
     * Engancha el registro/pintado/guardado y la aprobación desde Usuarios.
     */
    public function register() : void {
        self::crear_rol_pendiente();
        add_action( 'woocommerce_register_form', array($this, 'pintar_campos') );
        add_action( 'woocommerce_after_checkout_billing_form', array($this, 'pintar_campos') );
        add_action( 'woocommerce_created_customer', array($this, 'al_registrar') );
        add_filter( 'manage_users_columns', array($this, 'columna_estado') );
        add_filter(
            'manage_users_custom_column',
            array($this, 'columna_estado_valor'),
            10,
            3
        );
        add_filter(
            'user_row_actions',
            array($this, 'acciones_fila'),
            10,
            2
        );
        add_action( 'admin_post_entrepo_aprobar', array($this, 'manejar_aprobacion_admin') );
    }

    /**
     * Crea el rol `ROL_PENDIENTE` si todavía no existe (idempotente: `add_role()`
     * es no-op si el rol ya está registrado). Se llama tanto en `Activator::activate()`
     * como en `register()`, por si el plugin ya estaba activo cuando se despliega
     * esta versión y nunca pasó por la activación.
     *
     * Mismas capabilities que `customer` (si WooCommerce todavía no las registró,
     * cae a solo `read`) — un pendiente puede iniciar sesión y ver su cuenta, pero
     * no tiene ningún privilegio adicional hasta ser aprobado.
     */
    public static function crear_rol_pendiente() : void {
        if ( null !== get_role( self::ROL_PENDIENTE ) ) {
            return;
        }
        $rol_cliente = get_role( 'customer' );
        $capacidades = ( $rol_cliente ? $rol_cliente->capabilities : array(
            'read' => true,
        ) );
        add_role( self::ROL_PENDIENTE, __( 'Pending B2B approval', 'entrepo' ), $capacidades );
    }

    /**
     * Acción de nonce para el enlace "Aprobar como <grupo>" de un usuario
     * concreto. Incluye el `$user_id` Y el `$role_grupo` a propósito: así un
     * enlace firmado para aprobar al usuario X como el grupo A no sirve para
     * aprobarlo como el grupo B con solo cambiar el parámetro `grupo` de la URL.
     *
     * @param int    $user_id    ID del usuario a aprobar.
     * @param string $role_grupo Slug del rol/grupo (`GrupoRepository`) al que se le aprobaría.
     * @return string Acción única del nonce para ese par (usuario, grupo).
     */
    private static function accion_nonce_aprobar( int $user_id, string $role_grupo ) : string {
        return self::ACCION_NONCE_APROBAR . '_' . $user_id . '_' . $role_grupo;
    }

    /**
     * Callback de `woocommerce_register_form` / `woocommerce_after_checkout_billing_form`.
     * Imprime los 3 campos B2B + el nonce del formulario. Escapa toda la salida.
     */
    public function pintar_campos() : void {
        wp_nonce_field( self::ACCION_NONCE_REGISTRO, self::CAMPO_NONCE_REGISTRO );
        foreach ( $this->campos() as $meta_key => $campo ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- solo se relee para repintar el valor tras un intento fallido; no se actua sobre el.
            $valor_enviado = ( isset( $_POST[$meta_key] ) ? sanitize_text_field( wp_unslash( $_POST[$meta_key] ) ) : '' );
            echo '<p class="form-row form-row-wide entrepo-campo-b2b">';
            echo '<label for="' . esc_attr( $meta_key ) . '">' . esc_html( $campo['label'] );
            if ( !empty( $campo['required'] ) ) {
                echo '&nbsp;<span class="required">*</span>';
            }
            echo '</label>';
            if ( 'select' === $campo['type'] ) {
                echo '<select name="' . esc_attr( $meta_key ) . '" id="' . esc_attr( $meta_key ) . '" class="woocommerce-input woocommerce-input-select">';
                echo '<option value="">' . esc_html__( 'Select a country&hellip;', 'entrepo' ) . '</option>';
                foreach ( (array) ($campo['options'] ?? array()) as $codigo => $nombre_pais ) {
                    echo '<option value="' . esc_attr( $codigo ) . '" ' . selected( $valor_enviado, $codigo, false ) . '>' . esc_html( $nombre_pais ) . '</option>';
                }
                echo '</select>';
            } else {
                echo '<input type="text" class="woocommerce-input woocommerce-input-text input-text" name="' . esc_attr( $meta_key ) . '" id="' . esc_attr( $meta_key ) . '" value="' . esc_attr( $valor_enviado ) . '" />';
            }
            echo '</p>';
        }
    }

    /**
     * Callback de `woocommerce_created_customer`.
     *
     * Solo actúa si el nonce del formulario es válido Y todos los campos B2B
     * requeridos vienen no vacíos en `$_POST` Y `entrepo_pais` (si viene) es
     * una clave válida de `WC()->countries->get_countries()` (una omisión de
     * cualquiera de esas condiciones deja al usuario como `customer` normal,
     * sin tocar meta ni rol). Guarda la meta saneada con `sanitize_text_field`
     * y asigna `ROL_PENDIENTE` en lugar de `customer`.
     *
     * @param int $user_id ID del usuario recién creado por WooCommerce.
     */
    public function al_registrar( int $user_id ) : void {
        if ( !isset( $_POST[self::CAMPO_NONCE_REGISTRO] ) ) {
            return;
        }
        $nonce = sanitize_text_field( wp_unslash( $_POST[self::CAMPO_NONCE_REGISTRO] ) );
        if ( !wp_verify_nonce( $nonce, self::ACCION_NONCE_REGISTRO ) ) {
            return;
        }
        $campos = $this->campos();
        foreach ( $campos as $meta_key => $campo ) {
            $valor = ( isset( $_POST[$meta_key] ) ? trim( sanitize_text_field( wp_unslash( $_POST[$meta_key] ) ) ) : '' );
            if ( !empty( $campo['required'] ) && '' === $valor ) {
                return;
                // Campo requerido ausente/vacio: no es un registro B2B completo, no se toca nada.
            }
            // Campos `select` (hoy solo `entrepo_pais`) solo aceptan un valor
            // que sea clave de su propio allowlist de opciones (aqui, la lista
            // de paises de WooCommerce). Sin este guard, un POST directo que
            // se salte el <select> del formulario guardaria cualquier string
            // en `entrepo_pais`, y F2 (IVA UE) necesita que sea una clave ISO
            // valida. Un valor fuera del allowlist trata el registro como
            // INCOMPLETO, igual que un campo requerido vacio: no se guarda
            // meta ni se asigna ROL_PENDIENTE, el usuario queda `customer`.
            if ( 'select' === $campo['type'] && !isset( $campo['options'][$valor] ) ) {
                return;
            }
        }
        // Deuda tecnica de F1 CERRADA en F2 (NifValidator): un pais UE (o `XI`,
        // Irlanda del Norte) con un `entrepo_nif_iva` que NO casa con el
        // formato oficial de su tabla de VIES trata el registro como
        // INCOMPLETO — mismo tratamiento que el guard de pais de arriba (no se
        // guarda meta, no se asigna ROL_PENDIENTE, el usuario queda
        // `customer`). Los paises NO-UE no se bloquean aqui a proposito:
        // `NifValidator::validar()` los deja pasar "sin validar" (Entrepo no
        // debe frenar el registro de clientes extracomunitarios por un campo
        // que no le corresponde comprobar). La validacion VIES REAL (llamada
        // al servicio, no solo formato) es trabajo de las siguientes tareas de
        // F2 (`ViesClient`/`ViesJob`) y corre DESPUES de este guard, en cola.
        $pais_valor = ( isset( $_POST['entrepo_pais'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['entrepo_pais'] ) ) ) : '' );
        $nif_valor = ( isset( $_POST['entrepo_nif_iva'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['entrepo_nif_iva'] ) ) ) : '' );
        foreach ( $campos as $meta_key => $campo ) {
            $valor = ( isset( $_POST[$meta_key] ) ? sanitize_text_field( wp_unslash( $_POST[$meta_key] ) ) : '' );
            update_user_meta( $user_id, $meta_key, $valor );
        }
        $user = new \WP_User($user_id);
        $user->set_role( self::ROL_PENDIENTE );
    }

    /**
     * `true` si el usuario tiene el rol `ROL_PENDIENTE` (registro B2B sin
     * aprobar todavía).
     *
     * @param int $user_id ID del usuario a comprobar.
     * @return bool
     */
    public function esta_pendiente( int $user_id ) : bool {
        $user = get_userdata( $user_id );
        return $user instanceof \WP_User && in_array( self::ROL_PENDIENTE, (array) $user->roles, true );
    }

    /**
     * Todos los usuarios en `ROL_PENDIENTE` (registros B2B sin aprobar
     * todavía), con sus 3 metas B2B + email + fecha de registro + el estado
     * VIES (F2-3) — la fuente de datos de la vista de Clientes del panel
     * (F3-6).
     *
     * El estado `vies` viaja siempre como sub-array `{estado,fecha,nombre}`
     * aunque el cliente todavía no tenga ninguna meta `entrepo_vies_*` (p.
     * ej. un registro creado antes de F2, o uno cuyo job de encolado no llegó
     * a correr): `estado` queda en `''`, que el panel pinta como "—", nunca
     * como un error.
     *
     * @return array<int,array{user_id:int,razon_social:string,nif_iva:string,pais:string,email:string,registrado:string,vies:array{estado:string,fecha:string,nombre:string}}>
     */
    public function pendientes() : array {
        $usuarios = get_users( array(
            'role' => self::ROL_PENDIENTE,
        ) );
        $items = array();
        foreach ( $usuarios as $usuario ) {
            $items[] = array(
                'user_id'      => (int) $usuario->ID,
                'razon_social' => (string) get_user_meta( $usuario->ID, 'entrepo_razon_social', true ),
                'nif_iva'      => (string) get_user_meta( $usuario->ID, 'entrepo_nif_iva', true ),
                'pais'         => (string) get_user_meta( $usuario->ID, 'entrepo_pais', true ),
                'email'        => $usuario->user_email,
                'registrado'   => $usuario->user_registered,
                'vies'         => array(
                    'estado' => (string) get_user_meta( $usuario->ID, 'entrepo_vies_estado', true ),
                    'fecha'  => (string) get_user_meta( $usuario->ID, 'entrepo_vies_fecha', true ),
                    'nombre' => (string) get_user_meta( $usuario->ID, 'entrepo_vies_nombre', true ),
                ),
            );
        }
        return $items;
    }

    /**
     * Aprueba a un usuario pendiente asignándole el grupo `$role_grupo`.
     *
     * `$user->set_role()` reemplaza TODOS los roles actuales por uno solo, así
     * que ya quita `ROL_PENDIENTE` por sí mismo — no hace falta un
     * `remove_role()` aparte.
     *
     * Idempotente: exige que el usuario siga en `ROL_PENDIENTE` en el momento
     * de ejecutarse. El nonce de WP vive ~24h, así que un enlace de aprobación
     * ya usado (pestaña vieja, doble clic, botón atrás) sigue siendo válido
     * para `check_admin_referer()` mucho después de la primera aprobación; sin
     * este guard, una segunda llamada reasignaría en silencio a un usuario YA
     * aprobado a otro grupo y resellaría `entrepo_aprobado_en`. Cambiar el
     * grupo de un cliente ya aprobado es una operación distinta (panel de F3),
     * no esta.
     *
     * `$recargo` (F2-6, opcional y retrocompatible: por defecto `false`, así
     * que ninguna llamada existente a `aprobar()` cambia de comportamiento) es
     * puro JUICIO DEL ADMIN en el momento de aprobar — nunca una
     * auto-declaración del cliente: el admin decide, mirando la razón social y
     * el país del registro, si este cliente es en realidad un minorista
     * persona física acogido al régimen de recargo de equivalencia. Solo tiene
     * sentido para clientes con `entrepo_pais = ES` (el recargo es un régimen
     * exclusivamente español), pero `aprobar()` NO lo valida ni lo bloquea
     * aquí — es responsabilidad de la UI (`Clientes.js`, checkbox visible solo
     * si `pais === 'ES'`) ofrecer la opción solo cuando tiene sentido; el flag
     * en sí es inocuo para un cliente no-ES porque `FiscalHooks::remapear_tax_class()`
     * (F2-6) exige ADEMÁS país ES antes de aplicar ningún efecto. Si `true`,
     * guarda la meta `entrepo_recargo = '1'`; si `false` (el default, y
     * también el caso explícito de "aprobar sin recargo"), la borra — así una
     * aprobación repetida con el flag desmarcado limpia un recargo puesto por
     * error en un intento anterior.
     *
     * @param int    $user_id    ID del usuario pendiente a aprobar.
     * @param string $role_grupo Slug del grupo (`GrupoRepository`) al que se le asigna.
     * @param bool   $recargo    Si `true`, marca al cliente en recargo de equivalencia (meta `entrepo_recargo`).
     * @return bool `false` sin ningún cambio si `$role_grupo` no es un grupo
     *              gestionado por `GrupoRepository` (guard contra asignar
     *              `administrator`, `customer` o cualquier rol nativo de WP)
     *              o si el usuario ya no está pendiente (aprobación repetida).
     */
    public function aprobar( int $user_id, string $role_grupo, bool $recargo = false ) : bool {
        if ( !$this->esta_pendiente( $user_id ) ) {
            return false;
        }
        if ( !( new GrupoRepository() )->es_grupo( $role_grupo ) ) {
            return false;
        }
        $user = get_userdata( $user_id );
        if ( !$user instanceof \WP_User ) {
            return false;
        }
        $user->set_role( $role_grupo );
        update_user_meta( $user_id, self::META_APROBADO_EN, current_time( 'mysql' ) );
        if ( $recargo ) {
            update_user_meta( $user_id, 'entrepo_recargo', '1' );
        } else {
            delete_user_meta( $user_id, 'entrepo_recargo' );
        }
        return true;
    }

    /**
     * Callback de `manage_users_columns`: añade la columna "Entrepo B2B".
     *
     * @param array<string,string> $columnas Columnas actuales de la lista de Usuarios.
     * @return array<string,string>
     */
    public function columna_estado( array $columnas ) : array {
        $columnas[self::COLUMNA_USUARIOS] = __( 'Entrepo B2B', 'entrepo' );
        // Nombre de marca + sigla B2B: no se traduce.
        return $columnas;
    }

    /**
     * Callback de `manage_users_custom_column`: Pendiente / nombre del grupo / "—".
     * Devuelve HTML ya escapado (filtro: se retorna, no se imprime).
     *
     * @param string $salida         Valor por defecto de la columna (vacío para columnas propias).
     * @param string $columna_nombre Slug de la columna que WP está pintando.
     * @param int    $user_id        ID del usuario de la fila.
     * @return string
     */
    public function columna_estado_valor( string $salida, string $columna_nombre, int $user_id ) : string {
        if ( self::COLUMNA_USUARIOS !== $columna_nombre ) {
            return $salida;
        }
        if ( $this->esta_pendiente( $user_id ) ) {
            return '<span class="entrepo-estado-pendiente">' . esc_html__( 'Pending', 'entrepo' ) . '</span>';
        }
        $user = get_userdata( $user_id );
        if ( !$user instanceof \WP_User ) {
            return '&mdash;';
        }
        $grupo_repo = new GrupoRepository();
        foreach ( (array) $user->roles as $role ) {
            $grupo = $grupo_repo->obtener( $role );
            if ( null !== $grupo ) {
                return esc_html( $grupo['nombre'] );
            }
        }
        return '&mdash;';
    }

    /**
     * Callback de `user_row_actions`: para un usuario pendiente, un enlace
     * "Aprobar como <grupo>" por cada grupo existente, hacia el handler
     * `admin_post_entrepo_aprobar` (nonce por usuario + capability gate en el
     * propio handler).
     *
     * @param array<string,string> $acciones Acciones actuales de la fila del usuario.
     * @param \WP_User             $user     Usuario de la fila.
     * @return array<string,string>
     */
    public function acciones_fila( array $acciones, \WP_User $user ) : array {
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            return $acciones;
        }
        if ( !$this->esta_pendiente( $user->ID ) ) {
            return $acciones;
        }
        $grupo_repo = new GrupoRepository();
        foreach ( $grupo_repo->todos() as $role_grupo => $grupo ) {
            $url = wp_nonce_url( add_query_arg( array(
                'action'  => 'entrepo_aprobar',
                'user_id' => $user->ID,
                'grupo'   => $role_grupo,
            ), admin_url( 'admin-post.php' ) ), self::accion_nonce_aprobar( $user->ID, $role_grupo ) );
            $acciones['entrepo_aprobar_' . $role_grupo] = sprintf( 
                '<a href="%1$s">%2$s</a>',
                esc_url( $url ),
                /* translators: %s: B2B group name. */
                esc_html( sprintf( __( 'Approve as %s', 'entrepo' ), $grupo['nombre'] ) )
             );
        }
        return $acciones;
    }

    /**
     * Handler de `admin_post_entrepo_aprobar` (enlace de `acciones_fila()`).
     *
     * Gate de seguridad en dos pasos: `current_user_can('manage_woocommerce')`
     * PRIMERO (para no filtrar nada a quien no puede aprobar), y
     * `check_admin_referer()` con el nonce por (usuario, grupo) después — un
     * GET sin ambos no dispara `aprobar()`, y un enlace firmado para un grupo
     * no vale para otro (ver `accion_nonce_aprobar()`).
     */
    public function manejar_aprobacion_admin() : void {
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html__( 'You do not have permission to perform this action.', 'entrepo' ), 403 );
        }
        // El nonce SÍ se comprueba, tres líneas más abajo. No puede ir antes: la
        // acción del nonce se deriva de estos dos valores, así que hay que leerlos
        // primero. Ambos van saneados y no se usan para nada hasta después del
        // check_admin_referer().
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Ver la nota de arriba.
        $user_id = ( isset( $_GET['user_id'] ) ? absint( $_GET['user_id'] ) : 0 );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Ver la nota de arriba.
        $role_grupo = ( isset( $_GET['grupo'] ) ? sanitize_text_field( wp_unslash( $_GET['grupo'] ) ) : '' );
        check_admin_referer( self::accion_nonce_aprobar( $user_id, $role_grupo ) );
        if ( $user_id > 0 && '' !== $role_grupo ) {
            $this->aprobar( $user_id, $role_grupo );
        }
        $destino = wp_get_referer();
        wp_safe_redirect( ( $destino ? $destino : admin_url( 'users.php' ) ) );
        exit;
    }

}
