<?php

declare(strict_types=1);

namespace App\Http\Controllers\Studio;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\News\NewsService;
use App\Support\AvatarUrl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Inertia\Inertia;
use Inertia\Response;
use cPad\Plugins\News\Models\NewsArticle;
use cPad\Plugins\News\Models\NewsCategory;
use cPad\Plugins\News\Models\NewsTag;

final class StudioNewsController extends Controller
{
    public function __construct(private readonly NewsService $news)
    {
    }

    public function index(Request $request): Response
    {
        $this->authorizeNews($request);

        return Inertia::render('Studio/StudioNewsIndex', [
            'title' => 'Newsroom',
            'description' => 'Plan announcements, publish editorial stories, and connect articles to the rest of Nova.',
            'listing' => $this->news->studioListing($request->only(['q', 'status', 'type', 'category_id', 'per_page', 'page', 'order', 'direction'])),
            'statusOptions' => $this->news->editorialStatusOptions(),
            'typeOptions' => $this->news->articleTypeOptions(),
            'categoryOptions' => $this->news->categoryOptions(),
            'createUrl' => route('studio.news.create'),
            'categoriesUrl' => route('studio.news.categories'),
            'tagsUrl' => route('studio.news.tags'),
        ]);
    }

    public function create(Request $request): Response
    {
        $this->authorizeNews($request);

        $user = $request->user();

        return Inertia::render('Studio/StudioNewsEditor', [
            'title' => 'Create article',
            'description' => 'Draft a new News story with editorial workflow, SEO metadata, and related entity links.',
            'article' => null,
            'oldInput' => $request->session()->getOldInput(),
            'indexUrl' => route('studio.news.index'),
            'typeOptions' => $this->news->articleTypeOptions(),
            'statusOptions' => $this->news->editorialStatusOptions(),
            'categoryOptions' => $this->news->categoryOptions(),
            'tagOptions' => $this->news->tagOptions(),
            'newsTagLimit' => 30,
            'relationTypeOptions' => $this->news->relationTypeOptions(),
            'storeUrl' => route('studio.news.store'),
            'coverUploadUrl' => route('api.studio.news.media.upload'),
            'coverDeleteUrl' => route('api.studio.news.media.destroy'),
            'bodyMediaUploadUrl' => route('api.studio.news.media.upload'),
            'bodyMediaDeleteUrl' => route('api.studio.news.media.destroy'),
            'coverCdnBaseUrl' => rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/'),
            'entitySearchUrl' => route('studio.news.entity-search'),
            'categoriesUrl' => route('studio.news.categories'),
            'tagsUrl' => route('studio.news.tags'),
            'defaultAuthor' => $this->mapDefaultAuthor($user),
            'defaultPublishedAt' => now()->format('Y-m-d\TH:i'),
        ]);
    }

    public function store(Request $request): RedirectResponse
    {
        $this->authorizeNews($request);

        $article = $this->news->storeArticle($request->user(), $this->validateArticle($request));

        return redirect()->route('studio.news.edit', ['article' => $article->id])->with('success', 'Article draft created.');
    }

    public function edit(Request $request, NewsArticle $article): Response
    {
        $this->authorizeNews($request);

        return Inertia::render('Studio/StudioNewsEditor', [
            'title' => 'Edit article',
            'description' => 'Refine the story, tune SEO, and attach related Nova entities before publishing.',
            'article' => $this->news->mapStudioArticle($article, $request->user()),
            'oldInput' => $request->session()->getOldInput(),
            'indexUrl' => route('studio.news.index'),
            'typeOptions' => $this->news->articleTypeOptions(),
            'statusOptions' => $this->news->editorialStatusOptions(),
            'categoryOptions' => $this->news->categoryOptions(),
            'tagOptions' => $this->news->tagOptions(),
            'newsTagLimit' => 30,
            'relationTypeOptions' => $this->news->relationTypeOptions(),
            'coverUploadUrl' => route('api.studio.news.media.upload'),
            'coverDeleteUrl' => route('api.studio.news.media.destroy'),
            'bodyMediaUploadUrl' => route('api.studio.news.media.upload'),
            'bodyMediaDeleteUrl' => route('api.studio.news.media.destroy'),
            'coverCdnBaseUrl' => rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/'),
            'updateUrl' => route('studio.news.update', ['article' => $article->id]),
            'destroyUrl' => route('studio.news.destroy', ['article' => $article->id]),
            'previewUrl' => route('studio.news.preview', ['article' => $article->id]),
            'publishUrl' => route('studio.news.publish', ['article' => $article->id]),
            'archiveUrl' => route('studio.news.archive', ['article' => $article->id]),
            'featureUrl' => route('studio.news.feature', ['article' => $article->id]),
            'pinUrl' => route('studio.news.pin', ['article' => $article->id]),
            'entitySearchUrl' => route('studio.news.entity-search'),
            'categoriesUrl' => route('studio.news.categories'),
            'tagsUrl' => route('studio.news.tags'),
        ]);
    }

