<?php

namespace App\Models;

use App\Services\ThumbnailService;
use App\Support\ArtworkFeaturedImagePath;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Laravel\Scout\Searchable;
use Laravel\Scout\SearchableScope;

/**
 * App\Models\Artwork
 *
 * @property-read User $user
 * @property-read \Illuminate\Database\Eloquent\Collection|ArtworkTranslation[] $translations
 * @property-read ArtworkStats $stats
 * @property-read \Illuminate\Database\Eloquent\Collection|ArtworkComment[] $comments
 * @property-read \Illuminate\Database\Eloquent\Collection|ArtworkDownload[] $downloads
 */
class Artwork extends Model
{
    use HasFactory, Searchable, SoftDeletes;

    /**
     * Override Scout's bootSearchable to skip the ModelObserver (which fires MakeSearchable
     * on every save). We still register SearchableScope and Builder macros so that
     * scout:import and Builder::searchable() continue to work.
     * All indexing is managed explicitly via IndexArtworkJob.
     */
    public static function bootSearchable(): void
    {
        static::addGlobalScope(new SearchableScope);
        (new static)->registerSearchableMacros();
        // ModelObserver intentionally omitted — indexing is handled by IndexArtworkJob.
    }

    public const PUBLISHED_AS_USER = 'user';

    public const PUBLISHED_AS_GROUP = 'group';

    public const VISIBILITY_PUBLIC = 'public';

    public const VISIBILITY_UNLISTED = 'unlisted';

    public const VISIBILITY_PRIVATE = 'private';

    protected $table = 'artworks';

    protected $fillable = [
        'user_id',
        'group_id',
        'uploaded_by_user_id',
        'primary_author_user_id',
        'published_as_type',
        'published_as_id',
        'title',
        'slug',
        'description',
        'file_name',
        'file_path',
        'hash',
        'file_ext',
        'thumb_ext',
        'has_missing_thumbnails',
        'missing_thumbnail_variants_json',
        'thumbnails_checked_at',
        'file_size',
        'mime_type',
        'width',
        'height',
        'is_public',
        'visibility',
        'is_approved',
        'is_mature',
        'maturity_level',
        'maturity_source',
        'maturity_status',
        'maturity_ai_score',
        'maturity_ai_labels',
        'maturity_ai_label',
        'maturity_ai_confidence',
        'maturity_ai_model',
        'maturity_ai_threshold_used',
        'maturity_ai_analysis_time_ms',
        'maturity_ai_action_hint',
        'maturity_ai_advisory',
        'maturity_ai_status',
        'maturity_ai_detected_at',
        'maturity_declared_at',
        'maturity_flagged_at',
        'maturity_flag_reason',
        'maturity_reviewed_by',
        'maturity_reviewed_at',
        'maturity_reviewer_note',
        'maturity_mismatch_count',
        'published_at',
        'hash',
        'thumb_ext',
        'file_ext',
        'clip_tags_json',
        'blip_caption',
        'yolo_objects_json',
        'vision_metadata_updated_at',
        'last_vector_indexed_at',
        'ai_status',
        'title_source',
        'description_source',
        'tags_source',
        'category_source',
        // Versioning
        'current_version_id',
        'version_count',
        'version_updated_at',
        'requires_reapproval',
        // Scheduled publishing
        'publish_at',
        'artwork_status',
        'artwork_timezone',
    ];

    protected $casts = [
        'is_public' => 'boolean',
        'visibility' => 'string',
        'is_approved' => 'boolean',
        'is_mature' => 'boolean',
        'maturity_level' => 'string',
        'maturity_source' => 'string',
        'maturity_status' => 'string',
        'maturity_ai_score' => 'float',
        'maturity_ai_labels' => 'array',
        'maturity_ai_label' => 'string',
        'maturity_ai_confidence' => 'float',
        'maturity_ai_model' => 'string',
        'maturity_ai_threshold_used' => 'float',
        'maturity_ai_analysis_time_ms' => 'integer',
        'maturity_ai_action_hint' => 'string',
        'maturity_ai_advisory' => 'string',
        'maturity_ai_status' => 'string',
        'maturity_ai_detected_at' => 'datetime',
        'maturity_declared_at' => 'datetime',
        'maturity_flagged_at' => 'datetime',
        'maturity_reviewed_at' => 'datetime',
        'maturity_mismatch_count' => 'integer',
        'has_missing_thumbnails' => 'boolean',
        'published_at' => 'datetime',
        'missing_thumbnail_variants_json' => 'array',
        'thumbnails_checked_at' => 'datetime',
        'published_as_type' => 'string',
        'published_as_id' => 'integer',
        'publish_at' => 'datetime',
        'clip_tags_json' => 'array',
        'yolo_objects_json' => 'array',
        'vision_metadata_updated_at' => 'datetime',
        'last_vector_indexed_at' => 'datetime',
        'ai_status' => 'string',
        'title_source' => 'string',
        'description_source' => 'string',
        'tags_source' => 'string',
        'category_source' => 'string',
        'version_updated_at' => 'datetime',
        'requires_reapproval' => 'boolean',
    ];

