<?php
/**
 * Controlador REST de grupos B2B: CRUD completo sobre `entrepo/v1`.
 *
 * @package Entrepo
 */

namespace Entrepo\Rest;

use Entrepo\Grupos\GrupoRepository;
use Entrepo\Infra\RecalcJob;

defined( 'ABSPATH' ) || exit;

/**
 * Recurso REST `grupos`: los grupos B2B del plugin para el panel React.
 *
 * Sigue el patrón-casa: `registrar_rutas()` declara las rutas y `puede()`
 * es el `permission_callback` común (capability, nunca nonce a mano — el
 * nonce `X-WP-Nonce` lo verifica WordPress antes de llegar aquí).
 */
final class GruposController {

	/** Declara las rutas del recurso bajo `entrepo/v1`. */
	public function registrar_rutas(): void {
		register_rest_route(
			'entrepo/v1',
			'/grupos',
			array(
				array(
					'methods'             => 'GET',
					'permission_callback' => array( $this, 'puede' ),
					'callback'            => array( $this, 'listar' ),
				),
				array(
					'methods'             => 'POST',
					'permission_callback' => array( $this, 'puede' ),
					'callback'            => array( $this, 'crear' ),
				),
			)
		);

		register_rest_route(
			'entrepo/v1',
			'/grupos/(?P<role>[a-z0-9_\-]+)',
			array(
				array(
					'methods'             => 'PUT',
					'permission_callback' => array( $this, 'puede' ),
					'callback'            => array( $this, 'actualizar' ),
				),
				array(
					'methods'             => 'DELETE',
					'permission_callback' => array( $this, 'puede' ),
					'callback'            => array( $this, 'borrar' ),
				),
			)
		);
	}

	/**
	 * Gate de TODOS los endpoints del recurso, incluidos los GET: el listado
	 * de grupos expone datos comerciales (descuentos B2B).
	 *
	 * @return bool
	 */
	public function puede(): bool {
		return current_user_can( 'manage_woocommerce' );
	}

	/**
	 * Listado de grupos 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 listar( \WP_REST_Request $req ): \WP_REST_Response { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Firma del callback REST; se usará cuando el listado pagine/filtre.
		$grupos = ( new GrupoRepository() )->todos();

		$items = array();
		foreach ( $grupos as $role => $datos ) {
			$items[] = array(
				'role'          => $role,
				// $datos siempre trae 'nombre'/'descuento_pct': GrupoRepository::todos()
				// lo garantiza en su tipo de retorno (array<string,array{nombre:string,descuento_pct:float}>).
				'nombre'        => $datos['nombre'],
				'descuento_pct' => $datos['descuento_pct'],
				'num_clientes'  => $this->num_clientes( $role ),
			);
		}

		$total = count( $items );
		$data  = array(
			'items' => $items,
			'total' => $total,
			// Grupos no pagina de verdad todavia (F3-1): una sola pagina que
			// contiene todo. Se respeta la forma {items,total,pages} para que
			// DataViews reciba lo mismo que el resto de listados del panel.
			'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;
	}

	/**
	 * Crea un grupo B2B nuevo.
	 *
	 * Tras crear el grupo se encola el recálculo masivo del catálogo
	 * (`RecalcJob::encolar()`, F3-3): aunque un grupo recién creado no tiene
	 * overrides propios todavía, sí participa del % global desde ya, así que
	 * el catálogo necesita ganar sus filas de precio para este rol nuevo.
	 * El resultado del encolado viaja en la respuesta (`recalc_encolado`):
	 * casi siempre `true`; `false` solo si ya había un recálculo activo — el
	 * job vivo leerá el % de este grupo en los lotes que aún no ha
	 * procesado (caso límite aceptado, ver `RecalcJob::encolar()`).
	 *
	 * FREE, sin gate (fix ARBITRAJE, lote de revisión de rama — ver
	 * `Plan::MATRIZ`): el recálculo automático del catálogo al cambiar un
	 * grupo es corrección básica del motor de precios, no una comodidad Pro
	 * — sin él, el % nuevo del grupo queda sin reflejarse en la matriz
	 * `entrepo_precios` hasta que cada producto se re-guarde a mano, lo cual
	 * es justo el hueco que `RecalcJob` existe para cerrar (ver
	 * `GrupoRepository::actualizar()`). Antes de este fix este encolado
	 * dependía de `Plan::es_pro()`, lo que convertía la corrección del propio
	 * core free en una feature Pro fantasma. Lo que SÍ queda como concepto
	 * Pro futuro (ver `Plan::MATRIZ['pro']`, aún sin construir) es el
	 * recálculo PROGRAMADO o bajo demanda del catálogo completo — una
	 * operación distinta a este encolado automático tras crear/editar.
	 *
	 * @param \WP_REST_Request $req Petición con `nombre` (string, obligatorio) y `descuento_pct` (float).
	 * @return \WP_REST_Response|\WP_Error El grupo creado (201), o `WP_Error` 400 si falta el nombre.
	 */
	public function crear( \WP_REST_Request $req ) {
		$nombre = sanitize_text_field( (string) $req->get_param( 'nombre' ) );
		if ( '' === trim( $nombre ) ) {
			return new \WP_Error(
				'entrepo_nombre_requerido',
				__( 'The group name is required.', 'entrepo' ),
				array( 'status' => 400 )
			);
		}

		$descuento_pct = (float) $req->get_param( 'descuento_pct' );
		$repo          = new GrupoRepository();
		$role          = $repo->crear( $nombre, $descuento_pct );
		$grupo         = $repo->obtener( $role );

		$encolado = RecalcJob::encolar( 'grupo:' . $role );

		return new \WP_REST_Response(
			array(
				'role'            => $role,
				'nombre'          => $grupo['nombre'] ?? $nombre,
				'descuento_pct'   => $grupo['descuento_pct'] ?? $descuento_pct,
				'recalc_encolado' => $encolado,
			),
			201
		);
	}

