<?php

declare(strict_types=1);

namespace App\Services\Academy;

use App\Models\AcademyCourse;
use App\Models\AcademyCourseLesson;
use App\Models\AcademyAiComparisonResult;
use App\Models\AcademyChallenge;
use App\Models\AcademyLesson;
use App\Models\AcademyLessonBlock;
use App\Models\AcademyPromptPack;
use App\Models\AcademyPromptTemplate;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Cashier\Subscription;

final class AcademyAccessService
{
    /**
     * @var array<string, bool>
     */
    private array $assetExistsCache = [];

    /**
     * @var array<int, string|null>
     */
    private array $paidTierCache = [];

    /**
     * @var array<int, Subscription|null>
     */
    private array $subscriptionCache = [];

    /**
     * @var array<string, string>|null
     */
    private ?array $priceTierMap = null;

    public function canAccess(?User $user, string $requiredLevel): bool
    {
        return $this->canAccessContent($user, $requiredLevel);
    }

    public function canAccessContent(?User $user, string $accessLevel): bool
    {
        $accessLevel = $this->normalizeAccessLevel($accessLevel);

        if ($accessLevel === 'free') {
            return true;
        }

        if ($user === null) {
            return false;
        }

        return $this->rankForLevel($this->currentTier($user)) >= $this->rankForLevel($accessLevel);
    }

    public function currentTier(?User $user): string
    {
        if (! $user instanceof User) {
            return 'free';
        }

        if ($this->isAcademyAdmin($user)) {
            return 'admin';
        }

        return $this->paidTier($user) ?? 'free';
    }

    public function paidTier(?User $user): ?string
    {
        if (! $user instanceof User) {
            return null;
        }

        $cacheKey = (int) $user->getKey();

        if (array_key_exists($cacheKey, $this->paidTierCache)) {
            return $this->paidTierCache[$cacheKey];
        }

        return $this->paidTierCache[$cacheKey] = $this->resolveSubscriptionTier($user) ?? $this->resolveLegacyPaidTier($user);
    }

    public function hasActiveAcademySubscription(User $user): bool
    {
        return $this->activeAcademySubscription($user) instanceof Subscription;
    }

    /**
     * @return array<string, mixed>
     */
    public function accessSummary(?User $user): array
    {
        if (! $user instanceof User) {
            return [
                'signedIn' => false,
                'tier' => 'free',
                'tierLabel' => 'Guest',
                'hasPaidAccess' => false,
                'status' => 'guest',
                'statusLabel' => 'Preview access only',
                'expiresAt' => null,
                'dateLabel' => null,
                'renewsAutomatically' => false,
                'source' => 'none',
            ];
        }

        if ($this->isAcademyAdmin($user)) {
            return [
                'signedIn' => true,
                'tier' => 'admin',
                'tierLabel' => 'Admin',
                'hasPaidAccess' => true,
                'status' => 'staff_access',
                'statusLabel' => 'Full staff access',
                'expiresAt' => null,
                'dateLabel' => null,
                'renewsAutomatically' => false,
                'source' => 'admin',
            ];
        }

        $tier = $this->currentTier($user);
        $subscription = $this->activeAcademySubscription($user);

        if ($subscription instanceof Subscription) {
            $trialEndsAt = $subscription->trial_ends_at?->toISOString();
            $endsAt = $subscription->ends_at?->toISOString();

            if ($subscription->onGracePeriod()) {
                return [
                    'signedIn' => true,
                    'tier' => $tier,
                    'tierLabel' => $this->tierLabel($tier),
                    'hasPaidAccess' => $tier !== 'free',
                    'status' => 'grace_period',
                    'statusLabel' => 'Cancels soon',
                    'expiresAt' => $endsAt,
                    'dateLabel' => 'Access ends',
                    'renewsAutomatically' => false,
                    'source' => 'subscription',
                ];
            }

            if ($subscription->onTrial()) {
                return [
                    'signedIn' => true,
                    'tier' => $tier,
                    'tierLabel' => $this->tierLabel($tier),
                    'hasPaidAccess' => $tier !== 'free',
                    'status' => 'trialing',
                    'statusLabel' => 'Trial active',
                    'expiresAt' => $trialEndsAt,
                    'dateLabel' => 'Trial ends',
                    'renewsAutomatically' => ! $subscription->cancelled(),
                    'source' => 'subscription',
                ];
            }

            return [
                'signedIn' => true,
                'tier' => $tier,
                'tierLabel' => $this->tierLabel($tier),
                'hasPaidAccess' => $tier !== 'free',
                'status' => 'active',
                'statusLabel' => 'Renews automatically',
                'expiresAt' => null,
                'dateLabel' => null,
                'renewsAutomatically' => true,
                'source' => 'subscription',
            ];
        }

        if ($tier !== 'free') {
            return [
                'signedIn' => true,
                'tier' => $tier,
                'tierLabel' => $this->tierLabel($tier),
                'hasPaidAccess' => true,
                'status' => 'active',
                'statusLabel' => 'Full access active',
                'expiresAt' => null,
                'dateLabel' => null,
                'renewsAutomatically' => false,
                'source' => 'legacy_role',
            ];
        }

        return [
            'signedIn' => true,
            'tier' => 'free',
            'tierLabel' => 'Free',
            'hasPaidAccess' => false,
            'status' => 'free',
            'statusLabel' => 'Free access',
            'expiresAt' => null,
            'dateLabel' => null,
            'renewsAutomatically' => false,
            'source' => 'none',
        ];
    }

