<?php
/**
 * Grupos B2B implementados como roles de WordPress.
 *
 * Cada grupo de clientes mayoristas ES un rol nativo de WordPress (prefijo
 * `entrepo_g_`), con las mismas capabilities que `customer` — así el resto
 * del ecosistema (WooCommerce, otros plugins que miran `current_user_can()`
 * o `wp_get_current_user()->roles`) los trata como clientes normales sin
 * cambios. El único metadato propio de Entrepo — el % de descuento global
 * del grupo — no cabe en un rol de WP, así que se guarda aparte en la
 * opción `entrepo_grupos` (role => [ 'nombre', 'descuento_pct' ]), que es
 * la fuente de verdad que consultan `obtener()`, `todos()` y `es_grupo()`.
 *
 * @package Entrepo
 */

namespace Entrepo\Grupos;

use Entrepo\Precios\PrecioRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Repositorio de grupos B2B (mayoristas) implementados como roles de WordPress.
 *
 * CRUD de grupos: crea/actualiza/elimina el rol WP + sus metadatos propios
 * (nombre, % de descuento) en la opción `entrepo_grupos`.
 */
final class GrupoRepository {

	/** Prefijo del slug de rol WP de todo grupo B2B creado por Entrepo. */
	public const PREFIJO = 'entrepo_g_';

	/** Opción donde se guardan los metadatos (nombre, descuento_pct) de cada grupo. */
	private const OPCION = 'entrepo_grupos';

	/**
	 * Crea el grupo: un rol WP nuevo (con las capabilities de `customer`) +
	 * sus metadatos en la opción `entrepo_grupos`.
	 *
	 * @param string $nombre        Nombre visible del grupo.
	 * @param float  $descuento_pct Porcentaje de descuento global del grupo (0-100).
	 * @return string Slug del rol WP creado (p. ej. `entrepo_g_mayorista-a`).
	 */
	public function crear( string $nombre, float $descuento_pct ): string {
		$nombre = sanitize_text_field( $nombre );
		if ( '' === trim( $nombre ) ) {
			$nombre = __( 'B2B group', 'entrepo' ); // Evita un slug que sea solo el prefijo.
		}
		$descuento_pct = self::saturar( $descuento_pct );
		$slug          = $this->slug_disponible( $nombre );

		$rol_cliente = get_role( 'customer' );
		$capacidades = $rol_cliente ? $rol_cliente->capabilities : array();

		add_role( $slug, $nombre, $capacidades );

		$grupos          = self::leer_opcion();
		$grupos[ $slug ] = array(
			'nombre'        => $nombre,
			'descuento_pct' => $descuento_pct,
		);
		self::guardar_opcion( $grupos );

		return $slug;
	}

	/**
	 * Todos los grupos B2B registrados.
	 *
	 * @return array<string,array{nombre:string,descuento_pct:float}>
	 */
	public function todos(): array {
		return self::leer_opcion();
	}

	/**
	 * Metadatos de un grupo, o null si `$role` no es un grupo B2B conocido.
	 *
	 * @param string $role Slug de rol del grupo.
	 * @return array{nombre:string,descuento_pct:float}|null
	 */
	public function obtener( string $role ): ?array {
		$grupos = self::leer_opcion();
		return $grupos[ $role ] ?? null;
	}

