<?php
/**
 * Motor de cálculo: precalcula el precio neto de cada (producto/variación,
 * rol) y lo persiste en la matriz `entrepo_precios` vía
 * `PrecioRepository::guardar_producto()`.
 *
 * Este es el ÚNICO sitio donde se calcula un precio B2B. El resto del
 * plugin (frontend, carrito) nunca recalcula en runtime: solo lee la matriz
 * que este servicio deja escrita (ver `PrecioRepository::precio_para()`).
 * `neto_para()` es el núcleo puro y testable de esa cuenta; el override fijo
 * por producto/variación siempre GANA al % de descuento del grupo.
 *
 * @package Entrepo
 */

namespace Entrepo\Precios;

use Entrepo\Grupos\GrupoRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Motor de cálculo de precios B2B: precalcula el precio neto por (producto/
 * variación, rol) y lo persiste vía `PrecioRepository::guardar_producto()`.
 */
final class PrecioService {

	/**
	 * Precio neto para un (producto/variación, rol). Función pura, sin BD.
	 *
	 * El override fijo GANA siempre al % de grupo: si viene informado (no
	 * null, incluido 0.0), se usa tal cual, redondeado a 4 decimales; si no,
	 * se aplica el descuento porcentual del grupo sobre el precio base.
	 *
	 * @param float      $precio_base   Precio base (regular) sobre el que aplicar el descuento.
	 * @param float|null $override      Override fijo de precio, si existe (gana siempre al %).
	 * @param float      $descuento_pct Porcentaje de descuento del grupo (0-100).
	 * @return float
	 */
	public static function neto_para( float $precio_base, ?float $override, float $descuento_pct ): float {
		if ( null !== $override ) {
			return round( $override, 4 );
		}

		return round( $precio_base * ( 1 - $descuento_pct / 100 ), 4 );
	}

	/**
	 * Recalcula y persiste TODA la matriz de precios de un producto: para
	 * cada grupo B2B registrado (`GrupoRepository::todos()`), el precio a
	 * nivel de producto (`variation_id = 0`) y el de cada variación (si es
	 * un producto variable). Usa el precio regular de WooCommerce como base
	 * y el override fijo leído de la meta `_entrepo_precio_<role>` (del
	 * producto y de cada variación). Reemplaza atómicamente las filas
	 * existentes de ese producto en `entrepo_precios`.
	 *
	 * La fila a nivel de producto (`variation_id = 0`) solo se escribe si el
	 * producto tiene precio regular propio o hay un override para ese rol;
	 * un producto variable PADRE no tiene ninguno de los dos, así que no
	 * genera esa fila (su neto B2B vive solo en las filas de sus
	 * variaciones).
	 *
	 * @param int $product_id ID del producto (simple o variable) a recalcular.
	 */
	public static function recalcular_producto( int $product_id ): void {
		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return;
		}

		$grupos        = ( new GrupoRepository() )->todos();
		$variation_ids = $product->is_type( 'variable' ) ? $product->get_children() : array();

		$precio_base_producto = self::precio_base( $product );

		$filas = array();
		foreach ( $grupos as $role => $datos ) {
			$descuento_pct = (float) $datos['descuento_pct'];
			$override      = self::override_meta( $product_id, $role );

			// Un producto variable PADRE no tiene `_regular_price` propio: su
			// precio base es siempre 0.0. Si ademas no hay override fijo para
			// este rol, no escribas la fila a nivel de producto (variation_id
			// = 0): guardaria un precio espurio de 0. El neto B2B real vive
			// solo en las filas de las variaciones (mas abajo). Los productos
			// simples con precio (o con override) siguen escribiendo su fila
			// como siempre.
			if ( '' !== $product->get_regular_price() || null !== $override ) {
				$filas[] = array(
					'variation_id' => 0,
					'role'         => $role,
					'price'        => self::neto_para( $precio_base_producto, $override, $descuento_pct ),
				);
			}

			foreach ( $variation_ids as $variation_id ) {
				$variacion = wc_get_product( $variation_id );
				if ( ! $variacion ) {
					continue;
				}

				$filas[] = array(
					'variation_id' => $variation_id,
					'role'         => $role,
					'price'        => self::neto_para( self::precio_base( $variacion ), self::override_meta( $variation_id, $role ), $descuento_pct ),
				);
			}
		}

		( new PrecioRepository() )->guardar_producto( $product_id, $filas );
	}

	/**
	 * Recalcula una PÁGINA del catálogo (todos los productos, todos los
	 * roles): llama `recalcular_producto()` por cada id de la página.
	 *
	 * Es el bloque de construcción del recálculo masivo (`RecalcJob`, F3-3):
	 * como `recalcular_producto()` ya reescribe la matriz completa de un
	 * producto para TODOS los grupos, recorrer el catálogo entero deja la
	 * matriz consistente para todos los roles a la vez — no hace falta un
	 * camino "solo este rol". Trocear por página es lo que permite que
	 * `RecalcJob` reparta el trabajo en lotes en vez de recalcular todo el
	 * catálogo síncronamente dentro de una sola request.
	 *
	 * @param int $pagina     Página del catálogo a procesar (base 1).
	 * @param int $por_pagina Tamaño de la página.
	 * @return array{procesados:int,hay_mas:bool} `procesados`: cuántos productos tenía la página; `hay_mas`: si la página vino llena (puede haber más catálogo detrás).
	 */
	public static function recalcular_lote( int $pagina, int $por_pagina ): array {
		$ids = wc_get_products(
			array(
				'status'  => 'publish',
				'limit'   => $por_pagina,
				'page'    => $pagina,
				'return'  => 'ids',
				'orderby' => 'ID',
				'order'   => 'ASC',
			)
		);

		foreach ( $ids as $id ) {
			self::recalcular_producto( (int) $id );
		}

		return array(
			'procesados' => count( $ids ),
			'hay_mas'    => count( $ids ) === $por_pagina,
		);
	}

	/**
	 * Total de productos publicados del catálogo: el tamaño de trabajo que
	 * `RecalcJob::encolar()` necesita conocer de antemano para poder mostrar
	 * el progreso «X de Y» en el panel.
	 *
	 * @return int
	 */
	public static function contar_productos(): int {
		$resultado = wc_get_products(
			array(
				'status'   => 'publish',
				'limit'    => 1,
				'paginate' => true,
			)
		);

		return (int) $resultado->total;
	}

	/**
	 * Precio regular de WooCommerce como float ('' cuando no tiene => 0.0).
	 *
	 * @param \WC_Product $product Producto o variación del que leer el precio regular.
	 * @return float
	 */
	private static function precio_base( \WC_Product $product ): float {
		$precio = $product->get_regular_price();
		return '' === $precio ? 0.0 : (float) $precio;
	}

	/**
	 * Override fijo de precio para (post, rol), leído de la meta
	 * `_entrepo_precio_<role>`, o null si no existe o está vacía.
	 *
	 * @param int    $post_id ID del producto o variación.
	 * @param string $role    Rol/grupo B2B.
	 * @return float|null
	 */
	private static function override_meta( int $post_id, string $role ): ?float {
		$valor = get_post_meta( $post_id, '_entrepo_precio_' . $role, true );
		return '' === $valor ? null : (float) $valor;
	}
}