    public function canAccessLesson(?User $user, AcademyLesson $lesson): bool
    {
        return $this->canAccessContent($user, (string) $lesson->access_level);
    }

    public function canAccessPrompt(?User $user, AcademyPromptTemplate $prompt): bool
    {
        return $this->canAccessContent($user, (string) $prompt->access_level);
    }

    public function canAccessPack(?User $user, AcademyPromptPack $pack): bool
    {
        return $this->canAccessContent($user, (string) $pack->access_level);
    }

    public function canAccessChallenge(?User $user, AcademyChallenge $challenge): bool
    {
        return $this->canAccessContent($user, (string) $challenge->access_level);
    }

    public function canAccessCourseLesson(?User $user, AcademyCourseLesson $courseLesson): bool
    {
        $accessLevel = trim((string) ($courseLesson->access_override ?: $courseLesson->lesson?->access_level ?: 'free'));

        return $this->canAccessContent($user, $accessLevel);
    }

    public function lessonPayload(AcademyLesson $lesson, ?User $viewer, bool $includeFull = false, ?bool $authorizedOverride = null): array
    {
        $authorized = $authorizedOverride ?? $this->canAccessLesson($viewer, $lesson);
        $fullContent = $this->injectHeadingIds((string) ($lesson->content ?? ''));

        return [
            'id' => (int) $lesson->id,
            'title' => (string) $lesson->title,
            'slug' => (string) $lesson->slug,
            'lesson_number' => $lesson->lesson_number,
            'formatted_lesson_number' => $lesson->formatted_lesson_number,
            'course_order' => $lesson->course_order,
            'series_name' => (string) ($lesson->series_name ?? ''),
            'lesson_label' => $lesson->lesson_label,
            'excerpt' => (string) ($lesson->excerpt ?? ''),
            'content' => ($authorized && $includeFull) ? $fullContent : null,
            'content_preview' => $authorized ? null : $this->previewText($fullContent, 360),
            'difficulty' => (string) $lesson->difficulty,
            'access_level' => (string) $lesson->access_level,
            'lesson_type' => (string) $lesson->lesson_type,
            'cover_image' => $lesson->cover_image,
            'cover_image_url' => $this->resolveLessonCoverImageUrl((string) ($lesson->cover_image ?? '')),
            'article_cover_image' => $lesson->article_cover_image,
            'article_cover_image_url' => $this->resolveLessonCoverImageUrl((string) ($lesson->article_cover_image ?? '')),
            'tags' => array_values((array) ($lesson->tags ?? [])),
            'video_url' => $authorized ? $lesson->video_url : null,
            'reading_minutes' => (int) $lesson->reading_minutes,
            'featured' => (bool) $lesson->featured,
            'active' => (bool) $lesson->active,
            'published_at' => $lesson->published_at?->toISOString(),
            'category' => $lesson->category ? [
                'id' => (int) $lesson->category->id,
                'name' => (string) $lesson->category->name,
                'slug' => (string) $lesson->category->slug,
            ] : null,
            'blocks' => ($authorized && $includeFull)
                ? $lesson->activeBlocks->map(fn (AcademyLessonBlock $block): ?array => $this->lessonBlockPayload($block))->filter()->values()->all()
                : [],
            'locked' => ! $authorized,
            'can_access' => $authorized,
        ];
    }

    public function coursePayload(AcademyCourse $course, ?User $viewer, array $options = []): array
    {
        $progress = is_array($options['progress'] ?? null) ? $options['progress'] : null;
        $lessonCount = (int) ($course->lessons_count_cache ?: $course->courseLessons()->count());

        return [
            'id' => (int) $course->id,
            'title' => (string) $course->title,
            'slug' => (string) $course->slug,
            'subtitle' => (string) ($course->subtitle ?? ''),
            'excerpt' => (string) ($course->excerpt ?? ''),
            'description' => (string) ($course->description ?? ''),
            'cover_image' => (string) ($course->cover_image ?? ''),
            'cover_image_url' => $this->resolveLessonCoverImageUrl((string) ($course->cover_image ?? '')),
            'teaser_image' => (string) ($course->teaser_image ?? ''),
            'teaser_image_url' => $this->resolveLessonCoverImageUrl((string) ($course->teaser_image ?? '')),
            'access_level' => (string) $course->access_level,
            'difficulty' => (string) $course->difficulty,
            'status' => (string) $course->status,
            'is_featured' => (bool) $course->is_featured,
            'order_num' => (int) ($course->order_num ?? 0),
            'estimated_minutes' => (int) ($course->estimated_minutes ?? 0),
            'lessons_count' => $lessonCount,
            'published_at' => $course->published_at?->toISOString(),
            'public_url' => route('academy.courses.show', ['course' => $course->slug]),
            'progress' => $progress ? [
                'completedRequired' => (int) ($progress['completed_required'] ?? 0),
                'totalRequired' => (int) ($progress['total_required'] ?? 0),
                'percent' => (int) ($progress['progress_percent'] ?? 0),
                'completed' => (bool) ($progress['completed'] ?? false),
            ] : null,
            'continue_url' => $viewer ? $course->getContinueUrl($viewer) : route('academy.courses.show', ['course' => $course->slug]),
        ];
    }

