<?php

namespace App\Models\Products;

use App\Models\Products\LicenseToken;
use App\Models\Environments\Environment;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;

class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'products';

    protected $fillable = [
        'slug',
        'name',
        'summary',
        'image',
        'description',
        'tech',
        'type',
        'metadata',
        'actived',
        'enabled',
    ];

    protected $casts = [
        'metadata' => 'array',
        'actived'  => 'boolean',
        'enabled'  => 'boolean',
    ];

    /* -----------------------------
       Relaciones
    ------------------------------*/

    /**
     * Tokens que tienen asignado este producto.
     */
    public function tokens()
    {
        return $this->belongsToMany(LicenseToken::class, 'license_token_product')
            ->withPivot([
                'mode',
                'status',
                'start_at',
                'end_at',
                'observation',
                'assigned_by',
            ])
            ->withTimestamps();
    }

    /**
     * Query base de entornos que tienen instalado este producto
     * a través de una licencia.
     */
    public function environmentsQuery()
    {
        return Environment::whereHas('licenseToken.products', function ($query) {
            $query->where('products.id', $this->id);
        });
    }

    /**
     * Accesor para `$product->environments`.
     *
     * Devuelve la colección de entornos asociados al producto.
     */
    /**
     * Añade `environments_count` a la consulta, **en una sola consulta para toda la lista**.
     *
     * **El problema que resuelve (MGR-016).** Los entornos de un producto no son una
     * relación de Eloquent: se derivan de sus licencias
     * —`products` → `license_token_product` → `license_tokens` → `environments`— y con el
     * pivote en medio eso no se puede expresar con `hasManyThrough`. Por eso existía
     * `environmentsQuery()`, que devuelve un `Builder` y **no se puede precargar**: el
     * listado de productos hacía **un `count()` por tarjeta**. Con 5 productos son 5
     * consultas de más; con 40, cuarenta.
     *
     * La solución nativa es una **subconsulta correlacionada**, el mismo patrón que usa
     * el listado de usuarios para la última sesión: una columna calculada por el motor
     * en la misma pasada.
     *
     * **El `whereNull('deleted_at')` del pivote no es opcional**: un producto quitado de
     * una licencia está borrado en blando ahí, y sin el filtro los entornos de esa
     * licencia seguirían contando. Es la misma razón por la que `LicenseToken::products()`
     * lleva `wherePivotNull('deleted_at')`.
     */
    public function scopeWithEnvironmentsCount(Builder $query): Builder
    {
        return $query->addSelect(['environments_count' => Environment::query()
            ->selectRaw('count(*)')
            ->whereIn('environments.license_token_id', function ($sub) {
                $sub->select('license_token_id')
                    ->from('license_token_product')
                    ->whereColumn('license_token_product.product_id', 'products.id')
                    ->whereNull('license_token_product.deleted_at');
            }),
        ]);
    }

    public function getEnvironmentsAttribute()
    {
        return $this->environmentsQuery()->get();
    }

    /**
     * Tipos de entornos válidos para este producto
     */
    public function types()
    {
        return $this->belongsToMany(\App\Models\Environments\Type::class, 'product_type');
    }

    /**
     * Setups de configuración para este producto
     */
    public function setups()
    {
        return $this->hasMany(\App\Models\Setups\Setup::class);
    }

    /**
     * Versiones de Features para este producto
     */
    public function featureVersions()
    {
        return $this->hasMany(\App\Models\Features\FeatureVersion::class);
    }

    /**
     * Features (funcionalidades) de este producto (a través de versiones)
     */
    public function features()
    {
        return $this->hasManyThrough(\App\Models\Features\Feature::class, \App\Models\Features\FeatureVersion::class, 'product_id', 'feature_version_id');
    }

    /**
     * Versiones de Tutoriales para este producto
     */
    public function tutorialVersions()
    {
        return $this->hasMany(\App\Models\Tutorials\TutorialVersion::class);
    }

    /**
     * Versiones de Recursos para este producto
     */
    public function resourceVersions()
    {
        return $this->hasMany(\App\Models\Resources\ResourceVersion::class);
    }

    /**
     * Recursos de este producto (a través de versiones)
     */
    public function resources()
    {
        return $this->hasManyThrough(\App\Models\Resources\Resource::class, \App\Models\Resources\ResourceVersion::class, 'product_id', 'resource_version_id');
    }

    /**
     * Versiones SCSS para este producto
     */
    public function scssVersions()
    {
        return $this->hasMany(\App\Models\Scss\ScssVersion::class);
    }

    /**
     * Archivos SCSS para este producto (a través de versiones)
     * Helper para obtener todos los archivos de todas las versiones
     */
    public function getScssFilesProperty()
    {
        return \App\Models\Scss\ScssFile::whereIn('scss_version_id', function ($query) {
            $query->select('id')
                ->from('scss_versions')
                ->where('product_id', $this->id);
        })->get();
    }

    /**
     * Versiones JS para este producto
     */
    public function jsVersions()
    {
        return $this->hasMany(\App\Models\Js\JsVersion::class);
    }

    /**
     * Archivos JS para este producto (a través de versiones)
     * Helper para obtener todos los archivos de todas las versiones
     */
    public function getJsFilesProperty()
    {
        return \App\Models\Js\JsFile::whereIn('js_version_id', function ($query) {
            $query->select('id')
                ->from('js_versions')
                ->where('product_id', $this->id);
        })->get();
    }

    /* -----------------------------
       Helpers técnicos
    ------------------------------*/

    public function isMoodle(): bool
    {
        return $this->tech === 'moodle';
    }

    public function isWordPress(): bool
    {
        return $this->tech === 'wordpress';
    }

    public function isEnabledForSale(): bool
    {
        return $this->enabled === true;
    }

    public function isOperational(): bool
    {
        return $this->actived === true;
    }

    /* -----------------------------
       Vocabulario de `tech` y `type`
    ------------------------------*/

    /**
     * Los prefijos que Moodle reconoce como tipo de plugin.
     *
     * No es la lista completa de Moodle —son decenas— sino los tipos con los que puede
     * venir un producto nuestro. Añadir uno aquí es lo que hace falta el día que
     * empaquetemos, por ejemplo, un `qtype_`.
     */
    public const TIPOS_DE_PLUGIN = [
        'mod', 'block', 'local', 'theme', 'report', 'format', 'tool', 'auth',
        'enrol', 'filter', 'qtype', 'editor', 'repository', 'availability',
        'atto', 'tiny', 'customfield', 'profilefield', 'gradereport', 'webservice',
    ];

    /**
     * ¿El `slug` de este producto puede ser un componente instalado en un Moodle?
     *
     * **No es una heurística, es un hecho comprobable.** El inventario de un sitio
     * (`plugins.component`) lo rellena Moodle con `tipo_nombre`, y `tipo` sale de su lista
     * de tipos de plugin. Un producto cuyo prefijo no está en esa lista —`fresk_premium`,
     * que es un ecosistema comercial y no un plugin— **no puede coincidir nunca** con una
     * fila de ese inventario. Compararlo solo produce ruido: sale eternamente como
     * «licenciado y sin instalar» de todos los clientes que lo tienen contratado, y no hay
     * nada que instalar.
     *
     * **Se mira el `slug` y no la columna `type`** aunque ahí ponga «ecosistema»: `type` es
     * una etiqueta libre, admite null y el propio formulario deja escribir lo que sea (ver
     * {@see self::tiposSugeridos()}). Colgar de ella qué productos se comparan la
     * convertiría en un campo con consecuencias sin que nada lo diga en pantalla.
     */
    public function esPluginDeMoodle(): bool
    {
        if (! $this->slug || ! str_contains($this->slug, '_')) {
            return false;
        }

        return in_array(strtok($this->slug, '_'), self::TIPOS_DE_PLUGIN, true);
    }

    /**
     * Los tipos de producto que se sugieren en el formulario.
     *
     * **Sugerencias y no una lista cerrada.** El formulario tenía un `<select>` con cinco
     * opciones —`theme`, `block`, `local`, `report`, `format`— y el campo era
     * **obligatorio**, así que un producto que no es un plugin de Moodle —`fresk_premium`,
     * un agrupamiento comercial, un ecosistema— no se podía dar de alta sin mentir.
     *
     * Y la columna es **nullable a propósito**: el comentario de `create_products_table`
     * dice «tipo dentro de Moodle; en otros tech se quedará null». El formulario contradecía
     * a la base.
     *
     * `type` **no interviene en nada**: no lo lee la API, no empareja productos con nada y
     * no decide qué se sirve. Se pinta en el listado y en la ficha, y eso es todo. Así que
     * lo correcto es que sea una etiqueta libre con memoria: se ofrecen los tipos de plugin
     * de Moodle y **los que ya se hayan usado**, y quien necesite otro lo escribe.
     *
     * @return list<string>
     */
    public static function tiposSugeridos(): array
    {
        return self::vocabulario('type', ['theme', 'block', 'local', 'report', 'format', 'mod']);
    }

    /**
     * Las tecnologías que se sugieren.
     *
     * Igual que `tiposSugeridos()`: el campo ya era texto libre, así que lo único que
     * cambia es que ahora se ofrece lo que hay en lugar de tener que recordarlo.
     *
     * @return list<string>
     */
    public static function tecnologiasSugeridas(): array
    {
        return self::vocabulario('tech', ['moodle', 'wordpress', 'saas', 'api']);
    }

    /**
     * Los valores usados en una columna, unidos a una lista base, sin repetir.
     *
     * `withTrashed()` porque un producto borrado también dejó su vocabulario: si alguien
     * usó «ecosistema» y ese producto se retiró, la palabra sigue siendo la de la casa.
     *
     * @param  list<string>  $base
     * @return list<string>
     */
    private static function vocabulario(string $columna, array $base): array
    {
        $usados = self::withTrashed()
            ->whereNotNull($columna)
            ->where($columna, '!=', '')
            ->distinct()
            ->pluck($columna)
            ->all();

        $todos = array_values(array_unique(array_merge($base, $usados)));

        sort($todos);

        return $todos;
    }

    /* -----------------------------
       Accessors
    ------------------------------*/

    /**
     * Obtiene la URL pública de la imagen desde storage
     */
    public function getImageUrlAttribute(): ?string
    {
        if (!$this->image) {
            return null;
        }

        return Storage::disk('public')->url($this->image);
    }
}

