<?php

declare(strict_types=1);

namespace App\Models;

use cPad\Plugins\News\Models\NewsArticle;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;

class World extends Model
{
    use HasFactory;
    use SoftDeletes;

    protected static array $canonicalRecurrenceEditionIds = [];

    public const STATUS_DRAFT = 'draft';
    public const STATUS_PUBLISHED = 'published';
    public const STATUS_ARCHIVED = 'archived';

    public const RECAP_STATUS_DRAFT = 'draft';
    public const RECAP_STATUS_PUBLISHED = 'published';

    public const TYPE_SEASONAL = 'seasonal';
    public const TYPE_EVENT = 'event';
    public const TYPE_CAMPAIGN = 'campaign';
    public const TYPE_TRIBUTE = 'tribute';

    public const PARTICIPATION_MODE_MANUAL_APPROVAL = 'manual_approval';
    public const PARTICIPATION_MODE_AUTO_ADD = 'auto_add';
    public const PARTICIPATION_MODE_CLOSED = 'closed';

    protected $fillable = [
        'title',
        'slug',
        'tagline',
        'summary',
        'teaser_title',
        'teaser_summary',
        'description',
        'cover_path',
        'teaser_image_path',
        'theme_key',
        'accent_color',
        'accent_color_secondary',
        'background_motif',
        'icon_name',
        'status',
        'type',
        'starts_at',
        'ends_at',
        'promotion_starts_at',
        'promotion_ends_at',
        'submission_starts_at',
        'submission_ends_at',
        'is_featured',
        'is_active_campaign',
        'is_homepage_featured',
        'campaign_priority',
        'accepts_submissions',
        'participation_mode',
        'submission_note_enabled',
        'community_section_enabled',
        'allow_readd_after_removal',
        'is_recurring',
        'recurrence_key',
        'recurrence_rule',
        'edition_year',
        'cta_label',
        'cta_url',
        'badge_label',
        'campaign_label',
        'badge_description',
        'submission_guidelines',
        'badge_url',
        'seo_title',
        'seo_description',
        'og_image_path',
        'related_tags_json',
        'section_order_json',
        'section_visibility_json',
        'parent_world_id',
        'linked_challenge_id',
        'show_linked_challenge_section',
        'show_linked_challenge_entries',
        'show_linked_challenge_winners',
        'show_linked_challenge_finalists',
        'auto_grant_challenge_world_rewards',
        'challenge_teaser_override',
        'hidden_linked_challenge_artwork_ids_json',
        'created_by_user_id',
        'published_at',
        'recap_status',
        'recap_title',
        'recap_summary',
        'recap_intro',
        'recap_editor_note',
        'recap_cover_path',
        'recap_article_id',
        'recap_stats_snapshot_json',
        'recap_published_at',
    ];

    protected $casts = [
        'starts_at' => 'datetime',
        'ends_at' => 'datetime',
        'promotion_starts_at' => 'datetime',
        'promotion_ends_at' => 'datetime',
        'submission_starts_at' => 'datetime',
        'submission_ends_at' => 'datetime',
        'published_at' => 'datetime',
        'is_featured' => 'boolean',
        'is_active_campaign' => 'boolean',
        'is_homepage_featured' => 'boolean',
        'campaign_priority' => 'integer',
        'accepts_submissions' => 'boolean',
        'allow_readd_after_removal' => 'boolean',
        'submission_note_enabled' => 'boolean',
        'community_section_enabled' => 'boolean',
        'is_recurring' => 'boolean',
        'edition_year' => 'integer',
        'linked_challenge_id' => 'integer',
        'show_linked_challenge_section' => 'boolean',
        'show_linked_challenge_entries' => 'boolean',
        'show_linked_challenge_winners' => 'boolean',
        'show_linked_challenge_finalists' => 'boolean',
        'auto_grant_challenge_world_rewards' => 'boolean',
        'hidden_linked_challenge_artwork_ids_json' => 'array',
        'related_tags_json' => 'array',
        'section_order_json' => 'array',
        'section_visibility_json' => 'array',
        'recap_article_id' => 'integer',
        'recap_stats_snapshot_json' => 'array',
        'recap_published_at' => 'datetime',
    ];

    protected static function booted(): void
    {
        $flushRecurrenceCache = static function (): void {
            static::$canonicalRecurrenceEditionIds = [];
        };

        static::saved($flushRecurrenceCache);
        static::deleted($flushRecurrenceCache);
        static::restored($flushRecurrenceCache);
    }

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

