<?php

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

use App\Http\Requests\Api\Admin\V1\EntornoRequest;
use App\Http\Resources\Admin\V1\EnvironmentResource;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Type;
use App\Models\Monitoring\Log;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use App\Services\Licencias\ProductosDeLaLicencia;
use App\Support\BusquedaDeEntornos;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;

/**
 * Buscar, crear y editar entornos.
 *
 * Lo que corta servicio no está: apagarlo, cambiarlo de cliente, o quitarle o cambiarle
 * la licencia. **Asignar licencia a un entorno que no tiene sí**, porque es el último paso
 * de un alta.
 */
class EntornosController extends ControladorDeAdministracion
{
    public function __construct(private ProductosDeLaLicencia $productos) {}

    /**
     * `domain` exacto —único salvo en los `local`, así que puede devolver más de uno—, y
     * por cliente, licencia, plataforma y uso.
     *
     * `search` es la búsqueda aproximada por nombre y dominio, sin protocolo: ordena por
     * cercanía y, si nada coincide, sugiere los parecidos. Ver {@see BusquedaDeEntornos}.
     */
    public function index(Request $request): AnonymousResourceCollection
    {
        $porPagina = $this->validarListado($request, [
            'search' => 'nullable|string|max:100',
            'domain' => 'nullable|string',
            'client_id' => 'nullable|integer',
            'license_token_id' => 'nullable|integer',
            'type_id' => 'nullable|integer',
            'env' => 'nullable|string',
        ]);

        $consulta = Environment::query()
            // Igual que el mutador del modelo: sin espacios ni barra final.
            ->when($request->filled('domain'), fn ($q) => $q->where('domain', rtrim(trim((string) $request->query('domain')), '/')))
            ->when($request->filled('client_id'), fn ($q) => $q->where('client_id', (int) $request->query('client_id')))
            ->when($request->filled('license_token_id'), fn ($q) => $q->where('license_token_id', (int) $request->query('license_token_id')))
            ->when($request->filled('type_id'), fn ($q) => $q->where('type_id', (int) $request->query('type_id')))
            ->when($request->filled('env'), fn ($q) => $q->where('env', $request->query('env')));

        if (! $request->filled('search')) {
            return EnvironmentResource::collection(
                $consulta->orderBy('id')->paginate($porPagina)->withQueryString()
            );
        }

        // El orden es el de la cercanía, que no sale de SQL: se pagina sobre los ids ya
        // ordenados y se cargan solo los de la página.
        $ids = BusquedaDeEntornos::ids($consulta, (string) $request->query('search'));
        $pagina = LengthAwarePaginator::resolveCurrentPage();
        $deLaPagina = array_slice($ids, ($pagina - 1) * $porPagina, $porPagina);
        $modelos = Environment::whereIn('id', $deLaPagina)->get()->sortBy(fn ($e) => array_search($e->id, $deLaPagina, true))->values();

        $paginados = (new LengthAwarePaginator($modelos, count($ids), $porPagina, $pagina, [
            'path' => $request->url(),
        ]))->withQueryString();

        return EnvironmentResource::collection($paginados);
    }

    public function show(Environment $environment): EnvironmentResource
    {
        return EnvironmentResource::make($environment);
    }

    /**
     * Crea el entorno **activo**, con lo que llegue. Qué es obligatorio lo dice
     * {@see EntornoRequest}, que sigue al formulario del panel.
     */
    public function store(EntornoRequest $request, Client $client): JsonResponse
    {
        if (! $client->actived) {
            $this->fallo('El cliente está dado de baja: no admite entornos nuevos. Se reactiva desde el panel.', [], 409);
        }

        $datos = $request->safe()->only(EntornoRequest::CAMPOS);
        $licenciaId = $datos['license_token_id'] ?? null;

        if ($licenciaId !== null) {
            $this->comprobarLicencia(LicenseToken::findOrFail($licenciaId), $client->id, self::tipo($datos['type_id'] ?? null));
        }

        $entorno = DB::transaction(function () use ($request, $client, $datos, $licenciaId) {
            $entorno = Environment::create([
                ...$datos,
                'client_id' => $client->id,
                'active' => true,
            ]);

            $this->rastro(
                $request,
                'Entorno ' . $entorno->name . ' (' . $entorno->domain . ') creado para el cliente ' . $client->name
                . ' desde la API de administración' . ($licenciaId ? ', con la licencia ' . $licenciaId : ''),
                'Environment',
                $entorno->id
            );

            return $entorno;
        });

        return EnvironmentResource::make($entorno)->response()->setStatusCode(201);
    }