	/**
	 * Actualiza nombre y descuento del grupo. Si el rol WP sigue existiendo,
	 * se re-registra con el nuevo nombre (conservando sus capabilities) para
	 * que el nombre mostrado por WordPress no quede desincronizado del que
	 * ve Entrepo. No hace nada si `$role` no es un grupo conocido.
	 *
	 * HUECO LATENTE DE F1 YA CERRADO (F3-3): cambiar `descuento_pct` aquí NO
	 * recalcula por sí sola la matriz precalculada de `entrepo_precios` —
	 * esta clase solo persiste el metadato del grupo, no toca precios. Quien
	 * SÍ cierra el hueco es la capa REST: `GruposController::actualizar()`
	 * encola `RecalcJob::encolar()` justo después de llamar a este método,
	 * así que un cambio de % hecho desde el panel siempre deja el recálculo
	 * masivo del catálogo en marcha en segundo plano. Un `actualizar()`
	 * llamado directamente (fuera de la REST, p. ej. desde un verificador)
	 * sigue sin disparar el recálculo — es responsabilidad de quien la llame
	 * encolarlo si hace falta.
	 *
	 * @param string $role          Slug de rol del grupo a actualizar.
	 * @param string $nombre        Nuevo nombre visible del grupo.
	 * @param float  $descuento_pct Nuevo porcentaje de descuento global (0-100).
	 */
	public function actualizar( string $role, string $nombre, float $descuento_pct ): void {
		$grupos = self::leer_opcion();
		if ( ! isset( $grupos[ $role ] ) ) {
			return;
		}

		$nombre        = sanitize_text_field( $nombre );
		$descuento_pct = self::saturar( $descuento_pct );

		$rol_actual = get_role( $role );
		if ( $rol_actual ) {
			$capacidades = $rol_actual->capabilities;
			remove_role( $role );
			add_role( $role, $nombre, $capacidades );
		}

		$grupos[ $role ] = array(
			'nombre'        => $nombre,
			'descuento_pct' => $descuento_pct,
		);
		self::guardar_opcion( $grupos );
	}

	/**
	 * Elimina el grupo: purga todo su rastro y lo borra.
	 *
	 * Guard imprescindible: solo se borra si es un grupo Entrepo. Sin él, un
	 * `eliminar('customer')` o `eliminar('administrator')` haría `remove_role()`
	 * de un rol de WordPress y dejaría a esos usuarios sin capabilities.
	 *
	 * `remove_role()` de WP NO toca `wp_capabilities` de los usuarios que ya
	 * tienen el rol: quedan con un rol FANTASMA (`get_role($role)` es null,
	 * pero su meta de capabilities sigue teniendo la clave). Si luego se crea
	 * otro grupo con el mismo nombre, `slug_disponible()` (que solo mira
	 * `get_role()`) reutiliza el slug y esos usuarios quedarían reinscritos EN
	 * SILENCIO en el grupo nuevo — con su nuevo % de descuento, heredando
	 * además overrides de precio y filas de `entrepo_precios` que ya no
	 * significan lo que significaban. Por eso, ANTES de `remove_role()`, se
	 * limpia todo lo que podría sobrevivir a la reutilización del slug:
	 *
	 * 1. Usuarios con el rol: se les quita explícitamente (y quien se queda
	 *    sin ningún rol vuelve a `customer`, como un cliente normal), y se
	 *    les borra la meta `entrepo_recargo` (Task 6 / fix IMPORTANT del lote
	 *    de revisión de rama, ver el punto 4 más abajo).
	 * 2. Filas de precio del rol en `entrepo_precios` (huérfanas si no se
	 *    purgan: un grupo nuevo con el mismo slug heredaría precios viejos).
	 * 3. Overrides de precio por producto (`_entrepo_precio_<role>`).
	 * 4. `entrepo_recargo`: el recargo de equivalencia es un régimen ligado a
	 *    pertenecer a un grupo B2B (`FiscalHooks::calcular_recargo()` exige
	 *    grupo desde el mismo fix) — si el grupo desaparece y la meta
	 *    sobreviviera, el usuario quedaría "en recargo" sin grupo, la misma
	 *    asimetría con la exención intracomunitaria que ese fix cierra desde
	 *    el otro lado. Las metas `entrepo_vies_*` (estado, request_id, fecha,
	 *    nombre) se CONSERVAN deliberadamente: son historial/prueba de una
	 *    validación VIES ya hecha, no un privilegio ligado al grupo actual —
	 *    si el cliente vuelve a un grupo B2B más tarde, esa prueba sigue
	 *    siendo válida y no hay motivo fiscal para borrarla.
	 *
	 * @param string $role Slug de rol del grupo a eliminar.
	 */
	public function eliminar( string $role ): void {
		if ( ! $this->es_grupo( $role ) ) {
			return;
		}

		$usuarios = get_users(
			array(
				'role'   => $role,
				'fields' => 'ID',
			)
		);
		foreach ( $usuarios as $user_id ) {
			$user = new \WP_User( $user_id );
			$user->remove_role( $role );
			if ( empty( $user->roles ) ) {
				$user->add_role( 'customer' );
			}
			delete_user_meta( $user_id, 'entrepo_recargo' );
		}

		PrecioRepository::eliminar_rol( $role );
		delete_post_meta_by_key( '_entrepo_precio_' . $role );

		remove_role( $role );

		$grupos = self::leer_opcion();
		unset( $grupos[ $role ] );
		self::guardar_opcion( $grupos );
	}