    public function parentWorld(): BelongsTo
    {
        return $this->belongsTo(self::class, 'parent_world_id');
    }

    public function linkedChallenge(): BelongsTo
    {
        return $this->belongsTo(GroupChallenge::class, 'linked_challenge_id');
    }

    public function recapArticle(): BelongsTo
    {
        return $this->belongsTo(NewsArticle::class, 'recap_article_id');
    }

    public function archiveEditions(): HasMany
    {
        return $this->hasMany(self::class, 'parent_world_id')->orderByDesc('edition_year')->orderByDesc('starts_at');
    }

    public function worldRelations(): HasMany
    {
        return $this->hasMany(WorldRelation::class)->orderBy('section_key')->orderBy('sort_order')->orderBy('id');
    }

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

    public function editorialSuggestionStates(): HasMany
    {
        return $this->hasMany(WorldEditorialSuggestionState::class)->orderByDesc('updated_at')->orderByDesc('id');
    }

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

    public function webStories(): HasMany
    {
        return $this->hasMany(WorldWebStory::class)->orderByDesc('published_at')->orderByDesc('id');
    }

    public function publishedWebStory(): HasOne
    {
        return $this->hasOne(WorldWebStory::class)->visible()->latest('published_at')->latest('id');
    }

    public function scopePublished(Builder $query): Builder
    {
        return $query
            ->where('status', self::STATUS_PUBLISHED)
            ->where(function (Builder $builder): void {
                $builder->whereNull('published_at')
                    ->orWhere('published_at', '<=', now());
            });
    }

    public function scopePubliclyVisible(Builder $query): Builder
    {
        return $query
            ->whereIn('status', [self::STATUS_PUBLISHED, self::STATUS_ARCHIVED])
            ->where(function (Builder $builder): void {
                $builder->whereNull('published_at')
                    ->orWhere('published_at', '<=', now());
            });
    }

    public function scopeCurrent(Builder $query): Builder
    {
        return $query
            ->published()
            ->where(function (Builder $builder): void {
                $builder->whereNull('starts_at')
                    ->orWhere('starts_at', '<=', now());
            })
            ->where(function (Builder $builder): void {
                $builder->whereNull('ends_at')
                    ->orWhere('ends_at', '>=', now());
            });
    }

    public function scopeUpcoming(Builder $query): Builder
    {
        return $query
            ->published()
            ->whereNotNull('starts_at')
            ->where('starts_at', '>', now());
    }

    public function scopeCampaignActive(Builder $query): Builder
    {
        $now = now()->toDateTimeString();

        return $query
            ->published()
            ->where('is_active_campaign', true)
            ->where(function (Builder $builder) use ($now): void {
                $builder->whereRaw('COALESCE(promotion_starts_at, starts_at) IS NULL')
                    ->orWhereRaw('COALESCE(promotion_starts_at, starts_at) <= ?', [$now]);
            })
            ->where(function (Builder $builder) use ($now): void {
                $builder->whereRaw('COALESCE(promotion_ends_at, ends_at) IS NULL')
                    ->orWhereRaw('COALESCE(promotion_ends_at, ends_at) >= ?', [$now]);
            });
    }

    public function scopeCampaignUpcoming(Builder $query): Builder
    {
        return $query
            ->published()
            ->where('is_active_campaign', true)
            ->whereRaw('COALESCE(promotion_starts_at, starts_at) > ?', [now()->toDateTimeString()]);
    }

    public function scopeHomepageFeatured(Builder $query): Builder
    {
        return $query->where('is_homepage_featured', true);
    }

    public function scopeArchive(Builder $query): Builder
    {
        return $query
            ->publiclyVisible()
            ->where(function (Builder $builder): void {
                $builder->where('status', self::STATUS_ARCHIVED)
                    ->orWhere(function (Builder $expired): void {
                        $expired->whereNotNull('ends_at')
                            ->where('ends_at', '<', now());
                    });
            });
    }

    public function isPubliclyVisible(): bool
    {
        if (! in_array($this->status, [self::STATUS_PUBLISHED, self::STATUS_ARCHIVED], true)) {
            return false;
        }

        if ($this->published_at && $this->published_at->isFuture()) {
            return false;
        }

        return true;
    }