    /**
     * Thumbnail sizes and their options.
     * Keys are the size dir used in the CDN URL.
     */
    protected const THUMB_SIZES = [
        'sm' => ['height' => 240, 'quality' => 78, 'dir' => 'sm'],
        'md' => ['height' => 360, 'quality' => 82, 'dir' => 'md'],
        'lg' => ['height' => 1200, 'quality' => 85, 'dir' => 'lg'],
        'xl' => ['height' => 2400, 'quality' => 90, 'dir' => 'xl'],
    ];

    /**
     * Build the thumbnail URL for this artwork.
     * Returns null when no hash or thumb_ext is available.
     */
    public function thumbUrl(string $size = 'md'): ?string
    {
        if (empty($this->hash) || empty($this->thumb_ext)) {
            return null;
        }

        $sizeKey = array_key_exists($size, self::THUMB_SIZES) ? $size : 'md';

        return ThumbnailService::fromHash($this->hash, $this->thumb_ext, $sizeKey);
    }

    /**
     * Accessor for `$art->thumb` used in legacy views (default medium size).
     */
    public function getThumbAttribute(): string
    {
        return $this->thumbUrl('md') ?? 'https://files.skinbase.org/default/missing_md.webp';
    }

    /**
     * Accessor for `$art->thumb_url` used in some views.
     */
    public function getThumbUrlAttribute(): ?string
    {
        return $this->thumbUrl('md');
    }

    /**
     * Backwards-compatible alias used by legacy views: `$art->thumbnail_url`.
     * Prefer CDN thumbnail URL, then legacy `thumb` accessor, finally a placeholder.
     */
    public function getThumbnailUrlAttribute(): ?string
    {
        $url = $this->getThumbUrlAttribute();
        if (! empty($url)) {
            return $url;
        }
        $thumb = $this->getThumbAttribute();
        if (! empty($thumb)) {
            return $thumb;
        }

        return '/images/placeholder.jpg';
    }

    /**
     * Provide a responsive `srcset` for legacy views.
     */
    public function getThumbSrcsetAttribute(): ?string
    {
        if (empty($this->hash) || empty($this->thumb_ext)) {
            return null;
        }
        $sm = $this->thumbUrl('sm');
        $md = $this->thumbUrl('md');
        if (! $sm || ! $md) {
            return null;
        }

        return $sm.' 320w, '.$md.' 600w';
    }

    public function featuredThumbnailObjectPath(string $variant = 'desktop'): ?string
    {
        if (empty($this->hash)) {
            return null;
        }

        return app(ArtworkFeaturedImagePath::class)->objectPath($this, $variant);
    }

    public function featuredThumbnailUrl(string $variant = 'desktop'): string
    {
        $helper = app(ArtworkFeaturedImagePath::class);

        foreach ($helper->preferredVariantOrder($variant) as $candidate) {
            if ($this->hasFeaturedThumbnail($candidate)) {
                return $helper->url($this, $candidate);
            }
        }

        return $this->thumbUrl('xl')
            ?? $this->thumbUrl('lg')
            ?? 'https://files.skinbase.org/default/missing_xl.webp';
    }

    public function hasFeaturedThumbnail(?string $variant = null): bool
    {
        if (empty($this->hash)) {
            return false;
        }

        $helper = app(ArtworkFeaturedImagePath::class);
        $variants = $variant !== null ? [$helper->normalizeVariant($variant)] : $helper->variantNames();
        $disk = Storage::disk((string) config('uploads.object_storage.disk', 's3'));

        foreach ($variants as $variantName) {
            if ($disk->exists($helper->objectPath($this, $variantName))) {
                return true;
            }
        }

        return false;
    }

    public function hasAllFeaturedThumbnails(): bool
    {
        $helper = app(ArtworkFeaturedImagePath::class);

        foreach ($helper->variantNames() as $variant) {
            if (! $this->hasFeaturedThumbnail($variant)) {
                return false;
            }
        }

        return true;
    }

    public function featuredImageAltText(): string
    {
        $title = trim((string) ($this->title ?? ''));
        $author = trim((string) ($this->user?->name ?? ''));

        if ($title !== '' && $author !== '') {
            return sprintf('%s by %s', $title, $author);
        }

        if ($title !== '') {
            return $title;
        }

        return 'Featured artwork';
    }