    /**
     * Edita nombre, dominio, uso, plataforma, y asigna licencia **si no tiene**.
     *
     * - Un entorno con licencia no se pasa a otra ni se queda sin ella: 409. Ese sitio
     *   dejaría de recibir lo que tiene contratado, y eso se hace desde el panel.
     * - Cambiar la plataforma de un entorno con licencia exige que sus productos sean
     *   compatibles con la nueva.
     * - Cambiar el dominio corta el servicio hasta que el Moodle llame desde el nuevo, así
     *   que deja además el rastro de siempre (16020), como el panel.
     */
    public function update(EntornoRequest $request, Environment $environment): EnvironmentResource
    {
        $datos = $request->safe()->only(EntornoRequest::CAMPOS);

        if (array_key_exists('license_token_id', $datos)) {
            $nueva = $datos['license_token_id'] !== null ? (int) $datos['license_token_id'] : null;
            $actual = $environment->license_token_id;

            if ($actual !== null && $nueva !== $actual) {
                $this->fallo(
                    'El entorno ya tiene licencia: cambiársela o quitársela se hace desde el panel.',
                    [],
                    409
                );
            }
        }

        $licenciaId = $datos['license_token_id'] ?? $environment->license_token_id;
        $tipoId = array_key_exists('type_id', $datos) ? self::tipo($datos['type_id']) : self::tipo($environment->type_id);
        $cambiaLicencia = $licenciaId !== null && $environment->license_token_id === null;
        $cambiaTipo = array_key_exists('type_id', $datos) && $tipoId !== self::tipo($environment->type_id);

        if ($licenciaId !== null && ($cambiaLicencia || $cambiaTipo)) {
            $this->comprobarLicencia(LicenseToken::findOrFail($licenciaId), $environment->client_id, $tipoId);
        }

        $dominioAnterior = $environment->domain;
        $environment->fill($datos);
        $cambios = array_keys($environment->getDirty());

        if ($cambios === []) {
            return EnvironmentResource::make($environment);
        }

        DB::transaction(function () use ($request, $environment, $cambios, $dominioAnterior) {
            $environment->save();

            $this->rastro(
                $request,
                'Entorno ' . $environment->name . ' editado desde la API de administración: ' . implode(', ', $cambios),
                'Environment',
                $environment->id
            );

            if (in_array('domain', $cambios, true)) {
                Log::db(
                    'warning',
                    '16020',
                    'Dominio del entorno ' . $environment->name . ' cambiado de ' . ($dominioAnterior ?: 'vacío')
                    . ' a ' . ($environment->domain ?: 'vacío') . ' (por la API de administración)',
                    'Environment',
                    (string) $environment->id
                );
            }
        });

        return EnvironmentResource::make($environment->refresh());
    }

    /** La plataforma es opcional: sin ella, `null`, y no un `0` que no existe. */
    private static function tipo(mixed $valor): ?int
    {
        return $valor === null || $valor === '' ? null : (int) $valor;
    }

    /**
     * Las dos reglas para que un entorno tenga una licencia, con 422 si no se cumplen:
     *
     * - **Del mismo cliente** (la regla de `AssignsTokenEnvironments`, MGR-004).
     * - **Sus productos, compatibles con la plataforma del entorno** (`product_type`).
     */
    private function comprobarLicencia(LicenseToken $licencia, ?int $clienteId, ?int $tipoId): void
    {
        if ($licencia->client_id !== $clienteId) {
            $this->fallo('Los datos no son válidos.', [
                'license_token_id' => ['La licencia es de otro cliente: un entorno solo puede tener una licencia de su mismo cliente.'],
            ]);
        }

        $incompatibles = $this->productos->incompatibles(
            $licencia->products()->pluck('products.id')->all(),
            [$tipoId]
        );

        if ($incompatibles !== []) {
            $slugs = Product::whereIn('id', array_keys($incompatibles))->pluck('slug')->implode(', ');
            $plataforma = Type::withTrashed()->find($tipoId)?->shortname ?? $tipoId;

            $this->fallo('Los datos no son válidos.', [
                'license_token_id' => [
                    'La licencia tiene productos que no son compatibles con la plataforma ' . $plataforma . ': ' . $slugs . '.',
                ],
            ]);
        }
    }
}