    public function courseLessonPayload(AcademyCourseLesson $courseLesson, ?User $viewer, bool $includeFull = false, array $options = []): array
    {
        $lesson = $courseLesson->lesson;

        if (! $lesson instanceof AcademyLesson) {
            return [];
        }

        $authorized = $this->canAccessCourseLesson($viewer, $courseLesson);
        $payload = $this->lessonPayload($lesson, $viewer, $includeFull, $authorized);

        $payload['course_lesson_id'] = (int) $courseLesson->id;
        $payload['course_id'] = (int) $courseLesson->course_id;
        $payload['section_id'] = $courseLesson->section_id ? (int) $courseLesson->section_id : null;
        $payload['order_num'] = (int) ($courseLesson->order_num ?? 0);
        $payload['is_required'] = (bool) $courseLesson->is_required;
        $payload['access_override'] = $courseLesson->access_override;
        $payload['course_step_number'] = (int) ($options['course_step_number'] ?? 0);
        $payload['course_step_label'] = (string) ($options['course_step_label'] ?? '');
        $payload['completed'] = in_array((int) $courseLesson->lesson_id, array_map('intval', (array) ($options['completed_lesson_ids'] ?? [])), true);
        $payload['course_url'] = route('academy.courses.lessons.show', ['course' => $courseLesson->course->slug, 'lesson' => $lesson->slug]);

        return $payload;
    }

    public function promptPayload(AcademyPromptTemplate $prompt, ?User $viewer, bool $includeFull = false): array
    {
        $authorized = $this->canAccessPrompt($viewer, $prompt);
        $publicExamples = $this->promptPublicExamplesPayload($prompt, (array) ($prompt->tool_notes ?? []));
        $previewImage = $this->promptPreviewImagePayload((string) ($prompt->preview_image ?? ''));
        $documentation = $this->promptDocumentationPayload($prompt->documentation);
        $placeholders = $this->promptPlaceholdersPayload((array) ($prompt->placeholders ?? []));
        $hasPlaceholderInputs = $this->promptHasPlaceholderInputs((string) $prompt->prompt, $placeholders);
        $hasHelperPrompts = $this->promptHelperPromptsPayload((array) ($prompt->helper_prompts ?? [])) !== [];
        $hasPromptVariants = $this->promptVariantsPayload((array) ($prompt->prompt_variants ?? [])) !== [];
        $helperPrompts = $authorized && $includeFull
            ? $this->promptHelperPromptsPayload((array) ($prompt->helper_prompts ?? []))
            : [];
        $promptVariants = $authorized && $includeFull
            ? $this->promptVariantsPayload((array) ($prompt->prompt_variants ?? []))
            : [];

        return [
            'id' => (int) $prompt->id,
            'title' => (string) $prompt->title,
            'slug' => (string) $prompt->slug,
            'excerpt' => (string) ($prompt->excerpt ?? ''),
            'prompt' => ($authorized && $includeFull) ? (string) $prompt->prompt : null,
            'negative_prompt' => ($authorized && $includeFull) ? (string) ($prompt->negative_prompt ?? '') : null,
            'usage_notes' => ($authorized && $includeFull) ? (string) ($prompt->usage_notes ?? '') : null,
            'workflow_notes' => ($authorized && $includeFull) ? (string) ($prompt->workflow_notes ?? '') : null,
            'prompt_preview' => $authorized ? null : $this->previewText((string) $prompt->prompt, 220),
            'documentation' => $documentation,
            'placeholders' => $placeholders,
            'has_placeholder_inputs' => $hasPlaceholderInputs,
            'has_helper_prompts' => $hasHelperPrompts,
            'has_prompt_variants' => $hasPromptVariants,
            'helper_prompts' => $helperPrompts,
            'prompt_variants' => $promptVariants,
            'difficulty' => (string) $prompt->difficulty,
            'access_level' => (string) $prompt->access_level,
            'access_requirement' => $this->promptAccessRequirement((string) $prompt->access_level),
            'unlock_heading' => $this->promptUnlockHeading((string) $prompt->access_level),
            'unlock_description' => $this->promptUnlockDescription((string) $prompt->access_level),
            'aspect_ratio' => $prompt->aspect_ratio,
            'tags' => array_values((array) ($prompt->tags ?? [])),
            'public_examples' => $publicExamples,
            'tool_notes' => $authorized ? $this->promptToolNotesPayload((array) ($prompt->tool_notes ?? [])) : [],
            'preview_image' => $previewImage['url'],
            'preview_image_thumb' => $previewImage['thumb_url'],
            'preview_image_srcset' => $previewImage['srcset'],
            'featured' => (bool) $prompt->featured,
            'prompt_of_week' => (bool) $prompt->prompt_of_week,
            'published_at' => $prompt->published_at?->toISOString(),
            'category' => $prompt->category ? [
                'id' => (int) $prompt->category->id,
                'name' => (string) $prompt->category->name,
                'slug' => (string) $prompt->category->slug,
            ] : null,
            'locked' => ! $authorized,
            'can_access' => $authorized,
        ];
    }