    public function isCurrent(): bool
    {
        if (! $this->isPubliclyVisible()) {
            return false;
        }

        if ($this->starts_at && $this->starts_at->isFuture()) {
            return false;
        }

        if ($this->ends_at && $this->ends_at->isPast()) {
            return false;
        }

        return true;
    }

    public function isAcceptingSubmissions(): bool
    {
        if (! $this->isPubliclyVisible() || ! $this->accepts_submissions || ! $this->allowsCreatorParticipation()) {
            return false;
        }

        if ($this->submission_starts_at && $this->submission_starts_at->isFuture()) {
            return false;
        }

        if ($this->submission_ends_at && $this->submission_ends_at->isPast()) {
            return false;
        }

        return true;
    }

    public function effectivePromotionStartsAt(): ?Carbon
    {
        return $this->promotion_starts_at ?? $this->starts_at;
    }

    public function effectivePromotionEndsAt(): ?Carbon
    {
        return $this->promotion_ends_at ?? $this->ends_at;
    }

    public function isActiveCampaign(): bool
    {
        if (! $this->isPubliclyVisible() || ! (bool) $this->is_active_campaign || (string) $this->status !== self::STATUS_PUBLISHED) {
            return false;
        }

        $startsAt = $this->effectivePromotionStartsAt();
        $endsAt = $this->effectivePromotionEndsAt();

        if ($startsAt && $startsAt->isFuture()) {
            return false;
        }

        if ($endsAt && $endsAt->isPast()) {
            return false;
        }

        return true;
    }

    public function isUpcomingCampaign(): bool
    {
        if (! $this->isPubliclyVisible() || ! (bool) $this->is_active_campaign || (string) $this->status !== self::STATUS_PUBLISHED) {
            return false;
        }

        $startsAt = $this->effectivePromotionStartsAt();

        return $startsAt ? $startsAt->isFuture() : false;
    }

    public function isEndingSoon(?int $days = null): bool
    {
        if (! $this->isActiveCampaign()) {
            return false;
        }

        $endsAt = $this->effectivePromotionEndsAt();
        if (! $endsAt) {
            return false;
        }

        $threshold = now()->addDays($days ?? (int) config('worlds.campaign_ending_soon_days', 5));

        return $endsAt->greaterThanOrEqualTo(now()) && $endsAt->lessThanOrEqualTo($threshold);
    }

    public function isEndedEdition(): bool
    {
        return (string) $this->status === self::STATUS_ARCHIVED
            || ($this->ends_at && $this->ends_at->isPast());
    }

    public function hasPublishedRecap(): bool
    {
        return (string) $this->recap_status === self::RECAP_STATUS_PUBLISHED
            && $this->recap_published_at !== null;
    }

    public function hasRecapDraftContent(): bool
    {
        return trim((string) ($this->recap_title ?? '')) !== ''
            || trim((string) ($this->recap_summary ?? '')) !== ''
            || trim((string) ($this->recap_intro ?? '')) !== ''
            || trim((string) ($this->recap_cover_path ?? '')) !== ''
            || (int) ($this->recap_article_id ?? 0) > 0
            || ! empty($this->recap_stats_snapshot_json);
    }

    public function recapCoverUrl(): ?string
    {
        $path = trim((string) ($this->recap_cover_path ?: $this->cover_path ?: ''));

        if ($path === '') {
            return null;
        }

        if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
            return $path;
        }

