<?php

namespace App\Livewire\Environments;

use App\Models\ApiRequestLog;
use App\Models\Environments\Environment;
use App\Services\Api\Severidad;
use App\Services\Moodle\MoodleSyncService;
use App\Support\SaludDelEntorno;
use App\Support\VersionDeMoodle;
use Livewire\Component;

/**
 * La ficha de un entorno: `/environments/{id}`.
 *
 * **Antes de esto no se podía entrar a un entorno.** El nombre del listado no era un
 * enlace, y lo que había eran cinco pantallas sueltas —listado, editar, uso, plugins,
 * soportes—, cada una con su propio «volver al listado». Moverse entre los datos de un
 * mismo sitio era *listado → uso → listado → plugins → listado → soportes*, y **la única
 * vista completa de un entorno era su formulario de edición**: para leer la descripción o
 * el tipo había que abrir la pantalla de escribir.
 *
 * Y lo que no estaba en ninguna de las cinco es justo la pregunta que se hace:
 *
 * > ¿Este sitio está recibiendo lo que su cliente tiene contratado? Y si no, ¿por qué?
 *
 * Responderla exigía cruzar a mano la licencia, el registro de la API, el inventario y
 * `refresh_at`. Eso es lo que hace {@see SaludDelEntorno} y lo que dice el veredicto de
 * la cabecera.
 *
 * **De lectura.** Editar sigue en su formulario y apagar en su modal: aquí solo se enlaza
 * a ellas. Así un usuario con permiso de lectura —Support tiene los de lectura y no los
 * de escritura— puede ver la ficha entera sin que la pantalla se le rompa a trozos.
 */
class Show extends Component
{
    public Environment $environment;


    public function mount(Environment $environment): void
    {
        // El permiso de la ruta no se reaplica en /livewire/update (MGR-005). Es el de
        // lectura del listado: quien puede ver la lista puede ver la ficha.
        $this->authorize('admin.environments.index');

        $this->environment = $environment;
    }

    /**
     * ¿Se le puede pedir a este Moodle que se sincronice?
     *
     * Las dos cosas: permiso y que el sitio nos haya dado su token de servicios web. Sin
     * token no hay forma de llamarle, y es más honesto no ofrecer el botón que ofrecerlo
     * y fallar. Mismo criterio que en la tarjeta del listado.
     */
    public function getPuedeSincronizarProperty(): bool
    {
        return auth()->user()?->can('admin.environments.refresh')
            && trim((string) $this->environment->moodletoken) !== '';
    }

    public function sincronizar(MoodleSyncService $servicio): void
    {
        $this->authorize('admin.environments.refresh');

        $resultado = $servicio->lanzar($this->environment);

        if (! $resultado['ok']) {
            // En modal y no en un flash: el fallo trae un detalle técnico que hay que
            // poder leer y copiar.
            $this->dispatch('openModal',
                component: SyncResultModal::class,
                arguments: [
                    'dominio' => (string) $this->environment->domain,
                    'mensaje' => $resultado['mensaje'],
                    'detalle' => $resultado['detalle'] ?? null,
                ]
            );

            return;
        }

        $this->environment->refresh();

        session()->flash('success', $resultado['mensaje']);
    }

    public function abrirConexionMoodle(): void
    {
        $this->authorize('admin.environments.edit');

        $this->dispatch('openModal',
            component: MoodleConnectionModal::class,
            arguments: ['environmentId' => $this->environment->id]
        );
    }

    public function abrirCambioDeEstado(): void
    {
        $this->authorize('admin.environments.edit');

        $this->dispatch('openModal',
            component: EnvironmentStatusModal::class,
            arguments: ['environmentId' => $this->environment->id]
        );
    }

    /**
     * Abre la observación: qué hay que revisar en este entorno, o darlo por revisado.
     *
     * **Con permiso propio y no `edit`.** Soporte no tiene `edit` y es justo quien ve
     * estas cosas; darles `edit` entero sería abrirles el dominio y la licencia.
     */
    public function abrirRevision(): void
    {
        $this->authorize('admin.environments.review');

        $this->dispatch('openModal',
            component: \App\Livewire\Environments\RevisionModal::class,
            arguments: ['environmentId' => $this->environment->id]
        );
    }

