<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Requests\Api\Admin\V1\LicenciaRequest;
use App\Http\Requests\Api\Admin\V1\ProductosDeLaLicenciaRequest;
use App\Http\Resources\Admin\V1\LicenseTokenResource;
use App\Models\Clients\Client;
use App\Models\Environments\Type;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use App\Services\Licencias\ProductosDeLaLicencia;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;

/**
 * Buscar, crear y editar licencias, y fijar sus productos.
 *
 * Lo que corta servicio no está: apagar una licencia, regenerar su token o moverle
 * entornos. Eso se hace desde el panel, que pide motivo y avisa de a quién afecta.
 */
class LicenciasController extends ControladorDeAdministracion
{
    public function __construct(private ProductosDeLaLicencia $productos) {}

    /**
     * Por cliente y por estado. **Nunca por token** ni con el token en la respuesta: ver
     * {@see LicenseTokenResource}.
     */
    public function index(Request $request): AnonymousResourceCollection
    {
        $porPagina = $this->validarListado($request, [
            'client_id' => 'nullable|integer',
            'active' => 'nullable|in:true,false,1,0',
        ]);

        $licencias = LicenseToken::query()
            ->with(['environments:id,license_token_id', 'products'])
            ->when($request->filled('client_id'), fn ($q) => $q->where('client_id', (int) $request->query('client_id')))
            ->when($request->filled('active'), fn ($q) => $q->where('active', $request->boolean('active')))
            ->orderBy('id')
            ->paginate($porPagina)
            ->withQueryString();

        return LicenseTokenResource::collection($licencias);
    }

    /**
     * **La única respuesta de la API que lleva el token `3IP-…`.**
     *
     * Las demás operaciones sirven para encontrar el id de la licencia —`GET
     * /environments?domain=…` trae su `license_token_id`— y no lo devuelven, ni siquiera el
     * alta. Un solo sitio por el que sale la credencial es un solo sitio que vigilar (decidido
     * con Antonio el 2026-10-30).
     *
     * Con `Cache-Control: no-store`: la respuesta lleva una credencial y no debe quedarse en
     * ninguna caché intermedia ni del cliente HTTP.
     */
    public function show(LicenseToken $licenseToken): JsonResponse
    {
        $licenseToken->load(['environments:id,license_token_id', 'products']);

        return LicenseTokenResource::make($licenseToken)->conToken()->response()
            ->header('Cache-Control', 'no-store, private');
    }

    /**
     * Crea la licencia **activa y con un token que genera el Manager**. La respuesta no lo
     * trae: se pide con {@see show()}.
     */
    public function store(LicenciaRequest $request, Client $client): JsonResponse
    {
        if (! $client->actived) {
            $this->fallo('El cliente está dado de baja: no admite licencias nuevas. Se reactiva desde el panel.', [], 409);
        }

        $datos = $request->safe()->only(LicenciaRequest::camposAceptados(false));

        $licencia = DB::transaction(function () use ($request, $client, $datos) {
            $licencia = LicenseToken::create([
                ...$datos,
                'client_id' => $client->id,
                'token' => LicenseToken::generarToken(),
                'active' => true,
                'created_by' => $request->user()->id,
            ]);

            $this->rastro(
                $request,
                'Licencia ' . ($licencia->name ?: $licencia->id) . ' creada para el cliente ' . $client->name . ' desde la API de administración',
                'LicenseToken',
                $licencia->id
            );

            return $licencia;
        });

        $licencia->load(['environments:id,license_token_id', 'products']);

        return LicenseTokenResource::make($licencia)->response()->setStatusCode(201);
    }

    public function update(LicenciaRequest $request, LicenseToken $licenseToken): LicenseTokenResource
    {
        $licenseToken->fill($request->safe()->only(LicenciaRequest::camposAceptados(true)));
        $cambios = array_keys($licenseToken->getDirty());

        if ($cambios !== []) {
            $licenseToken->save();

            $this->rastro(
                $request,
                'Licencia ' . ($licenseToken->name ?: $licenseToken->id) . ' editada desde la API de administración: ' . implode(', ', $cambios),
                'LicenseToken',
                $licenseToken->id
            );
        }

        $licenseToken->load(['environments:id,license_token_id', 'products']);

        return LicenseTokenResource::make($licenseToken);
    }