    /**
     * @param  mixed  $documentation
     * @return array<string, mixed>
     */
    private function promptDocumentationPayload(mixed $documentation): array
    {
        $normalized = is_array($documentation) ? $documentation : [];
        $listFields = ['best_for', 'how_to_use', 'required_inputs', 'workflow', 'tips', 'common_mistakes', 'data_accuracy_notes'];
        $payload = [
            'summary' => $this->nullableTrimmedString($normalized['summary'] ?? null),
            'display_notes' => $this->nullableTrimmedString($normalized['display_notes'] ?? null),
        ];

        foreach ($listFields as $field) {
            $payload[$field] = $this->normalizeStringList($normalized[$field] ?? []);
        }

        return $payload;
    }

    /**
     * @param  array<int, mixed>  $placeholders
     * @return array<int, array<string, mixed>>
     */
    private function promptPlaceholdersPayload(array $placeholders): array
    {
        return collect($placeholders)
            ->filter(static fn ($placeholder): bool => is_array($placeholder))
            ->map(function (array $placeholder): array {
                return [
                    'key' => trim((string) ($placeholder['key'] ?? '')),
                    'label' => $this->nullableTrimmedString($placeholder['label'] ?? null),
                    'description' => $this->nullableTrimmedString($placeholder['description'] ?? null),
                    'required' => filter_var($placeholder['required'] ?? false, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false,
                    'example' => $placeholder['example'] ?? null,
                    'default' => $placeholder['default'] ?? null,
                    'type' => $this->nullableTrimmedString($placeholder['type'] ?? null),
                ];
            })
            ->filter(function (array $placeholder): bool {
                return collect([
                    $placeholder['key'],
                    $placeholder['label'],
                    $placeholder['description'],
                    $placeholder['example'],
                    $placeholder['default'],
                    $placeholder['type'],
                ])->contains(fn ($item): bool => $item !== null && $item !== '' && $item !== []);
            })
            ->values()
            ->all();
    }

    /**
     * @param  array<int, array<string, mixed>>  $placeholders
     */
    private function promptHasPlaceholderInputs(string $prompt, array $placeholders): bool
    {
        if ($prompt === '' || $placeholders === []) {
            return false;
        }

        foreach ($placeholders as $placeholder) {
            $key = trim((string) ($placeholder['key'] ?? ''));

            if ($key === '') {
                continue;
            }

            if (mb_stripos($prompt, '['.$key.']') !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param  array<int, mixed>  $helperPrompts
     * @return array<int, array<string, mixed>>
     */
    private function promptHelperPromptsPayload(array $helperPrompts): array
    {
        return collect($helperPrompts)
            ->filter(static fn ($helperPrompt): bool => is_array($helperPrompt))
            ->map(function (array $helperPrompt): array {
                return [
                    'title' => trim((string) ($helperPrompt['title'] ?? '')),
                    'type' => trim((string) ($helperPrompt['type'] ?? 'other')) ?: 'other',
                    'description' => $this->nullableTrimmedString($helperPrompt['description'] ?? null),
                    'prompt' => trim((string) ($helperPrompt['prompt'] ?? '')),
                    'expected_output' => trim((string) ($helperPrompt['expected_output'] ?? 'text')) ?: 'text',
                    'active' => filter_var($helperPrompt['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true,
                ];
            })
            ->filter(function (array $helperPrompt): bool {
                return $helperPrompt['active'] !== false
                    && collect([
                        $helperPrompt['title'],
                        $helperPrompt['description'],
                        $helperPrompt['prompt'],
                    ])->contains(fn ($item): bool => $item !== null && $item !== '');
            })
            ->values()
            ->all();
    }

    /**
     * @param  array<int, mixed>  $variants
     * @return array<int, array<string, mixed>>
     */
    private function promptVariantsPayload(array $variants): array
    {
        return collect($variants)
            ->filter(static fn ($variant): bool => is_array($variant))
            ->map(function (array $variant): array {
                return [
                    'title' => trim((string) ($variant['title'] ?? '')),
                    'slug' => $this->nullableTrimmedString($variant['slug'] ?? null),
                    'description' => $this->nullableTrimmedString($variant['description'] ?? null),
                    'prompt' => trim((string) ($variant['prompt'] ?? '')),
                    'negative_prompt' => $this->nullableTrimmedString($variant['negative_prompt'] ?? null),
                    'recommended' => filter_var($variant['recommended'] ?? false, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false,
                    'recommended_for' => $this->normalizeStringList($variant['recommended_for'] ?? []),
                    'risk_notes' => $this->normalizeStringList($variant['risk_notes'] ?? []),
                    'active' => filter_var($variant['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true,
                ];
            })
            ->filter(function (array $variant): bool {
                return $variant['active'] !== false
                    && collect([
                        $variant['title'],
                        $variant['description'],
                        $variant['prompt'],
                        $variant['negative_prompt'],
                    ])->contains(fn ($item): bool => $item !== null && $item !== '');
            })
            ->values()
            ->all();
    }

    /**
     * @param  array<int, mixed>  $notes
     * @return array<int, array<string, mixed>>
     */
    private function promptPublicExamplesPayload(AcademyPromptTemplate $prompt, array $notes): array
    {
        $promptTitle = trim((string) $prompt->title);

        return collect($notes)
            ->values()
            ->filter(static fn ($note): bool => is_array($note))
            ->filter(function (array $note): bool {
                return (filter_var($note['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true) !== false;
            })
            ->map(function (array $note, int $index) use ($promptTitle): ?array {
                $imagePayload = $this->responsiveLessonImagePayload(
                    (string) ($note['image_path'] ?? ''),
                    (string) ($note['thumb_path'] ?? ''),
                );
                $imagePath = $imagePayload['image_path'];
                $thumbPath = $imagePayload['thumb_path'];
                $imageUrl = $imagePayload['image_url'];
                $thumbUrl = $imagePayload['thumb_url'];

                if ($imageUrl === null && $thumbUrl === null) {
                    return null;
                }

                $displayType = trim((string) ($note['display_type'] ?? ''));
                $provider = trim((string) ($note['provider'] ?? ''));
                $modelName = trim((string) ($note['model_name'] ?? ''));
                $typeLabel = $displayType !== ''
                    ? (string) Str::of($displayType)->replace(['_', '-'], ' ')->headline()
                    : 'Prompt variation';
                $title = $displayType !== ''
                    ? $typeLabel
                    : ($modelName !== '' ? $modelName : ($provider !== '' ? $provider : sprintf('Prompt Example %02d', $index + 1)));
                $caption = $displayType !== ''
                    ? sprintf('%s preview for %s.', $typeLabel, $promptTitle !== '' ? $promptTitle : 'this prompt')
                    : sprintf('Example result preview for %s.', $promptTitle !== '' ? $promptTitle : 'this prompt');

                return [
                    'type_label' => $typeLabel,
                    'title' => $title,
                    'caption' => $caption,
                    'alt' => sprintf('%s preview image for %s', $title, $promptTitle !== '' ? $promptTitle : 'Skinbase Academy prompt'),
                    'provider' => $provider,
                    'model_name' => $modelName,
                    'image_path' => $imagePath,
                    'image_url' => $imageUrl,
                    'thumb_path' => $thumbPath,
                    'thumb_url' => $thumbUrl,
                    'image_srcset' => $imagePayload['srcset'],
                    'score' => filled($note['score'] ?? null) ? (int) $note['score'] : null,
                ];
            })
            ->filter()
            ->values()
            ->all();
    }

    private function promptAccessRequirement(string $accessLevel): ?string
    {
        return match (trim(strtolower($accessLevel))) {
            'pro' => 'Requires Pro access.',
            'creator' => 'Requires Creator or Pro access.',
            default => null,
        };
    }

    private function promptUnlockHeading(string $accessLevel): ?string
    {
        return match (trim(strtolower($accessLevel))) {
            'pro' => 'Unlock the full Pro prompt.',
            'creator' => 'Unlock the full Creator prompt.',
            default => null,
        };
    }

    private function promptUnlockDescription(string $accessLevel): ?string
    {
        return match (trim(strtolower($accessLevel))) {
            'pro' => 'Get the complete reusable prompt, negative prompt, workflow notes, model settings, and variation strategy.',
            'creator' => 'Get the complete reusable prompt, negative prompt, workflow notes, and creative workflow.',
            default => null,
        };
    }

    /**
     * @param  array<int, mixed>  $notes
     * @return array<int, array<string, mixed>>
     */
    private function promptToolNotesPayload(array $notes): array
    {
        return collect($notes)
            ->filter(static fn ($note): bool => is_array($note))
            ->map(function (array $note): array {
                $imagePayload = $this->responsiveLessonImagePayload(
                    (string) ($note['image_path'] ?? ''),
                    (string) ($note['thumb_path'] ?? ''),
                );

                return [
                    'display_type' => trim((string) ($note['display_type'] ?? '')),
                    'provider' => trim((string) ($note['provider'] ?? '')),
                    'model_name' => trim((string) ($note['model_name'] ?? '')),
                    'notes' => trim((string) ($note['notes'] ?? '')),
                    'strengths' => trim((string) ($note['strengths'] ?? '')),
                    'weaknesses' => trim((string) ($note['weaknesses'] ?? '')),
                    'best_for' => trim((string) ($note['best_for'] ?? '')),
                    'image_path' => $imagePayload['image_path'],
                    'image_url' => $imagePayload['image_url'],
                    'thumb_path' => $imagePayload['thumb_path'],
                    'thumb_url' => $imagePayload['thumb_url'],
                    'image_srcset' => $imagePayload['srcset'],
                    'settings' => trim((string) ($note['settings'] ?? '')),
                    'score' => filled($note['score'] ?? null) ? (int) $note['score'] : null,
                    'active' => filter_var($note['active'] ?? true, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true,
                ];
            })
            ->filter(function (array $note): bool {
                return collect([
                    $note['display_type'],
                    $note['provider'],
                    $note['model_name'],
                    $note['notes'],
                    $note['strengths'],
                    $note['weaknesses'],
                    $note['best_for'],
                    $note['image_path'],
                    $note['thumb_path'],
                    $note['settings'],
                ])->contains(fn (string $value): bool => $value !== '') || $note['score'] !== null;
            })
            ->values()
            ->all();
    }

    public function packPayload(AcademyPromptPack $pack, ?User $viewer, bool $includePrompts = false): array
    {
        $authorized = $this->canAccessPack($viewer, $pack);

        return [
            'id' => (int) $pack->id,
            'title' => (string) $pack->title,
            'slug' => (string) $pack->slug,
            'excerpt' => (string) ($pack->excerpt ?? ''),
            'description' => (string) ($pack->description ?? ''),
            'access_level' => (string) $pack->access_level,
            'cover_image' => $pack->cover_image,
            'tags' => array_values((array) ($pack->tags ?? [])),
            'featured' => (bool) $pack->featured,
            'published_at' => $pack->published_at?->toISOString(),
            'locked' => ! $authorized,
            'can_access' => $authorized,
            'prompts' => $includePrompts
                ? $pack->prompts->map(fn (AcademyPromptTemplate $prompt): array => $this->promptPayload($prompt, $viewer, $authorized))->values()->all()
                : [],
        ];
    }

    public function challengePayload(AcademyChallenge $challenge, ?User $viewer, bool $includeSubmissions = false): array
    {
        $authorized = $this->canAccessChallenge($viewer, $challenge);

        return [
            'id' => (int) $challenge->id,
            'title' => (string) $challenge->title,
            'slug' => (string) $challenge->slug,
            'excerpt' => (string) ($challenge->excerpt ?? ''),
            'description' => (string) ($challenge->description ?? ''),
            'brief' => (string) ($challenge->brief ?? ''),
            'rules' => (string) ($challenge->rules ?? ''),
            'access_level' => (string) $challenge->access_level,
            'status' => (string) $challenge->status,
            'starts_at' => $challenge->starts_at?->toISOString(),
            'ends_at' => $challenge->ends_at?->toISOString(),
            'voting_starts_at' => $challenge->voting_starts_at?->toISOString(),
            'voting_ends_at' => $challenge->voting_ends_at?->toISOString(),
            'cover_image' => $challenge->cover_image,
            'prize_text' => $challenge->prize_text,
            'required_tags' => array_values((array) ($challenge->required_tags ?? [])),
            'allowed_categories' => array_values((array) ($challenge->allowed_categories ?? [])),
            'featured' => (bool) $challenge->featured,
            'locked' => ! $authorized,
            'can_access' => $authorized,
            'submission_count' => $includeSubmissions ? (int) $challenge->submissions()->approved()->count() : null,
        ];
    }

    private function rankForLevel(string $accessLevel): int
    {
        return match ($this->normalizeAccessLevel($accessLevel)) {
            'admin' => 99,
            'pro' => 30,
            'creator' => 20,
            default => 10,
        };
    }

    private function normalizeAccessLevel(string $accessLevel): string
    {
        return match (Str::lower(trim($accessLevel))) {
            'admin' => 'admin',
            'pro' => 'pro',
            'creator', 'premium' => 'creator',
            'mixed' => 'free',
            default => 'free',
        };
    }

    private function tierLabel(string $tier): string
    {
        return match ($this->normalizeAccessLevel($tier)) {
            'admin' => 'Admin',
            'pro' => 'Pro',
            'creator' => 'Creator',
            default => 'Free',
        };
    }

    private function isAcademyAdmin(User $user): bool
    {
        return $user->hasStaffAccess() || $user->isModerator();
    }

    private function resolveSubscriptionTier(User $user): ?string
    {
        $subscription = $this->activeAcademySubscription($user);

        if (! $subscription instanceof Subscription) {
            return null;
        }

        $matchedTier = null;

        foreach ($subscription->items as $item) {
            $priceId = trim((string) $item->stripe_price);

            if ($priceId === '') {
                continue;
            }

            $tier = $this->priceTierMap()[$priceId] ?? null;

            if ($tier === null) {
                continue;
            }

            if ($matchedTier === null || $this->rankForLevel($tier) > $this->rankForLevel($matchedTier)) {
                $matchedTier = $tier;
            }
        }

        return $matchedTier;
    }

    private function resolveLegacyPaidTier(User $user): ?string
    {
        return match (Str::lower(trim((string) ($user->role ?? '')))) {
            'academy_pro' => 'pro',
            'academy_creator' => 'creator',
            default => null,
        };
    }

    private function activeAcademySubscription(User $user): ?Subscription
    {
        $cacheKey = (int) $user->getKey();

        if (array_key_exists($cacheKey, $this->subscriptionCache)) {
            return $this->subscriptionCache[$cacheKey];
        }

        $subscription = $user->subscription($this->subscriptionName());

        if (! $subscription instanceof Subscription) {
            return $this->subscriptionCache[$cacheKey] = null;
        }

        if (! $subscription->active() && ! $subscription->onGracePeriod()) {
            return $this->subscriptionCache[$cacheKey] = null;
        }

        return $this->subscriptionCache[$cacheKey] = $subscription->loadMissing('items');
    }

    /**
     * @return array<string, string>
     */
    private function priceTierMap(): array
    {
        if (is_array($this->priceTierMap)) {
            return $this->priceTierMap;
        }

        $map = [];

        foreach ((array) config('academy_billing.plans', []) as $plan) {
            if (! is_array($plan)) {
                continue;
            }

            $priceId = trim((string) ($plan['stripe_price_id'] ?? ''));
            $tier = $this->normalizeAccessLevel((string) ($plan['tier'] ?? 'free'));

            if ($priceId === '' || $tier === 'free') {
                continue;
            }

            $map[$priceId] = $tier;
        }

        return $this->priceTierMap = $map;
    }

    private function subscriptionName(): string
    {
        return (string) config('academy_billing.subscription_name', 'academy');
    }

    private function previewText(string $value, int $limit): string
    {
        $plain = trim(strip_tags($value));

        if ($plain === '') {
            return '';
        }

        $length = mb_strlen($plain);
        $previewLength = min($limit, max(12, (int) ceil($length * 0.55)));

        if ($previewLength >= $length) {
            $previewLength = max(1, $length - 1);
        }

        return rtrim(mb_substr($plain, 0, $previewLength)).'...';
    }

    private function nullableTrimmedString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        $normalized = trim((string) $value);

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

    /**
     * @return array<int, string>
     */
    private function normalizeStringList(mixed $value): array
    {
        if (! is_array($value)) {
            $value = $value === null ? [] : [$value];
        }

        return collect($value)
            ->map(fn ($item): string => trim((string) $item))
            ->filter(static fn (string $item): bool => $item !== '')
            ->values()
            ->all();
    }

    private function resolvePreviewImageUrl(string $previewImage): ?string
    {
        $previewImage = trim($previewImage);

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

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

        return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($previewImage);
    }

    /**
     * @return array{url:?string,thumb_url:?string,srcset:?string}
     */
    private function promptPreviewImagePayload(string $previewImage): array
    {
        $url = $this->resolvePreviewImageUrl($previewImage);
        $thumbPath = $this->existingResponsiveVariantPath($previewImage, 'thumb');
        $mediumPath = $this->existingResponsiveVariantPath($previewImage, 'md');

        return [
            'url' => $url,
            'thumb_url' => $thumbPath !== null ? $this->resolvePreviewImageUrl($thumbPath) : $url,
            'srcset' => $this->buildResponsiveSrcset([
                ['url' => $thumbPath !== null ? $this->resolvePreviewImageUrl($thumbPath) : null, 'width' => 480],
                ['url' => $mediumPath !== null ? $this->resolvePreviewImageUrl($mediumPath) : null, 'width' => 960],
            ]),
        ];
    }

    private function resolveLessonCoverImageUrl(string $coverImage): ?string
    {
        $coverImage = trim($coverImage);

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

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

        return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($coverImage);
    }

    private function resolveLessonMediaUrl(string $path): ?string
    {
        $path = trim($path);

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

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

        return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($path);
    }

    /**
     * @return array{image_path:string,image_url:?string,thumb_path:string,thumb_url:?string,srcset:?string}
     */
    private function responsiveLessonImagePayload(string $imagePath, string $thumbPath = ''): array
    {
        $resolvedImagePath = trim($imagePath);
        $resolvedThumbPath = trim($thumbPath);
        $imageUrl = $this->resolveLessonMediaUrl($resolvedImagePath);
        $thumbUrl = $resolvedThumbPath !== '' ? $this->resolveLessonMediaUrl($resolvedThumbPath) : $imageUrl;
        $mediumPath = $resolvedThumbPath !== '' ? $this->existingResponsiveVariantPath($resolvedImagePath, 'md') : null;

        return [
            'image_path' => $resolvedImagePath,
            'image_url' => $imageUrl,
            'thumb_path' => $resolvedThumbPath,
            'thumb_url' => $thumbUrl,
            'srcset' => $this->buildResponsiveSrcset([
                ['url' => $thumbUrl, 'width' => $resolvedThumbPath !== '' ? 480 : null],
                ['url' => $mediumPath !== null ? $this->resolveLessonMediaUrl($mediumPath) : null, 'width' => $mediumPath !== null ? 960 : null],
            ]),
        ];
    }

    private function responsiveVariantPath(string $path, string $variant): ?string
    {
        $path = trim($path);

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

        $directory = pathinfo($path, PATHINFO_DIRNAME);
        $filename = pathinfo($path, PATHINFO_FILENAME);
        $baseFilename = preg_replace('/-(thumb|md)$/', '', $filename) ?? $filename;

        return sprintf('%s/%s-%s.webp', $directory, $baseFilename, $variant);
    }

    private function existingResponsiveVariantPath(string $path, string $variant): ?string
    {
        $variantPath = $this->responsiveVariantPath($path, $variant);

        if ($variantPath === null || ! $this->storagePathExists($variantPath)) {
            return null;
        }

        return $variantPath;
    }

    private function storagePathExists(string $path): bool
    {
        $normalizedPath = trim($path);

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

        $cacheKey = (string) config('uploads.object_storage.disk', 's3') . ':' . $normalizedPath;

        if (array_key_exists($cacheKey, $this->assetExistsCache)) {
            return $this->assetExistsCache[$cacheKey];
        }

        try {
            $exists = Storage::disk((string) config('uploads.object_storage.disk', 's3'))->exists($normalizedPath);
        } catch (\Throwable) {
            $exists = false;
        }

        $this->assetExistsCache[$cacheKey] = $exists;

        return $exists;
    }

    /**
     * @param  array<int, array{url:?string,width:int|null}>  $variants
     */
    private function buildResponsiveSrcset(array $variants): ?string
    {
        $entries = collect($variants)
            ->filter(static fn (array $variant): bool => filled($variant['url'] ?? null) && (int) ($variant['width'] ?? 0) > 0)
            ->unique(fn (array $variant): string => (string) $variant['url'])
            ->map(fn (array $variant): string => sprintf('%s %dw', (string) $variant['url'], (int) $variant['width']))
            ->values()
            ->all();

        return $entries !== [] ? implode(', ', $entries) : null;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function lessonBlockPayload(AcademyLessonBlock $block): ?array
    {
        if ($block->type !== 'ai_comparison') {
            return null;
        }

        $payload = is_array($block->payload) ? $block->payload : [];
        $criteria = collect($payload['criteria'] ?? [])
            ->map(static fn ($criterion): string => trim((string) $criterion))
            ->filter(static fn (string $criterion): bool => $criterion !== '')
            ->values()
            ->all();
        $results = $block->activeComparisonResults
            ->map(function (AcademyAiComparisonResult $result): array {
                $imagePayload = $this->responsiveLessonImagePayload(
                    (string) $result->image_path,
                    (string) ($result->thumb_path ?? ''),
                );

                return [
                    'id' => (int) $result->id,
                    'provider' => (string) ($result->provider ?? ''),
                    'model_name' => (string) ($result->model_name ?? ''),
                    'image_path' => $imagePayload['image_path'],
                    'image_url' => $imagePayload['image_url'],
                    'thumb_path' => $imagePayload['thumb_path'],
                    'thumb_url' => $imagePayload['thumb_url'],
                    'image_srcset' => $imagePayload['srcset'],
                    'settings' => (string) ($result->settings ?? ''),
                    'strengths' => (string) ($result->strengths ?? ''),
                    'weaknesses' => (string) ($result->weaknesses ?? ''),
                    'best_for' => (string) ($result->best_for ?? ''),
                    'score' => $result->score,
                    'sort_order' => (int) $result->sort_order,
                    'active' => (bool) $result->active,
                ];
            })
            ->values()
            ->all();

        $hasPromptData = filled($payload['prompt'] ?? null)
            || filled($payload['negative_prompt'] ?? null)
            || filled($payload['intro'] ?? null)
            || filled($payload['title'] ?? null)
            || filled($payload['aspect_ratio'] ?? null)
            || ! empty($criteria);

        if (! $hasPromptData && $results === []) {
            return null;
        }

        return [
            'id' => (int) $block->id,
            'type' => (string) $block->type,
            'title' => (string) ($block->title ?? ($payload['title'] ?? '')),
            'payload' => [
                'title' => (string) ($payload['title'] ?? ''),
                'intro' => (string) ($payload['intro'] ?? ''),
                'prompt' => (string) ($payload['prompt'] ?? ''),
                'negative_prompt' => (string) ($payload['negative_prompt'] ?? ''),
                'aspect_ratio' => (string) ($payload['aspect_ratio'] ?? ''),
                'criteria' => $criteria,
            ],
            'sort_order' => (int) $block->sort_order,
            'active' => (bool) $block->active,
            'comparison_results' => $results,
        ];
    }

    private function injectHeadingIds(string $html): string
    {
        if ($html === '') {
            return $html;
        }

        $seenIds = [];

        return preg_replace_callback(
            '/<(h[23])([^>]*)>(.*?)<\/\1>/si',
            function (array $m) use (&$seenIds): string {
                [, $tag, $attrs, $inner] = $m;

                if (preg_match('/\bid\s*=/i', $attrs)) {
                    return $m[0];
                }

                $textContent = strip_tags($inner);
                $baseId = $this->slugifyHeading($textContent);
                $count = $seenIds[$baseId] ?? 0;
                $id = $count > 0 ? $baseId.'-'.($count + 1) : $baseId;
                $seenIds[$baseId] = $count + 1;

                return "<{$tag} id=\"{$id}\"{$attrs}>{$inner}</{$tag}>";
            },
            $html
        ) ?? $html;
    }

    private function slugifyHeading(string $text): string
    {
        $slug = mb_strtolower($text);
        $slug = preg_replace('/[^a-z0-9_\s\-]/', '', $slug) ?? '';
        $slug = preg_replace('/\s+/', '-', trim($slug)) ?? '';
        $slug = preg_replace('/-+/', '-', $slug) ?? '';
        $slug = trim($slug, '-');

        return $slug !== '' ? $slug : 'section';
    }
}