    /** Las modales avisan al terminar, para que la ficha se repinte con el estado nuevo. */
    #[\Livewire\Attributes\On('environmentStatusChanged')]
    #[\Livewire\Attributes\On('environmentSynced')]
    #[\Livewire\Attributes\On('environmentReviewChanged')]
    public function refrescar(): void
    {
        $this->environment->refresh();
    }

    /**
     * Qué le está funcionando a este sitio y qué no: **una fila por acción**, con el
     * resultado de la última de cada una, más el resumen de la ventana de 30 días.
     *
     * **Se cuenta por severidad y no por `http_status`.** Contando todo lo que pasa de
     * 400 como error, la mitad de las «averías» son respuestas correctas: `3002`, `4002`
     * y `5002` significan «no hay contenido publicado para esa versión», y un producto
     * sin features nunca va a tener features aunque su plugin pregunte cada hora. En la
     * base de desarrollo son **7 errores de verdad de 14 «errores»** contados a la
     * antigua. La columna `severity` la calcula el propio modelo al crear la fila, y es
     * la misma fuente que usa Monitorización.
     *
     * @return array{peticiones: \Illuminate\Support\Collection, resumen: array}
     *         `peticiones` es una fila por acción, de la más reciente a la más antigua.
     */
    private function actividadDeLaApi(): array
    {
        $consulta = ApiRequestLog::where('environment_id', $this->environment->id);

        // ============ Una fila por acción, no las últimas N ============
        //
        // **Las últimas seis peticiones en orden cronológico contaban mal la historia.**
        // `licence` son 76 de las 108 peticiones del entorno con datos, así que las seis
        // últimas eran seis `licence` seguidas y las acciones que fallan —`features`,
        // `products`, `scss-cdn`— no aparecían nunca.
        //
        // Y cada acción es **un servicio distinto**: la licencia puede responder bien
        // mientras el contenido no llega. Lo que se pregunta de un sitio es «¿qué le está
        // funcionando y qué no?», y eso es una fila por acción.
        $porAccion = (clone $consulta)
            ->selectRaw("action,
                count(*) as total,
                max(started_at) as ultima,
                sum(case when severity = 'ok' then 1 else 0 end) as n_ok,
                sum(case when severity = 'error' then 1 else 0 end) as n_error,
                sum(case when severity = 'error' and reviewed_at is null then 1 else 0 end) as n_error_pendiente,
                sum(case when severity = 'sin_contenido' then 1 else 0 end) as n_sin_contenido,
                sum(case when severity = 'negocio' then 1 else 0 end) as n_negocio")
            ->groupBy('action')
            ->orderByDesc('ultima')
            ->get();

        // El resultado de la ÚLTIMA de cada acción, en una sola consulta: son seis o siete
        // acciones como máximo, pero una por acción serían seis o siete viajes.
        $ultimas = $porAccion->isEmpty()
            ? collect()
            : (clone $consulta)
                ->whereIn('started_at', $porAccion->pluck('ultima')->all())
                ->orderBy('started_at')
                ->get(['action', 'plugin', 'started_at', 'severity', 'error_code', 'error', 'http_status', 'duration_ms'])
                ->keyBy('action');

        $peticiones = $porAccion->map(function ($fila) use ($ultimas) {
            $ultima = $ultimas[$fila->action] ?? null;

            return [
                'accion' => $fila->action,
                'total' => (int) $fila->total,
                'cuando' => $ultima?->started_at,
                'plugin' => $ultima?->plugin,
                'severidad' => $ultima?->severity,
                'codigo' => (int) ($ultima?->error_code ?? 0),
                'duracion' => $ultima?->duration_ms,
                // La explicación en castellano de lo que le pasa a esa acción, que es lo
                // que no se puede deducir de un número de cuatro cifras.
                'explicacion' => $ultima !== null && $ultima->severity !== Severidad::OK
                    ? SaludDelEntorno::explicar($ultima)
                    : null,
                // El desglose es histórico: cuenta todo. Lo que se pinta en rojo es solo
                // lo pendiente, que es la regla del panel.
                'erroresPendientes' => (int) $fila->n_error_pendiente,
                'desglose' => [
                    Severidad::OK => (int) $fila->n_ok,
                    Severidad::ERROR => (int) $fila->n_error,
                    Severidad::SIN_CONTENIDO => (int) $fila->n_sin_contenido,
                    Severidad::NEGOCIO => (int) $fila->n_negocio,
                ],
            ];
        })->values();

        $desde = now()->subDays(SaludDelEntorno::DIAS_DE_RESUMEN);
        $ventana = (clone $consulta)->where('started_at', '>=', $desde);

        $porSeveridad = (clone $ventana)
            ->selectRaw('severity, count(*) as c')
            ->groupBy('severity')
            ->pluck('c', 'severity');

        return [
            'peticiones' => $peticiones,
            'resumen' => [
                'total' => (int) $porSeveridad->sum(),
                'ok' => (int) ($porSeveridad[Severidad::OK] ?? 0),
                'error' => (int) ($porSeveridad[Severidad::ERROR] ?? 0),
                'sinContenido' => (int) ($porSeveridad[Severidad::SIN_CONTENIDO] ?? 0),
                'negocio' => (int) ($porSeveridad[Severidad::NEGOCIO] ?? 0),
                'ultimoError' => (clone $consulta)
                    ->where('severity', Severidad::ERROR)
                    ->max('started_at'),
                'dias' => SaludDelEntorno::DIAS_DE_RESUMEN,
            ],
        ];
    }

    /**
     * Las claves que trae el **último envío** del plugin, que es lo único que dice qué se
     * sabe hoy de ese Moodle.
     *
     * **Por qué no vale mirar la tabla `data`.** Ahí `policyagreed` y `contactable` están
     * a `0`, y **no vienen en el payload**: ese 0 es el valor por defecto de la columna,
     * no algo que haya dicho el sitio del cliente. Y como el modelo los castea a
     * `boolean`, un null también se lee como `false`. Con cualquiera de los dos criterios
     * la ficha afirmaba «privacidad sin aceptar» y «no contactable» de un cliente del que
     * no sabemos ni una cosa ni la otra.
     *
     * `environment_stats.data` guarda el **payload completo sin mapear** —se guarda así a
     * propósito, para poder responder preguntas que hoy no nos hacemos—, así que es la
     * única fuente de la verdad sobre qué llegó. Y lo que llega hoy son **15 claves,
     * todas de volumen**: usuarios, cursos, matrículas, mensajes, preguntas, recursos,
     * insignias, medias y dispositivos. El perfil del sitio —nombre y teléfono del
     * administrador, país, privacidad, contactable— **no está en el envío diario**.
     *
     * @return array<string,mixed>
     */
    private function ultimoEnvio(): array
    {
        $ultimo = $this->environment->environmentStats()
            ->orderByDesc('date')
            ->first(['id', 'data']);

        return $ultimo === null ? [] : ($ultimo->data ?? []);
    }

    /**
     * El contacto del administrador **según el último envío**, y no lo que arrastre la tabla.
     *
     * **Por qué del payload y no de `data`.** `mapDataFields()` usa `isset()`, así que una
     * clave que llega a `null` —y el plugin manda las tres de contacto vacías— **no
     * sobrescribe** la columna: se queda el valor de la última vez que vino con algo. La
     * ficha llegó a enseñar un correo guardado en mayo como si fuera el de hoy.
     *
     * El payload crudo del último envío es lo único que dice qué afirma el sitio **ahora**.
     * Si no lo manda, no se enseña: un dato de hace cuatro meses presentado como actual es
     * peor que un hueco.
     *
     * @param  array<string,mixed>  $ultimoEnvio
     * @return array{nombre: ?string, email: ?string, telefono: ?string}
     */
    private function contactoDelSitio(array $ultimoEnvio): array
    {
        // Vacío y ausente son lo mismo aquí: las dos cosas significan «no lo dice».
        $valor = function (string $campo) use ($ultimoEnvio): ?string {
            $v = $ultimoEnvio[$campo] ?? null;

            return is_string($v) && trim($v) !== '' ? trim($v) : null;
        };

        return [
            'nombre' => $valor('contactname'),
            'email' => $valor('contactemail'),
            'telefono' => $valor('contactphone'),
        ];
    }

    /**
     * Los ajustes del sitio que **se pueden afirmar**, como frases.
     *
     * Un campo se afirma si viene en el último envío. Lo que hay en la tabla y no viene en
     * el envío es un arrastre —de una versión anterior del plugin, o del Manager
     * antiguo—: puede ser cierto y puede tener dos años, y la ficha no tiene forma de
     * distinguirlo, así que no lo presenta como el estado actual.
     *
     * @param  list<string>  $claves
     * @return list<string>
     */
    private function ajustesDelSitio(array $claves): array
    {
        $datos = $this->environment->data;

        if ($datos === null) {
            return [];
        }

        $frases = [];

        // El idioma es la excepción razonable: no viene en el envío diario, pero tampoco
        // es una afirmación sobre la configuración de privacidad de nadie, y cambia poco.
        if ($datos->getRawOriginal('language')) {
            $frases[] = $datos->language;
        }

        if (in_array('countrycode', $claves, true) && $datos->countrycode) {
            $frases[] = $datos->countrycode;
        }

        // **Presente pero vacía no es «llega».** `mapDataFields()` usa `isset()`: una
        // clave con `null` se descarta como si no viniera, pero `''` y `0` **sí se
        // guardan**. Así que venir en el envío no basta: hay que tener valor.
        if (in_array('policyagreed', $claves, true) && $datos->getRawOriginal('policyagreed') !== null
            && $datos->getRawOriginal('policyagreed') !== '') {
            $frases[] = $datos->policyagreed ? 'privacidad aceptada' : 'privacidad sin aceptar';
        }

        // `contactable` ya no se enseña: no existe en el registro de sitio de Moodle, así
        // que esa columna no se puede rellenar sin inventarla. Ver MGR-077.

        return $frases;
    }

    public function render()
    {
        $entorno = $this->environment;

        $api = $this->actividadDeLaApi();

        $soporte = $entorno->soporteVigente;

        // Qué trae el último envío del plugin: es lo que decide qué se puede
        // afirmar del sitio y qué es un arrastre. Ver `ultimoEnvio()`.
        $ultimoEnvio = $this->ultimoEnvio();
        $claves = array_keys($ultimoEnvio);

        return view('livewire.environments.show', [
            'salud' => SaludDelEntorno::de($entorno),
            'version' => VersionDeMoodle::de($entorno),
            'licencia' => $entorno->licenseToken,
            'productos' => $entorno->licenseToken?->products->unique('id') ?? collect(),
            'peticiones' => $api['peticiones'],
            'resumenApi' => $api['resumen'],
            // El payload del último `sync`: 40 columnas de las que hoy no se enseñaba
            // casi ninguna.
            'datos' => $entorno->data,
            'clavesDelUltimoEnvio' => $claves,
            'ajustesDelSitio' => $this->ajustesDelSitio($claves),
            'contactoDelSitio' => $this->contactoDelSitio($ultimoEnvio),
            'soporte' => $soporte,
            'periodosDeSoporte' => $entorno->supports()->count(),
            'plugins' => [
                'total' => $entorno->plugins()->count(),
                'conActualizacion' => $entorno->plugins()->where('has_updates', true)->count(),
                // **Los que Moodle tiene registrados y no están en el disco.** Va aquí y no
                // solo en el inventario porque esta ficha es donde se mira «cómo está este
                // sitio», y un plugin registrado sin ficheros es lo único de este bloque que
                // ya está roto —lo demás es trabajo planificable—. La regla la define
                // `Plugin::scopeAusenteDelDisco()`, compartida con las otras tres pantallas.
                'ausentes' => $entorno->plugins()->ausenteDelDisco()->count(),
            ],
            // `projectid` no es columna de `environments`: el alta guarda ese valor en
            // `key`, y el que manda el plugin en cada petición queda en el log. Se
            // enseña el último que llegó, que es el que está usando de verdad.
            'projectid' => ApiRequestLog::where('environment_id', $entorno->id)
                ->whereNotNull('projectid')
                ->orderByDesc('started_at')
                ->value('projectid'),
        ])->layout('layouts.app');
    }
}
