<?php

namespace App\Services\Licencias;

use App\Models\Monitoring\Log;
use App\Models\Products\LicenseToken;
use App\Models\Products\LicenseTokenProduct;
use App\Models\Products\Product;
use Illuminate\Support\Facades\DB;

/**
 * Vincular y quitar productos de una licencia.
 *
 * **Vivía dentro de la ficha de la licencia** (`LicenseTokens\Show`), y la API de
 * administración (PRODSECU-215) necesita hacer exactamente lo mismo. Las dos cosas que
 * hacen falta saber de esta tabla son fáciles de olvidar, y por eso se sacan aquí en vez
 * de copiarlas:
 *
 * - **El índice único `(license_token_id, product_id)` no incluye `deleted_at`.** No puede
 *   haber dos filas para el mismo par, así que volver a poner un producto que se quitó es
 *   **restaurar su fila**, no insertar otra: un `attach` reventaría contra el índice.
 * - **Quitar es borrar en blando y dejar rastro (16021).** `LicenseToken::products()`
 *   filtra los borrados, así que un producto quitado deja de dar servicio en la siguiente
 *   petición del Moodle.
 */
class ProductosDeLaLicencia
{
    /**
     * Vincula un producto, **recuperando su fila si estaba quitada**.
     *
     * Al restaurar se limpia una `end_at` pasada: un producto que vuelve no puede volver
     * caducado —quedaría «activo y caducado», que es el estado que evitamos en las
     * renovaciones—.
     *
     * Si ya estaba vinculado no hace nada: así fijar dos veces el mismo conjunto no cambia
     * nada.
     *
     * @return bool si ha cambiado algo
     */
    public function vincular(LicenseToken $licencia, int $productId, ?int $usuarioId): bool
    {
        $fila = LicenseTokenProduct::withTrashed()
            ->where('license_token_id', $licencia->id)
            ->where('product_id', $productId)
            ->first();

        if ($fila === null) {
            $licencia->products()->attach($productId, [
                'status' => 'active',
                'assigned_by' => $usuarioId,
            ]);

            return true;
        }

        if (! $fila->trashed()) {
            return false;
        }

        $fila->restore();
        $fila->status = 'active';

        if ($fila->end_at && $fila->end_at->isPast()) {
            $fila->end_at = null;
        }

        $fila->save();

        return true;
    }

    /**
     * Quita un producto **en blando** y deja el rastro 16021.
     *
     * `detach()` borraba la fila de verdad —con sus fechas y su `assigned_by`— pese a que
     * la tabla tiene `deleted_at`.
     *
     * @return bool si ha cambiado algo
     */
    public function quitar(LicenseToken $licencia, int $productId): bool
    {
        $fila = LicenseTokenProduct::where('license_token_id', $licencia->id)
            ->where('product_id', $productId)
            ->first();

        if ($fila === null) {
            return false;
        }

        $fila->delete();

        Log::db(
            'warning',
            '16021',
            'Producto ' . (Product::find($productId)?->name ?? $productId)
            . ' quitado de la licencia ' . ($licencia->name ?? 'sin nombre')
            . ' (' . ($licencia->client?->name ?? 'sin cliente') . '), quitado ahora'
            . '. Afecta a ' . $licencia->environments()->count() . ' entorno(s)',
            'LicenseToken',
            (string) $licencia->id
        );

        return true;
    }

    /**
     * Deja la licencia con **exactamente** estos productos: vincula los que faltan y quita
     * los que sobran.
     *
     * Es idempotente: repetirlo con la misma lista no toca ninguna fila ni escribe ningún
     * log, porque ni `vincular()` ni `quitar()` hacen nada si ya está como se pide.
     *
     * @param  array<int|string>  $productIds
     * @return array{anadidos: list<int>, quitados: list<int>}
     */
    public function fijar(LicenseToken $licencia, array $productIds, ?int $usuarioId): array
    {
        $pedidos = array_values(array_unique(array_map('intval', $productIds)));
        $actuales = $licencia->products()->pluck('products.id')->map(fn ($id) => (int) $id)->all();

        $aAnadir = array_values(array_diff($pedidos, $actuales));
        $aQuitar = array_values(array_diff($actuales, $pedidos));

        return DB::transaction(function () use ($licencia, $aAnadir, $aQuitar, $usuarioId) {
            $anadidos = [];
            foreach ($aAnadir as $productId) {
                if ($this->vincular($licencia, $productId, $usuarioId)) {
                    $anadidos[] = $productId;
                }
            }

            $quitados = [];
            foreach ($aQuitar as $productId) {
                if ($this->quitar($licencia, $productId)) {
                    $quitados[] = $productId;
                }
            }

            return ['anadidos' => $anadidos, 'quitados' => $quitados];
        });
    }

    /**
     * Los productos que **no** son compatibles con alguna de estas plataformas.
     *
     * La compatibilidad está en `product_type`: un producto vale para una plataforma si
     * hay fila para el par. Un producto tiene que valer para **todas** las plataformas de
     * los entornos de su licencia, porque la licencia le da servicio a todos.
     *
     * @param  array<int>  $productIds
     * @param  array<int|null>  $typeIds  los nulos se ignoran: un entorno sin plataforma no restringe
     * @return array<int, list<int>>  `[product_id => [type_id que no admite, …]]`
     */
    public function incompatibles(array $productIds, array $typeIds): array
    {
        $productIds = array_values(array_unique(array_map('intval', $productIds)));
        $typeIds = array_values(array_unique(array_map('intval', array_filter($typeIds, fn ($id) => $id !== null))));

        if ($productIds === [] || $typeIds === []) {
            return [];
        }

        $compatibles = DB::table('product_type')
            ->whereIn('product_id', $productIds)
            ->whereIn('type_id', $typeIds)
            ->get(['product_id', 'type_id'])
            ->groupBy('product_id')
            ->map(fn ($filas) => $filas->pluck('type_id')->map(fn ($id) => (int) $id)->all());

        $resultado = [];
        foreach ($productIds as $productId) {
            $faltan = array_values(array_diff($typeIds, $compatibles->get($productId, [])));

            if ($faltan !== []) {
                $resultado[$productId] = $faltan;
            }
        }

        return $resultado;
    }
}
