<?php

namespace App\Models\Clients;

use App\Models\Auth\User;
use App\Models\Products\LicenseToken;
use App\Models\Environments\Environment;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class Client extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'clients';

    /**
     * Campos que se pueden asignar masivamente.
     */
    protected $fillable = [
        'name',
        'shortname',
        'crm_id',
        'cif',
        'razon_social',
        'fecha_inicio_relacion',
        'description',
        'contacts',
        'jira',
        'actived',
        'metadata',
        'observation',
    ];

    /**
     * Casts para tipos seguros.
     */
    protected $casts = [
        'actived' => 'boolean',
        // La baja NO va en $fillable a proposito: se hace con `darDeBaja()`, que corta
        // licencias y entornos y deja rastro. Un `update(['actived' => false])` desde un
        // formulario se saltaria todo eso.
        'deactivated_at' => 'datetime',
        'metadata'   => 'array',
        'fecha_inicio_relacion' => 'date',
        'contacts' => 'array',
    ];

    /**
     * Accessor para asegurar que contacts siempre sea un array.
     */
    public function getContactsAttribute($value)
    {
        if (is_array($value)) {
            return $value;
        }

        if (is_string($value)) {
            $decoded = json_decode($value, true);
            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    /**
     * Un cliente tiene muchos usuarios vinculados.
     */
    public function users()
    {
        return $this->belongsToMany(
            User::class,
            'client_user',
            'client_id',
            'user_id'
        );
    }

    /**
     * Un cliente tiene muchos entornos.
     */
    public function environments()
    {
        return $this->hasMany(Environment::class);
    }

    /**
     * Un cliente tiene muchos tokens de licencia.
     */
    public function tokens()
    {
        return $this->hasMany(LicenseToken::class);
    }

    /**
     * Estado agregado de las licencias del cliente, en una palabra.
     *
     * Cinco valores, y el primero es la razón de que este método exista:
     *
     * - `nunca` — **no tiene ninguna licencia**, nunca la ha tenido.
     * - `desactivada` — las tiene, y están apagadas.
     * - `vencida`, `pronto`, `activa`.
     *
     * **Lo que había mezclaba los dos primeros.** `getLicenseStatus()` devolvía
     * «desactivada» tanto para un cliente sin licencias como para uno con todas
     * apagadas, y en la base de desarrollo eso son **18 de 20 clientes en la misma
     * cesta**: la columna no separaba nada. Son dos situaciones opuestas —una es un alta
     * sin terminar, la otra una relación que se cortó— y llevan a acciones distintas.
     *
     * Agrega **por lo peor**, no por lo más frecuente: si una sola licencia está
     * vencida, el cliente sale «vencida». Un cliente con nueve licencias bien y una
     * vencida tiene un problema, y promediarlo lo esconde.
     */
    public function estadoDeLasLicencias(): string
    {
        if ($this->tokens->isEmpty()) {
            return 'nunca';
        }

        $estados = $this->tokens->map(fn ($token) => $token->estado());

        foreach (['vencida', 'pronto', 'desactivada'] as $peor) {
            if ($estados->contains($peor)) {
                return $peor;
            }
        }

        return 'activa';
    }

    /**
     * Lo que se lee en la etiqueta de licencias del listado.
     *
     * Si todas están igual, el número y el estado en plural: «3 · Activas». Si están
     * mezcladas, **el desglose**: «1 activa · 1 desactivada». Un cliente con una
     * licencia buena y una vencida no se puede resumir en una palabra sin mentir en una
     * de las dos.
     */
    public function etiquetaDeLasLicencias(): string
    {
        if ($this->tokens->isEmpty()) {
            return '';
        }

        $cuentas = $this->tokens
            ->groupBy(fn ($token) => $token->estado())
            ->map->count();

        $plural = [
            'activa' => 'Activas',
            'pronto' => 'Vencen pronto',
            'vencida' => 'Vencidas',
            'desactivada' => 'Desactivadas',
        ];

        if ($cuentas->count() === 1) {
            return $this->tokens->count() . ' · ' . $plural[$cuentas->keys()->first()];
        }

        $singular = [
            'activa' => 'activa',
            'pronto' => 'vence pronto',
            'vencida' => 'vencida',
            'desactivada' => 'desactivada',
        ];

        return $cuentas
            ->map(fn ($n, $estado) => $n . ' ' . $singular[$estado])
            ->implode(' · ');
    }


    /** Quién dio la baja. `nullOnDelete`: si el usuario se borra, la baja sigue. */
    public function deactivatedBy()
    {
        return $this->belongsTo(User::class, 'deactivated_by');
    }

    /* ==========================================================
       Baja y alta del cliente
       ========================================================== */

    /**
     * Da de baja al cliente y a todo lo suyo.
     *
     * **Por qué no basta con `actived = false`.** El middleware ya cortaba por ahí
     * —«Cliente inactivo»—, pero las licencias y los entornos se quedaban marcados como
     * activos en el panel: la ficha decía «activa» de algo que estaba cortado. Es la
     * misma incoherencia que se arregló para las licencias caducadas, y la que hace que
     * nadie se fíe de lo que ve.
     *
     * **Se recuerda exactamente qué se desactivó**, en `metadata`, para que reactivar no
     * encienda cosas que ya estaban apagadas por otro motivo: una licencia que alguien
     * había desactivado a mano antes de la baja tiene que seguir apagada al volver.
     *
     * Todo en una transacción: una baja a medias —cliente inactivo con licencias
     * activas, o al revés— es peor que no haberla hecho, porque nadie sabe en qué estado
     * está.
     *
     * @return array{licencias: int, entornos: int}
     */
    public function darDeBaja(?string $motivo = null, ?int $usuarioId = null): array
    {
        $licencias = $this->tokens()->where('active', true)->pluck('id')->all();
        $entornos = $this->environments()->where('active', true)->pluck('id')->all();

        DB::transaction(function () use ($licencias, $entornos, $motivo, $usuarioId) {
            if ($licencias !== []) {
                LicenseToken::whereIn('id', $licencias)->update(['active' => false]);
            }

            if ($entornos !== []) {
                Environment::whereIn('id', $entornos)->update(['active' => false]);
            }

            $metadata = $this->metadata ?? [];
            $metadata['baja'] = [
                'licencias' => $licencias,
                'entornos' => $entornos,
                'fecha' => now()->toIso8601String(),
            ];

            $this->forceFill([
                'actived' => false,
                'deactivated_at' => now(),
                'deactivated_by' => $usuarioId,
                'deactivation_reason' => $motivo,
                'metadata' => $metadata,
            ])->save();
        });

        return ['licencias' => count($licencias), 'entornos' => count($entornos)];
    }

    /**
     * Vuelve a dar de alta al cliente.
     *
     * Reactiva **solo lo que la baja apagó**, y de eso, solo las licencias que **siguen
     * en fecha**: reactivar una licencia vencida durante la baja la dejaría «activa y
     * caducada», que es justo el estado que la tarea de caducidad existe para evitar —y
     * que además mandaría un correo esa noche—. Esas se quedan apagadas y la pantalla lo
     * dice.
     *
     * @return array{licencias: int, entornos: int, caducadas: int}
     */
    public function reactivar(?int $usuarioId = null): array
    {
        $registro = $this->metadata['baja'] ?? null;
        $idsLicencias = $registro['licencias'] ?? [];
        $idsEntornos = $registro['entornos'] ?? [];

        $licencias = $idsLicencias === []
            ? collect()
            : LicenseToken::whereIn('id', $idsLicencias)->get();

        $enFecha = $licencias->filter(
            fn (LicenseToken $licencia) => $licencia->end_at === null || $licencia->end_at->isFuture()
        );

        DB::transaction(function () use ($enFecha, $idsEntornos) {
            if ($enFecha->isNotEmpty()) {
                LicenseToken::whereIn('id', $enFecha->pluck('id'))->update(['active' => true]);
            }

            if ($idsEntornos !== []) {
                Environment::whereIn('id', $idsEntornos)->update(['active' => true]);
            }

            $metadata = $this->metadata ?? [];
            unset($metadata['baja']);

            $this->forceFill([
                'actived' => true,
                'deactivated_at' => null,
                'deactivated_by' => null,
                'deactivation_reason' => null,
                'metadata' => $metadata,
            ])->save();
        });

        return [
            'licencias' => $enFecha->count(),
            'entornos' => count($idsEntornos),
            'caducadas' => $licencias->count() - $enFecha->count(),
        ];
    }

    /** Lo que se va a cortar, para poder decirlo ANTES de cortarlo. */
    public function impactoDeLaBaja(): array
    {
        return [
            'licencias' => $this->tokens()->where('active', true)->count(),
            'entornos' => $this->environments()->where('active', true)->count(),
        ];
    }

    /**
     * Lo que se reactivaría, para decirlo antes.
     *
     * @return array{licencias: int, entornos: int, caducadas: int}
     */
    public function impactoDelAlta(): array
    {
        $registro = $this->metadata['baja'] ?? null;
        $ids = $registro['licencias'] ?? [];

        $licencias = $ids === [] ? collect() : LicenseToken::whereIn('id', $ids)->get();
        $enFecha = $licencias->filter(
            fn (LicenseToken $licencia) => $licencia->end_at === null || $licencia->end_at->isFuture()
        );

        return [
            'licencias' => $enFecha->count(),
            'entornos' => count($registro['entornos'] ?? []),
            'caducadas' => $licencias->count() - $enFecha->count(),
        ];
    }

    protected static function booted(): void
    {
        /**
         * Un borrado (en blando) tiene que dejar lo mismo que una baja.
         *
         * El esquema declara `cascadeOnDelete` en `license_tokens.client_id` y
         * `nullOnDelete` en `environments.client_id`, pero **con SoftDeletes el DELETE
         * nunca llega a la base**: las dos cascadas no se disparan jamás. Un cliente
         * borrado dejaba licencias y entornos apuntando a un cliente invisible y
         * marcados como activos.
         *
         * La API sí cortaba —`$licenseToken->client` devuelve null y responde «Cliente
         * inactivo»—, así que el servicio quedaba parado y el panel decía lo contrario.
         */
        static::deleting(function (self $cliente) {
            if ($cliente->isForceDeleting()) {
                return;
            }

            if ($cliente->actived) {
                $cliente->darDeBaja('Cliente borrado', Auth::id());
            }
        });
    }
}