	/**
	 * ¿`$role` es un grupo B2B gestionado por Entrepo?
	 *
	 * @param string $role Slug de rol a comprobar.
	 * @return bool
	 */
	public function es_grupo( string $role ): bool {
		return isset( self::leer_opcion()[ $role ] );
	}

	/**
	 * El grupo B2B de MAYOR `descuento_pct` entre los roles de un usuario
	 * ARBITRARIO (no necesariamente el actual). `''` si `$user_id` no existe
	 * o no pertenece a ningún grupo Entrepo.
	 *
	 * Es la misma regla de desempate que usaba `PrecioHooks::rol_actual()`
	 * (ahora delegada aquí, ver el docblock de ese método): si el usuario
	 * tiene varios roles de grupo a la vez, gana el de MAYOR % — el cliente
	 * se lleva el mejor precio disponible, no el primero que WordPress liste
	 * en `->roles`. En un empate EXACTO de `descuento_pct` gana el PRIMER rol
	 * de grupo que aparezca en `$user->roles` (la comparación es con `>`
	 * estricto, no `>=`), simplemente porque no hay ningún criterio de
	 * negocio para preferir un grupo empatado sobre otro y el orden de
	 * `->roles` es estable — no es una garantía documentada de WordPress,
	 * solo el comportamiento determinista más simple posible.
	 *
	 * @param int $user_id ID de usuario cuyo mejor grupo se busca.
	 * @return string Slug de rol del mejor grupo, o `''`.
	 */
	public function mejor_grupo_de_usuario( int $user_id ): string {
		$usuario = get_userdata( $user_id );
		if ( ! $usuario ) {
			return '';
		}

		$grupos = self::leer_opcion();

		$mejor_role      = '';
		$mejor_descuento = -1.0;

		foreach ( $usuario->roles as $role ) {
			if ( ! isset( $grupos[ $role ] ) ) {
				continue; // No es un grupo B2B de Entrepo (p. ej. subscriber, editor...).
			}

			$descuento_pct = (float) $grupos[ $role ]['descuento_pct'];
			if ( $descuento_pct > $mejor_descuento ) {
				$mejor_descuento = $descuento_pct;
				$mejor_role      = $role;
			}
		}

		return $mejor_role;
	}

	/**
	 * Slug único para un nombre nuevo: `PREFIJO . sanitize_title($nombre)`,
	 * con sufijo incremental si ya existe un rol WP con ese slug.
	 *
	 * @param string $nombre Nombre del grupo del que se deriva el slug.
	 * @return string
	 */
	private function slug_disponible( string $nombre ): string {
		$base = self::PREFIJO . sanitize_title( $nombre );
		$slug = $base;
		$i    = 2;
		while ( null !== get_role( $slug ) ) {
			$slug = $base . '-' . $i;
			++$i;
		}
		return $slug;
	}

	/**
	 * Satura el % de descuento al rango [0, 100].
	 *
	 * @param float $descuento_pct Porcentaje sin saturar.
	 * @return float
	 */
	private static function saturar( float $descuento_pct ): float {
		return max( 0.0, min( 100.0, $descuento_pct ) );
	}

	/**
	 * Lee la opción `entrepo_grupos` completa (todos los grupos con sus metadatos).
	 *
	 * @return array<string,array{nombre:string,descuento_pct:float}>
	 */
	private static function leer_opcion(): array {
		$grupos = get_option( self::OPCION, array() );
		return is_array( $grupos ) ? $grupos : array();
	}

	/**
	 * Persiste la opción `entrepo_grupos` completa.
	 *
	 * @param array<string,array{nombre:string,descuento_pct:float}> $grupos Grupos a guardar.
	 */
	private static function guardar_opcion( array $grupos ): void {
		update_option( self::OPCION, $grupos );
	}
}