    public function preview(Request $request, NewsArticle $article): View
    {
        $this->authorizeNews($request);

        $article->loadMissing(['author.profile', 'category', 'tags', 'relatedEntities']);

        $related = NewsArticle::with('author', 'category')
            ->published()
            ->when($article->category_id, fn ($query) => $query->where('category_id', $article->category_id))
            ->where('id', '!=', $article->id)
            ->editorialOrder()
            ->limit(config('news.related_limit', 4))
            ->get();

        return view('news.show', [
            'article' => $article,
            'related' => $related,
            'relatedEntities' => $this->news->resolveRelatedEntities($article, $request->user()),
            'comments' => collect(),
            'commentsCount' => 0,
            'previewMode' => true,
            'previewCanonical' => route('studio.news.preview', ['article' => $article->id]),
            'previewBackUrl' => route('studio.news.edit', ['article' => $article->id]),
        ] + $this->news->sidebarData());
    }

    public function update(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $this->news->updateArticle($article, $request->user(), $this->validateArticle($request, $article));

        return redirect()->route('studio.news.edit', ['article' => $article->id])->with('success', 'Article updated.');
    }

    public function destroy(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $this->news->deleteArticle($article);

        return redirect()->route('studio.news.index')->with('success', 'Article moved to trash.');
    }

    public function publish(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $this->news->publish($article);

        return back()->with('success', 'Article published.');
    }

    public function archive(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $this->news->archive($article);

        return back()->with('success', 'Article archived.');
    }

    public function feature(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $updated = $this->news->toggleFeature($article);

        return back()->with('success', $updated->is_featured ? 'Article featured.' : 'Article removed from featured surface.');
    }

    public function pin(Request $request, NewsArticle $article): RedirectResponse
    {
        $this->authorizeNews($request);

        $updated = $this->news->togglePin($article);

        return back()->with('success', $updated->is_pinned ? 'Article pinned.' : 'Article unpinned.');
    }

    public function categories(Request $request): Response
    {
        $this->authorizeNews($request);

        return Inertia::render('Studio/StudioNewsTaxonomies', [
            'title' => 'News taxonomies',
            'description' => 'Manage News categories and tags used across the editorial surface.',
            'activeTab' => 'categories',
            'categories' => NewsCategory::query()
                ->withCount('publishedArticles')
                ->ordered()
                ->get()
                ->map(fn (NewsCategory $category): array => [
                    'id' => (int) $category->id,
                    'name' => (string) $category->name,
                    'slug' => (string) $category->slug,
                    'description' => (string) ($category->description ?? ''),
                    'position' => (int) $category->position,
                    'is_active' => (bool) $category->is_active,
                    'published_count' => (int) $category->published_articles_count,
                ])
                ->all(),
            'tags' => $this->tagPayload(),
            'storeCategoryUrl' => route('studio.news.categories.store'),
            'storeTagUrl' => route('studio.news.tags.store'),
            'updateCategoryUrlPattern' => route('studio.news.categories.update', ['category' => '__CATEGORY__']),
            'updateTagUrlPattern' => route('studio.news.tags.update', ['tag' => '__TAG__']),
        ]);
    }

    public function tags(Request $request): Response
    {
        $this->authorizeNews($request);

        return Inertia::render('Studio/StudioNewsTaxonomies', [
            'title' => 'News taxonomies',
            'description' => 'Manage News categories and tags used across the editorial surface.',
            'activeTab' => 'tags',
            'categories' => NewsCategory::query()
                ->withCount('publishedArticles')
                ->ordered()
                ->get()
                ->map(fn (NewsCategory $category): array => [
                    'id' => (int) $category->id,
                    'name' => (string) $category->name,
                    'slug' => (string) $category->slug,
                    'description' => (string) ($category->description ?? ''),
                    'position' => (int) $category->position,
                    'is_active' => (bool) $category->is_active,
                    'published_count' => (int) $category->published_articles_count,
                ])
                ->all(),
            'tags' => $this->tagPayload(),
            'storeCategoryUrl' => route('studio.news.categories.store'),
            'storeTagUrl' => route('studio.news.tags.store'),
            'updateCategoryUrlPattern' => route('studio.news.categories.update', ['category' => '__CATEGORY__']),
            'updateTagUrlPattern' => route('studio.news.tags.update', ['tag' => '__TAG__']),
        ]);
    }