    // Relations
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function group(): BelongsTo
    {
        return $this->belongsTo(Group::class);
    }

    public function maturityAuditFinding(): HasOne
    {
        return $this->hasOne(ArtworkMaturityAuditFinding::class);
    }

    public function uploadedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'uploaded_by_user_id');
    }

    public function primaryAuthor(): BelongsTo
    {
        return $this->belongsTo(User::class, 'primary_author_user_id');
    }

    public function contributors(): HasMany
    {
        return $this->hasMany(ArtworkContributor::class)->orderBy('sort_order');
    }

    public function worldSubmissions(): HasMany
    {
        return $this->hasMany(WorldSubmission::class)->orderByDesc('reviewed_at')->orderByDesc('created_at');
    }

    public function worldRewardGrants(): HasMany
    {
        return $this->hasMany(WorldRewardGrant::class)->orderByDesc('granted_at')->orderByDesc('id');
    }

    public function isPublishedByGroup(): bool
    {
        return $this->publishedAsType() === self::PUBLISHED_AS_GROUP;
    }

    public function publishedAsType(): string
    {
        if (in_array($this->published_as_type, [self::PUBLISHED_AS_USER, self::PUBLISHED_AS_GROUP], true)) {
            return (string) $this->published_as_type;
        }

        return (int) ($this->group_id ?? 0) > 0 ? self::PUBLISHED_AS_GROUP : self::PUBLISHED_AS_USER;
    }

    public function publishedAsId(): int
    {
        if ((int) ($this->published_as_id ?? 0) > 0) {
            return (int) $this->published_as_id;
        }

        return $this->publishedAsType() === self::PUBLISHED_AS_GROUP
            ? (int) ($this->group_id ?? 0)
            : (int) $this->user_id;
    }

    public function translations(): HasMany
    {
        return $this->hasMany(ArtworkTranslation::class);
    }

    public function stats(): HasOne
    {
        return $this->hasOne(ArtworkStats::class, 'artwork_id');
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class, 'artwork_category', 'artwork_id', 'category_id');
    }

    public function collections(): BelongsToMany
    {
        return $this->belongsToMany(Collection::class, 'collection_artwork', 'artwork_id', 'collection_id')
            ->withPivot(['order_num'])
            ->withTimestamps()
            ->orderByPivot('order_num');
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class, 'artwork_tag', 'artwork_id', 'tag_id')
            ->withPivot(['source', 'confidence']);
    }

    public function comments(): HasMany
    {
        return $this->hasMany(ArtworkComment::class);
    }

    public function downloads(): HasMany
    {
        return $this->hasMany(ArtworkDownload::class);
    }

    public function embeddings(): HasMany
    {
        return $this->hasMany(ArtworkEmbedding::class, 'artwork_id');
    }

    public function similarities(): HasMany
    {
        return $this->hasMany(ArtworkSimilarity::class, 'artwork_id');
    }

    public function features(): HasMany
    {
        return $this->hasMany(ArtworkFeature::class, 'artwork_id');
    }

    /** All favourite pivot rows for this artwork. */
    public function favourites(): HasMany
    {
        return $this->hasMany(ArtworkFavourite::class, 'artwork_id');
    }

    /** Users who have favourited this artwork (many-to-many shortcut). */
    public function favouritedBy(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'artwork_favourites', 'artwork_id', 'user_id')
            ->withPivot('legacy_id')
            ->withTimestamps();
    }

    public function awards(): HasMany
    {
        return $this->hasMany(ArtworkAward::class);
    }

    public function medals(): HasMany
    {
        return $this->hasMany(ArtworkMedal::class, 'artwork_id');
    }

    /** All file versions for this artwork (oldest first). */
    public function versions(): HasMany
    {
        return $this->hasMany(ArtworkVersion::class)->orderBy('version_number');
    }

    public function outgoingEvolutionRelations(): HasMany
    {
        return $this->hasMany(ArtworkRelation::class, 'source_artwork_id')
            ->orderBy('sort_order')
            ->orderBy('id');
    }

    public function incomingEvolutionRelations(): HasMany
    {
        return $this->hasMany(ArtworkRelation::class, 'target_artwork_id')
            ->orderByDesc('updated_at')
            ->orderByDesc('id');
    }

    /** The currently active version record. */
    public function currentVersion(): BelongsTo
    {
        return $this->belongsTo(ArtworkVersion::class, 'current_version_id');
    }

    public function artworkAiAssist(): HasOne
    {
        return $this->hasOne(ArtworkAiAssist::class, 'artwork_id');
    }

    public function awardStat(): HasOne
    {
        return $this->hasOne(ArtworkAwardStat::class);
    }

    public function medalStats(): HasOne
    {
        return $this->hasOne(ArtworkMedalStat::class, 'artwork_id');
    }

    /**
     * Build the Meilisearch document for this artwork.
     * Includes all fields required for search, filtering, sorting, and display.
     */
    public function toSearchableArray(): array
    {
        $this->loadMissing(['user', 'group', 'tags', 'categories.contentType', 'stats', 'awardStat']);

        $stat = $this->stats;
        $awardStat = $this->awardStat;
        $publishedSortAt = $this->published_at ?? $this->created_at;
        $sortedCategories = $this->categories->sortBy(
            fn ($category) => sprintf(
                '%010d|%s|%010d',
                (int) ($category->sort_order ?? 999999999),
                strtolower((string) ($category->name ?? '')),
                (int) ($category->id ?? 0)
            )
        )->values();

        // Orientation derived from pixel dimensions
        $orientation = 'square';
        if ($this->width && $this->height) {
            if ($this->width > $this->height) {
                $orientation = 'landscape';
            } elseif ($this->height > $this->width) {
                $orientation = 'portrait';
            }
        }

        // Resolution string e.g. "1920x1080"
        $resolution = ($this->width && $this->height)
            ? $this->width.'x'.$this->height
            : '';

        // Primary category slug follows the same sort_order-first semantics used by page presenters.
        $primaryCategory = $sortedCategories->first();
        $categorySlugs = $sortedCategories
            ->pluck('slug')
            ->filter()
            ->map(static fn ($slug) => (string) $slug)
            ->unique()
            ->values()
            ->all();
        $contentTypeSlugs = $sortedCategories
            ->map(static fn ($category) => $category->contentType?->slug)
            ->filter()
            ->map(static fn ($slug) => (string) $slug)
            ->unique()
            ->values()
            ->all();
        $category = $primaryCategory?->slug ?? '';
        $content_type = $primaryCategory?->contentType?->slug ?? '';

        // Tag slugs array
        $tags = $this->tags->pluck('slug')->values()->all();

        return [
            'id' => $this->id,
            'slug' => $this->slug,
            'title' => $this->title,
            'description' => (string) ($this->description ?? ''),
            'author_id' => $this->publishedAsId(),
            'author_name' => $this->group?->name ?? $this->user?->name ?? 'Skinbase',
            'published_as_type' => $this->publishedAsType(),
            'category' => $category,
            'categories' => $categorySlugs,
            'content_type' => $content_type,
            'content_types' => $contentTypeSlugs,
            'tags' => $tags,
            'ai_clip_tags' => collect((array) ($this->clip_tags_json ?? []))
                ->map(static fn ($row) => is_array($row) ? (string) ($row['tag'] ?? '') : '')
                ->filter()
                ->values()
                ->all(),
            'ai_blip_caption' => (string) ($this->blip_caption ?? ''),
            'ai_yolo_objects' => collect((array) ($this->yolo_objects_json ?? []))
                ->map(static fn ($row) => is_array($row) ? (string) ($row['tag'] ?? '') : '')
                ->filter()
                ->values()
                ->all(),
            'resolution' => $resolution,
            'orientation' => $orientation,
            'downloads' => (int) ($stat?->downloads ?? 0),
            'likes' => (int) ($stat?->favorites ?? 0),
            'views' => (int) ($stat?->views ?? 0),
            'created_at' => $publishedSortAt?->toDateString() ?? '',
            'published_at_ts' => $publishedSortAt?->getTimestamp() ?? 0,
            'is_public' => (bool) $this->is_public,
            'is_approved' => (bool) $this->is_approved,
            'is_mature' => (bool) $this->is_mature,
            'is_mature_effective' => (bool) ($this->is_mature || $this->maturity_level === 'mature' || $this->maturity_status === 'suspected'),
            'maturity_level' => (string) ($this->maturity_level ?? 'safe'),
            'maturity_status' => (string) ($this->maturity_status ?? 'clear'),
            'has_missing_thumbnails' => (bool) ($this->has_missing_thumbnails ?? false),
            'missing_thumbnail_rank' => (int) (($this->has_missing_thumbnails ?? false) ? 1 : 0),
            // ── Trending / discovery fields ────────────────────────────────────
            'trending_score_1h' => (float) ($this->trending_score_1h ?? 0),
            'trending_score_24h' => (float) ($this->trending_score_24h ?? 0),
            'trending_score_7d' => (float) ($this->trending_score_7d ?? 0),
            'favorites_count' => (int) ($stat?->favorites ?? 0),
            'awards_received_count' => (int) ($awardStat?->score_total ?? 0),
            'awards_score_7d' => (int) ($awardStat?->score_7d ?? 0),
            'awards_score_30d' => (int) ($awardStat?->score_30d ?? 0),
            'downloads_count' => (int) ($stat?->downloads ?? 0),
            // ── Ranking V2 fields ───────────────────────────────────────────────
            'ranking_score' => (float) ($stat?->ranking_score ?? 0),
            'engagement_velocity' => (float) ($stat?->engagement_velocity ?? 0),
            'shares_count' => (int) ($stat?->shares_count ?? 0),
            'comments_count' => (int) ($stat?->comments_count ?? 0),
            // ── Rising / Heat fields ────────────────────────────────────────────────────
            'heat_score' => (float) ($stat?->heat_score ?? 0),
            'awards' => [
                'gold' => $awardStat?->gold_count ?? 0,
                'silver' => $awardStat?->silver_count ?? 0,
                'bronze' => $awardStat?->bronze_count ?? 0,
                'score' => $awardStat?->score_total ?? 0,
                'score_7d' => $awardStat?->score_7d ?? 0,
                'score_30d' => $awardStat?->score_30d ?? 0,
            ],
        ];
    }

    // Scopes
    public function scopePublic(Builder $query): Builder
    {
        // Compose approved() so behavior is consistent and composable
        $table = $this->getTable();

        return $query->approved()->where("{$table}.is_public", true);
    }

    public function scopeApproved(Builder $query): Builder
    {
        // Respect soft deletes and mark approved content
        $table = $this->getTable();

        return $query->whereNull("{$table}.deleted_at")->where("{$table}.is_approved", true);
    }

    public function scopePublished(Builder $query): Builder
    {
        // Respect soft deletes and only include published items up to now
        $table = $this->getTable();

        return $query->whereNull("{$table}.deleted_at")
            ->whereNotNull("{$table}.published_at")
            ->where("{$table}.published_at", '<=', now());
    }

    public function scopeCatalogVisible(Builder $query): Builder
    {
        $table = $this->getTable();

        return $query
            ->approved()
            ->where("{$table}.is_public", true)
            ->where(function (Builder $visibilityQuery) use ($table): void {
                $visibilityQuery->whereNull("{$table}.visibility")
                    ->orWhere("{$table}.visibility", self::VISIBILITY_PUBLIC);
            })
            ->published();
    }

    public function scopeSafeForGeneralAudience(Builder $query): Builder
    {
        $table = $this->getTable();

        return $query
            ->whereRaw('COALESCE('.$table.'.is_mature, 0) = 0')
            ->whereRaw('COALESCE('.$table.".maturity_status, 'clear') != ?", ['suspected']);
    }

    public function scopeWithoutMissingThumbnails(Builder $query): Builder
    {
        $table = $this->getTable();

        return $query->where(function (Builder $thumbnailQuery) use ($table): void {
            $thumbnailQuery->whereNull("{$table}.has_missing_thumbnails")
                ->orWhere("{$table}.has_missing_thumbnails", false);
        });
    }

    public function scopeOrderMissingThumbnailsLast(Builder $query): Builder
    {
        $table = $this->getTable();

        return $query->orderByRaw("CASE WHEN {$table}.has_missing_thumbnails = 1 THEN 1 ELSE 0 END ASC");
    }

    public function getRouteKeyName(): string
    {
        return 'slug';
    }

    protected static function booted(): void
    {
        static::saving(function (Artwork $artwork): void {
            if ($artwork->published_at === null) {
                return;
            }

            $publishedAt = $artwork->published_at->copy();

            if ($artwork->created_at === null || ! $artwork->created_at->equalTo($publishedAt)) {
                $artwork->created_at = $publishedAt;
            }
        });

        static::deleting(function (Artwork $artwork): void {
            if (! method_exists($artwork, 'isForceDeleting') || ! $artwork->isForceDeleting()) {
                return;
            }

            // Cleanup pivot rows and decrement usage counts on force delete.
            $tagIds = DB::table('artwork_tag')->where('artwork_id', $artwork->id)->pluck('tag_id')->all();
            if ($tagIds === []) {
                return;
            }

            DB::table('artwork_tag')->where('artwork_id', $artwork->id)->delete();
            DB::table('tags')
                ->whereIn('id', $tagIds)
                ->update(['usage_count' => DB::raw('CASE WHEN usage_count > 0 THEN usage_count - 1 ELSE 0 END')]);
        });
    }
}