        return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
    }

    public function teaserImageUrl(): ?string
    {
        $path = trim((string) ($this->teaser_image_path ?: $this->cover_path ?: ''));

        if ($path === '') {
            return null;
        }

        if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
            return $path;
        }

        return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
    }

    public function teaserTitle(): string
    {
        $title = trim((string) ($this->teaser_title ?? ''));

        return $title !== '' ? $title : (string) $this->title;
    }

    public function teaserSummary(): ?string
    {
        $summary = trim((string) ($this->teaser_summary ?? ''));

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

        $fallback = trim((string) ($this->summary ?? ''));

        return $fallback !== '' ? $fallback : null;
    }

    public function allowsCreatorParticipation(): bool
    {
        return in_array((string) $this->participation_mode, [
            self::PARTICIPATION_MODE_MANUAL_APPROVAL,
            self::PARTICIPATION_MODE_AUTO_ADD,
        ], true);
    }

    public function submissionStartsAsLive(): bool
    {
        return (string) $this->participation_mode === self::PARTICIPATION_MODE_AUTO_ADD;
    }

    public function coverUrl(): ?string
    {
        $path = trim((string) $this->cover_path);

        if ($path === '') {
            return null;
        }

        if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
            return $path;
        }

        return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
    }

    public function ogImageUrl(): ?string
    {
        $path = trim((string) ($this->og_image_path ?: $this->cover_path ?: ''));

        if ($path === '') {
            return null;
        }

        if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
            return $path;
        }

        return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
    }

    public function publicUrl(): string
    {
        if (! $this->is_recurring || trim((string) $this->recurrence_key) === '') {
            return route('worlds.show', ['world' => $this->slug]);
        }

        if ($this->isCanonicalEdition()) {
            return $this->familyUrl();
        }

        if ($this->edition_year !== null) {
            return route('worlds.editions.show', ['world' => $this->recurrence_key, 'year' => $this->edition_year]);
        }

        return $this->familyUrl();
    }

    public function familySlug(): string
    {
        return trim((string) ($this->recurrence_key ?: $this->slug));
    }

    public function familyUrl(): string
    {
        return route('worlds.show', ['world' => $this->familySlug()]);
    }

    public function editionUrl(): ?string
    {
        if (! $this->is_recurring || trim((string) $this->recurrence_key) === '' || $this->edition_year === null) {
            return null;
        }

        return route('worlds.editions.show', ['world' => $this->recurrence_key, 'year' => $this->edition_year]);
    }

    public function isCanonicalEdition(): bool
    {
        if (! $this->is_recurring || trim((string) $this->recurrence_key) === '') {
            return true;
        }

        return static::canonicalEditionIdForRecurrence((string) $this->recurrence_key) === (int) $this->id;
    }

    public static function primeCanonicalEditionIds(iterable $recurrenceKeys): void
    {
        $keys = collect($recurrenceKeys)
            ->map(static fn ($key): string => trim((string) $key))
            ->filter()
            ->unique()
            ->reject(static fn (string $key): bool => array_key_exists($key, static::$canonicalRecurrenceEditionIds))
            ->values();

        if ($keys->isEmpty()) {
            return;
        }

        $editionsByRecurrence = static::query()
            ->publiclyVisible()
            ->whereIn('recurrence_key', $keys->all())
            ->get()
            ->groupBy('recurrence_key');

        foreach ($keys as $key) {
            $canonical = static::selectCanonicalEdition(new EloquentCollection($editionsByRecurrence->get($key, collect())->all()));

            static::$canonicalRecurrenceEditionIds[$key] = $canonical ? (int) $canonical->id : null;
        }
    }

    public function sectionOrder(): array
    {
        $defaults = array_values(array_filter(config('worlds.default_section_order', []), 'is_string'));
        $custom = array_values(array_filter($this->section_order_json ?? [], 'is_string'));

        return array_values(array_unique(array_merge($custom, $defaults)));
    }

    public function sectionVisibility(): array
    {
        $defaults = collect(array_keys((array) config('worlds.sections', [])))
            ->mapWithKeys(fn (string $key): array => [$key => true])
            ->all();

        $custom = collect((array) $this->section_visibility_json)
            ->mapWithKeys(fn ($value, $key): array => [(string) $key => (bool) $value])
            ->all();

        return array_merge($defaults, $custom);
    }

    private static function canonicalEditionIdForRecurrence(string $recurrenceKey): ?int
    {
        if (array_key_exists($recurrenceKey, static::$canonicalRecurrenceEditionIds)) {
            return static::$canonicalRecurrenceEditionIds[$recurrenceKey];
        }

        $canonical = static::selectCanonicalEdition(
            static::query()
                ->publiclyVisible()
                ->where('recurrence_key', $recurrenceKey)
                ->get()
        );

        return static::$canonicalRecurrenceEditionIds[$recurrenceKey] = $canonical ? (int) $canonical->id : null;
    }

    private static function selectCanonicalEdition(EloquentCollection $editions): ?self
    {
        return $editions
            ->sortBy([
                fn (self $world): int => (string) $world->status === self::STATUS_PUBLISHED ? 0 : 1,
                fn (self $world): int => $world->isCurrent() ? 0 : 1,
                fn (self $world): int => -1 * (int) ($world->edition_year ?? 0),
                fn (self $world): int => -1 * ($world->starts_at?->getTimestamp() ?? $world->published_at?->getTimestamp() ?? 0),
                fn (self $world): int => -1 * (int) $world->id,
            ])
            ->first();
    }
}