    private function mapDefaultAuthor(mixed $user): ?array
    {
        if (! $user instanceof User) {
            return null;
        }

        $user->loadMissing('profile');

        return [
            'id' => (int) $user->id,
            'entity_type' => 'user',
            'entity_label' => 'User',
            'title' => (string) ($user->name ?: $user->username),
            'subtitle' => $user->username ? '@' . $user->username : null,
            'description' => Str::limit(trim((string) ($user->profile?->bio ?? '')), 120),
            'url' => $user->username ? route('profile.show', ['username' => $user->username]) : null,
            'image' => null,
            'avatar' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null, 96),
            'context_label' => 'Profile',
            'meta' => [],
        ];
    }

    public function storeCategory(Request $request): RedirectResponse
    {
        $this->authorizeNews($request);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:120', 'unique:news_categories,name'],
            'slug' => ['nullable', 'string', 'max:120', 'unique:news_categories,slug'],
            'description' => ['nullable', 'string'],
            'position' => ['nullable', 'integer', 'min:0', 'max:65535'],
            'is_active' => ['nullable', 'boolean'],
        ]);

        NewsCategory::query()->create([
            'name' => trim((string) $validated['name']),
            'slug' => NewsCategory::generateUniqueSlug((string) ($validated['slug'] ?? $validated['name'])),
            'description' => $validated['description'] ?? null,
            'position' => (int) ($validated['position'] ?? 0),
            'is_active' => (bool) ($validated['is_active'] ?? true),
        ]);

        return back()->with('success', 'Category created.');
    }

    public function updateCategory(Request $request, NewsCategory $category): RedirectResponse
    {
        $this->authorizeNews($request);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:120', Rule::unique('news_categories', 'name')->ignore($category->id)],
            'slug' => ['nullable', 'string', 'max:120', Rule::unique('news_categories', 'slug')->ignore($category->id)],
            'description' => ['nullable', 'string'],
            'position' => ['nullable', 'integer', 'min:0', 'max:65535'],
            'is_active' => ['nullable', 'boolean'],
        ]);

        $category->update([
            'name' => trim((string) $validated['name']),
            'slug' => NewsCategory::generateUniqueSlug((string) ($validated['slug'] ?? $validated['name']), (int) $category->id),
            'description' => $validated['description'] ?? null,
            'position' => (int) ($validated['position'] ?? 0),
            'is_active' => (bool) ($validated['is_active'] ?? true),
        ]);

        return back()->with('success', 'Category updated.');
    }

    public function storeTag(Request $request): RedirectResponse
    {
        $this->authorizeNews($request);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:80', 'unique:news_tags,name'],
            'slug' => ['nullable', 'string', 'max:80', 'unique:news_tags,slug'],
        ]);

        NewsTag::query()->create([
            'name' => trim((string) $validated['name']),
            'slug' => $this->uniqueTagSlug((string) ($validated['slug'] ?? $validated['name'])),
        ]);

        return back()->with('success', 'Tag created.');
    }

    public function updateTag(Request $request, NewsTag $tag): RedirectResponse
    {
        $this->authorizeNews($request);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:80', Rule::unique('news_tags', 'name')->ignore($tag->id)],
            'slug' => ['nullable', 'string', 'max:80', Rule::unique('news_tags', 'slug')->ignore($tag->id)],
        ]);

        $tag->update([
            'name' => trim((string) $validated['name']),
            'slug' => $this->uniqueTagSlug((string) ($validated['slug'] ?? $validated['name']), (int) $tag->id),
        ]);

        return back()->with('success', 'Tag updated.');
    }

    public function entitySearch(Request $request): JsonResponse
    {
        $this->authorizeNews($request);

        $validated = $request->validate([
            'type' => ['required', Rule::in(array_column($this->news->relationTypeOptions(), 'value'))],
            'q' => ['nullable', 'string', 'max:120'],
        ]);

        return response()->json([
            'items' => $this->news->searchEntities((string) $validated['type'], (string) ($validated['q'] ?? ''), $request->user()),
        ]);
    }

    private function authorizeNews(Request $request): void
    {
        abort_unless($request->user() && ($request->user()->isAdmin() || $request->user()->isModerator()), 403);
    }

    private function validateArticle(Request $request, ?NewsArticle $article = null): array
    {
        $validated = $request->validate([
            'title' => ['required', 'string', 'max:255'],
            'slug' => ['nullable', 'string', 'max:255'],
            'excerpt' => ['nullable', 'string', 'max:800'],
            'content' => ['required', 'string', 'max:500000'],
            'cover_image' => ['nullable', 'string', 'max:2048'],
            'type' => ['required', Rule::in(array_column($this->news->articleTypeOptions(), 'value'))],
            'category_id' => ['required', 'integer', 'exists:news_categories,id'],
            'author_id' => ['nullable', 'integer', 'exists:users,id'],
            'editorial_status' => ['required', Rule::in(array_column($this->news->editorialStatusOptions(), 'value'))],
            'published_at' => ['nullable', 'date'],
            'is_featured' => ['nullable', 'boolean'],
            'is_pinned' => ['nullable', 'boolean'],
            'comments_enabled' => ['nullable', 'boolean'],
            'tag_ids' => ['nullable', 'array'],
            'tag_ids.*' => ['integer', 'exists:news_tags,id'],
            'new_tag_names' => ['nullable', 'array', 'max:30'],
            'new_tag_names.*' => ['string', 'max:80'],
            'meta_title' => ['nullable', 'string', 'max:255'],
            'meta_description' => ['nullable', 'string', 'max:300'],
            'meta_keywords' => ['nullable', 'string', 'max:255'],
            'og_title' => ['nullable', 'string', 'max:255'],
            'og_description' => ['nullable', 'string', 'max:300'],
            'og_image' => ['nullable', 'string', 'max:2048'],
            'relations' => ['nullable', 'array', 'max:12'],
            'relations.*.entity_type' => ['required_with:relations', Rule::in(array_column($this->news->relationTypeOptions(), 'value'))],
            'relations.*.entity_id' => ['nullable', 'integer', 'min:1'],
            'relations.*.external_url' => ['nullable', 'string', 'max:2048'],
            'relations.*.context_label' => ['nullable', 'string', 'max:120'],
        ]);

        $relationErrors = [];

        foreach ((array) ($validated['relations'] ?? []) as $index => $relation) {
            $entityType = trim(Str::lower((string) ($relation['entity_type'] ?? '')));

            if ($entityType === NewsService::RELATION_SOURCE) {
                $externalUrl = $this->normalizeExternalRelationUrl($relation['external_url'] ?? null);

                if ($externalUrl === null) {
                    $relationErrors["relations.{$index}.external_url"] = 'Source relations need a valid URL.';
                    continue;
                }

                $validated['relations'][$index]['entity_id'] = null;
                $validated['relations'][$index]['external_url'] = $externalUrl;

                continue;
            }

            if ((int) ($relation['entity_id'] ?? 0) < 1) {
                $relationErrors["relations.{$index}.entity_id"] = 'Select a related entity.';
            }

            $validated['relations'][$index]['external_url'] = null;
        }

        if ($relationErrors !== []) {
            throw ValidationException::withMessages($relationErrors);
        }

        if (($validated['editorial_status'] ?? null) === NewsArticle::EDITORIAL_STATUS_SCHEDULED && empty($validated['published_at'])) {
            throw ValidationException::withMessages([
                'published_at' => 'Scheduled articles need a publish date and time.',
            ]);
        }

        return $validated;
    }

    private function normalizeExternalRelationUrl(mixed $value): ?string
    {
        $url = trim((string) ($value ?? ''));

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

        if (preg_match('/^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/i', $url, $matches) === 1) {
            $url = trim((string) ($matches[1] ?? ''));
        }

        if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
            return null;
        }

        return Str::limit($url, 2048, '');
    }

    private function tagPayload(): array
    {
        return NewsTag::query()
            ->withCount(['articles' => fn ($query) => $query->published()])
            ->orderBy('name')
            ->get()
            ->map(fn (NewsTag $tag): array => [
                'id' => (int) $tag->id,
                'name' => (string) $tag->name,
                'slug' => (string) $tag->slug,
                'published_count' => (int) $tag->articles_count,
            ])
            ->all();
    }

    private function uniqueTagSlug(string $source, ?int $ignoreId = null): string
    {
        $base = Str::slug($source);
        $slug = $base !== '' ? $base : 'tag';
        $counter = 1;

        $query = NewsTag::query()->where('slug', $slug);
        if ($ignoreId !== null) {
            $query->where('id', '!=', $ignoreId);
        }

        while ($query->exists()) {
            $slug = ($base !== '' ? $base : 'tag') . '-' . $counter++;
            $query = NewsTag::query()->where('slug', $slug);
            if ($ignoreId !== null) {
                $query->where('id', '!=', $ignoreId);
            }
        }

        return $slug;
    }
}