    /**
     * Deja la licencia con exactamente estos productos, **sin quitar ninguno**.
     *
     * Quitar un producto corta el servicio en todos los sitios de la licencia, y eso se hace
     * desde el panel, que avisa de a quién afecta y deja rastro. Así que si en la petición
     * falta alguno de los que ya tiene, es un **409 que dice cuáles** y no se cambia nada:
     * un script que manda la lista incompleta lo sabe, en vez de quitar en silencio o de
     * quedarse creyendo que ya no están.
     *
     * Antes de tocar nada se comprueba todo, y si algo falla es un 422 **sin cambiar
     * nada**:
     *
     * - Los productos que se **añaden** tienen que estar activos y habilitados para la
     *   venta. Los que ya tenía se pueden mantener aunque hoy no lo estén: quitárselos por
     *   eso sería cortar servicio sin que nadie lo haya pedido.
     * - **Cada producto tiene que ser compatible con la plataforma de todos los entornos de
     *   la licencia** (`product_type`): la licencia les da servicio a todos.
     */
    public function products(ProductosDeLaLicenciaRequest $request, LicenseToken $licenseToken): LicenseTokenResource
    {
        $pedidos = $request->productIds();
        $actuales = $licenseToken->products()->pluck('products.id')->map(fn ($id) => (int) $id)->all();
        $nuevos = array_values(array_diff($pedidos, $actuales));
        $faltan = array_values(array_diff($actuales, $pedidos));

        if ($faltan !== []) {
            $this->fallo(
                'Quitar productos de una licencia se hace desde el panel. Faltan en la petición: '
                . Product::withTrashed()->whereIn('id', $faltan)->orderBy('slug')->pluck('slug')->implode(', ') . '.',
                ['products' => ['La lista tiene que incluir los productos que la licencia ya tiene.']],
                409
            );
        }

        $noVendibles = Product::whereIn('id', $nuevos)
            ->where(fn ($q) => $q->where('actived', false)->orWhere('enabled', false))
            ->pluck('slug', 'id');

        if ($noVendibles->isNotEmpty()) {
            $this->fallo('Hay productos que no se pueden añadir.', $this->erroresPorProducto(
                $pedidos,
                $noVendibles->map(fn ($slug) => 'El producto ' . $slug . ' no está activo o no está habilitado para la venta.')->all()
            ));
        }

        $tipos = $licenseToken->environments()->pluck('type_id')->all();
        $incompatibles = $this->productos->incompatibles($pedidos, $tipos);

        if ($incompatibles !== []) {
            $this->fallo(
                'Hay productos que no son compatibles con la plataforma de algún entorno de la licencia.',
                $this->erroresPorProducto($pedidos, $this->mensajesDeIncompatibilidad($incompatibles))
            );
        }

        // **Solo `vincular()`, nunca `fijar()`.** `fijar()` vuelve a leer los productos al
        // escribir y quita los que no vengan: si entre la comprobación de arriba y este punto
        // alguien añade uno desde el panel, la API lo quitaría (auditoría PRODSECU-215). Así
        // la API no tiene ningún camino por el que quitar.
        $anadidos = DB::transaction(function () use ($licenseToken, $nuevos, $request) {
            $hechos = [];

            foreach ($nuevos as $productId) {
                if ($this->productos->vincular($licenseToken, $productId, $request->user()->id)) {
                    $hechos[] = $productId;
                }
            }

            return $hechos;
        });

        if ($anadidos !== []) {
            $slugs = Product::whereIn('id', $anadidos)->pluck('slug')->implode(', ');

            $this->rastro(
                $request,
                'Productos añadidos a la licencia ' . ($licenseToken->name ?: $licenseToken->id)
                . ' desde la API de administración: ' . $slugs,
                'LicenseToken',
                $licenseToken->id
            );
        }

        $licenseToken->load(['environments:id,license_token_id', 'products']);

        return LicenseTokenResource::make($licenseToken);
    }

    /**
     * `[product_id => mensaje]` → `{"products.N.product_id": [mensaje]}`, con la posición
     * que el producto tenía en la petición.
     *
     * @param  list<int>  $pedidos
     * @param  array<int, string>  $mensajes
     * @return array<string, list<string>>
     */
    private function erroresPorProducto(array $pedidos, array $mensajes): array
    {
        $errores = [];

        foreach ($pedidos as $posicion => $productId) {
            if (isset($mensajes[$productId])) {
                $errores['products.' . $posicion . '.product_id'] = [$mensajes[$productId]];
            }
        }

        return $errores;
    }

    /**
     * @param  array<int, list<int>>  $incompatibles
     * @return array<int, string>
     */
    private function mensajesDeIncompatibilidad(array $incompatibles): array
    {
        $slugs = Product::whereIn('id', array_keys($incompatibles))->pluck('slug', 'id');
        $tipos = Type::withTrashed()->pluck('shortname', 'id');

        $mensajes = [];
        foreach ($incompatibles as $productId => $tipoIds) {
            $mensajes[$productId] = 'El producto ' . ($slugs[$productId] ?? $productId)
                . ' no es compatible con la plataforma ' . implode(', ', array_map(fn ($id) => $tipos[$id] ?? $id, $tipoIds)) . '.';
        }

        return $mensajes;
    }
}