	/**
	 * Actualiza nombre y descuento de un grupo existente.
	 *
	 * Tras actualizar el grupo se encola el recálculo masivo del catálogo
	 * (`RecalcJob::encolar()`, F3-3): esto es lo que cierra en producción el
	 * hueco latente documentado en `GrupoRepository::actualizar()` — cambiar
	 * el % desde el panel ya no deja la matriz de `entrepo_precios` obsoleta.
	 * El resultado del encolado viaja en la respuesta (`recalc_encolado`):
	 * casi siempre `true`; `false` solo si ya había un recálculo activo — el
	 * job vivo leerá el % nuevo en los lotes que aún no ha procesado (caso
	 * límite aceptado, ver `RecalcJob::encolar()`).
	 *
	 * FREE, sin gate (fix ARBITRAJE, lote de revisión de rama): mismo
	 * criterio que `crear()` — ver su docblock.
	 *
	 * @param \WP_REST_Request $req Petición con el parámetro de ruta `role` y el body `nombre`/`descuento_pct`.
	 * @return \WP_REST_Response|\WP_Error El grupo actualizado (200), o `WP_Error` 404 si `role` no es un grupo.
	 */
	public function actualizar( \WP_REST_Request $req ) {
		$role = sanitize_key( (string) $req->get_param( 'role' ) );
		$repo = new GrupoRepository();

		if ( ! $repo->es_grupo( $role ) ) {
			return new \WP_Error(
				'entrepo_grupo_no_encontrado',
				__( 'The group does not exist.', 'entrepo' ),
				array( 'status' => 404 )
			);
		}

		// Mismo guard que crear(): sin él, un PUT directo (Postman, un futuro
		// consumidor de la API) con nombre vacío dejaría el grupo sin nombre de
		// forma permanente — GrupoRepository::actualizar() no tiene fallback.
		$nombre = sanitize_text_field( (string) $req->get_param( 'nombre' ) );
		if ( '' === trim( $nombre ) ) {
			return new \WP_Error(
				'entrepo_nombre_requerido',
				__( 'The group name is required.', 'entrepo' ),
				array( 'status' => 400 )
			);
		}

		$descuento_pct = (float) $req->get_param( 'descuento_pct' );
		$repo->actualizar( $role, $nombre, $descuento_pct );

		$encolado = RecalcJob::encolar( 'grupo:' . $role );

		$grupo = $repo->obtener( $role );
		return new \WP_REST_Response(
			array(
				'role'            => $role,
				'nombre'          => $grupo['nombre'] ?? $nombre,
				'descuento_pct'   => $grupo['descuento_pct'] ?? $descuento_pct,
				'recalc_encolado' => $encolado,
			),
			200
		);
	}

	/**
	 * Elimina un grupo: purga usuarios, filas de precio y overrides asociados
	 * (vía `GrupoRepository::eliminar()`).
	 *
	 * @param \WP_REST_Request $req Petición con el parámetro de ruta `role`.
	 * @return \WP_REST_Response|\WP_Error `{deleted:true}` (200), o `WP_Error` 404 si `role` no es un grupo.
	 */
	public function borrar( \WP_REST_Request $req ) {
		$role = sanitize_key( (string) $req->get_param( 'role' ) );
		$repo = new GrupoRepository();

		if ( ! $repo->es_grupo( $role ) ) {
			return new \WP_Error(
				'entrepo_grupo_no_encontrado',
				__( 'The group does not exist.', 'entrepo' ),
				array( 'status' => 404 )
			);
		}

		$repo->eliminar( $role );

		return new \WP_REST_Response( array( 'deleted' => true ), 200 );
	}

	/**
	 * Numero de clientes con el rol de este grupo.
	 *
	 * @param string $role Slug de rol del grupo.
	 * @return int
	 */
	private function num_clientes( string $role ): int {
		return count(
			get_users(
				array(
					'role'   => $role,
					'fields' => 'ID',
				)
			)
		);
	}
}
