<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Group;
use App\Models\GroupPost;
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class GroupPostService
{
    public function __construct(
        private readonly GroupHistoryService $history,
        private readonly NotificationService $notifications,
    ) {
    }

    public function create(Group $group, User $actor, array $attributes): GroupPost
    {
        $post = GroupPost::query()->create([
            'group_id' => $group->id,
            'author_user_id' => $actor->id,
            'type' => (string) ($attributes['type'] ?? GroupPost::TYPE_ANNOUNCEMENT),
            'title' => trim((string) $attributes['title']),
            'slug' => $this->makeUniqueSlug((string) $attributes['title']),
            'excerpt' => $this->normalizeExcerpt($attributes['excerpt'] ?? null, $attributes['content'] ?? null),
            'content' => $this->sanitizeContent($attributes['content'] ?? null),
            'cover_path' => $attributes['cover_path'] ?? null,
            'status' => GroupPost::STATUS_DRAFT,
            'is_pinned' => false,
            'published_at' => null,
        ]);

        $this->history->record(
            $group,
            $actor,
            'post_created',
            sprintf('Created draft post "%s".', $post->title),
            'group_post',
            (int) $post->id,
            null,
            $post->only(['type', 'title', 'status']),
        );

        return $post->fresh(['group', 'author.profile']);
    }

    public function update(GroupPost $post, User $actor, array $attributes): GroupPost
    {
        $before = $post->only(['type', 'title', 'excerpt', 'content', 'cover_path', 'status', 'is_pinned']);

        $title = trim((string) ($attributes['title'] ?? $post->title));

        $post->fill([
            'type' => (string) ($attributes['type'] ?? $post->type),
            'title' => $title,
            'slug' => $title !== $post->title ? $this->makeUniqueSlug($title, (int) $post->id) : $post->slug,
            'excerpt' => array_key_exists('excerpt', $attributes)
                ? $this->normalizeExcerpt($attributes['excerpt'], $attributes['content'] ?? $post->content)
                : $post->excerpt,
            'content' => array_key_exists('content', $attributes)
                ? $this->sanitizeContent($attributes['content'])
                : $post->content,
            'cover_path' => array_key_exists('cover_path', $attributes) ? ($attributes['cover_path'] ?: null) : $post->cover_path,
        ])->save();

        $this->history->record(
            $post->group,
            $actor,
            'post_updated',
            sprintf('Updated post "%s".', $post->title),
            'group_post',
            (int) $post->id,
            $before,
            $post->only(['type', 'title', 'excerpt', 'content', 'cover_path', 'status', 'is_pinned']),
        );

        return $post->fresh(['group', 'author.profile']);
    }

    public function publish(GroupPost $post, User $actor): GroupPost
    {
        if ($post->group->status !== Group::LIFECYCLE_ACTIVE) {
            throw ValidationException::withMessages([
                'group' => 'Archived or suspended groups cannot publish posts.',
            ]);
        }

        if (trim((string) $post->title) === '') {
            throw ValidationException::withMessages([
                'title' => 'A published post needs a title.',
            ]);
        }

        $before = $post->only(['status', 'published_at']);

        $post->forceFill([
            'status' => GroupPost::STATUS_PUBLISHED,
            'published_at' => now(),
        ])->save();

        $this->history->record(
            $post->group,
            $actor,
            'post_published',
            sprintf('Published post "%s".', $post->title),
            'group_post',
            (int) $post->id,
            $before,
            ['status' => GroupPost::STATUS_PUBLISHED, 'published_at' => $post->published_at?->toISOString()],
        );

        app(GroupActivityService::class)->record(
            $post->group,
            $actor,
            'post_published',
            'group_post',
            (int) $post->id,
            sprintf('%s published a new group post: %s', $post->group->name, $post->title),
            $post->excerpt,
            'public',
        );

        foreach ($post->group->follows()->with('user')->get() as $follow) {
            if ($follow->user) {
                $this->notifications->notifyGroupPostPublished($follow->user, $actor, $post->group, $post);
            }
        }

        return $post->fresh(['group', 'author.profile']);
    }

    public function pin(GroupPost $post, User $actor, bool $isPinned = true): GroupPost
    {
        DB::transaction(function () use ($post, $actor, $isPinned): void {
            if ($isPinned) {
                GroupPost::query()
                    ->where('group_id', $post->group_id)
                    ->where('id', '!=', $post->id)
                    ->where('is_pinned', true)
                    ->update([
                        'is_pinned' => false,
                        'updated_at' => now(),
                    ]);
            }

            $before = ['is_pinned' => (bool) $post->is_pinned];

            $post->forceFill([
                'is_pinned' => $isPinned,
            ])->save();

            $this->history->record(
                $post->group,
                $actor,
                $isPinned ? 'post_pinned' : 'post_unpinned',
                sprintf('%s post "%s".', $isPinned ? 'Pinned' : 'Unpinned', $post->title),
                'group_post',
                (int) $post->id,
                $before,
                ['is_pinned' => $isPinned],
            );
        });

        return $post->fresh(['group', 'author.profile']);
    }

    public function archive(GroupPost $post, User $actor): GroupPost
    {
        $before = $post->only(['status', 'is_pinned']);

        $post->forceFill([
            'status' => GroupPost::STATUS_ARCHIVED,
            'is_pinned' => false,
        ])->save();

        $this->history->record(
            $post->group,
            $actor,
            'post_archived',
            sprintf('Archived post "%s".', $post->title),
            'group_post',
            (int) $post->id,
            $before,
            ['status' => GroupPost::STATUS_ARCHIVED, 'is_pinned' => false],
        );

        return $post->fresh(['group', 'author.profile']);
    }

    public function publicPosts(Group $group, int $limit = 12): array
    {
        return GroupPost::query()
            ->with('author.profile')
            ->where('group_id', $group->id)
            ->where('status', GroupPost::STATUS_PUBLISHED)
            ->orderByDesc('is_pinned')
            ->orderByDesc('published_at')
            ->limit($limit)
            ->get()
            ->map(fn (GroupPost $post): array => $this->mapPublicPost($group, $post))
            ->values()
            ->all();
    }

    public function pinnedPost(Group $group): ?array
    {
        $post = GroupPost::query()
            ->with('author.profile')
            ->where('group_id', $group->id)
            ->where('status', GroupPost::STATUS_PUBLISHED)
            ->where('is_pinned', true)
            ->latest('published_at')
            ->first();

        return $post ? $this->mapPublicPost($group, $post) : null;
    }

    public function recentPosts(Group $group, int $limit = 3): array
    {
        return GroupPost::query()
            ->with('author.profile')
            ->where('group_id', $group->id)
            ->where('status', GroupPost::STATUS_PUBLISHED)
            ->latest('published_at')
            ->limit($limit)
            ->get()
            ->map(fn (GroupPost $post): array => $this->mapPublicPost($group, $post))
            ->values()
            ->all();
    }

    public function studioListing(Group $group, array $filters = []): array
    {
        $bucket = (string) ($filters['bucket'] ?? 'all');
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = min(max((int) ($filters['per_page'] ?? 20), 10), 50);

        $query = GroupPost::query()
            ->with('author.profile')
            ->where('group_id', $group->id);

        if ($bucket !== 'all') {
            $query->where('status', $bucket);
        }

        $paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page);

        return [
            'items' => collect($paginator->items())->map(fn (GroupPost $post): array => $this->mapStudioPost($group, $post))->values()->all(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'last_page' => $paginator->lastPage(),
                'per_page' => $paginator->perPage(),
                'total' => $paginator->total(),
            ],
            'filters' => [
                'bucket' => $bucket,
            ],
            'bucket_options' => [
                ['value' => 'all', 'label' => 'All'],
                ['value' => GroupPost::STATUS_DRAFT, 'label' => 'Drafts'],
                ['value' => GroupPost::STATUS_PUBLISHED, 'label' => 'Published'],
                ['value' => GroupPost::STATUS_ARCHIVED, 'label' => 'Archived'],
            ],
        ];
    }

    public function mapPublicPost(Group $group, GroupPost $post): array
    {
        return [
            'id' => (int) $post->id,
            'type' => (string) $post->type,
            'title' => (string) $post->title,
            'slug' => (string) $post->slug,
            'excerpt' => $post->excerpt,
            'content' => $post->content,
            'cover_url' => $post->cover_path,
            'is_pinned' => (bool) $post->is_pinned,
            'published_at' => $post->published_at?->toISOString(),
            'author' => $post->author ? [
                'id' => (int) $post->author->id,
                'name' => $post->author->name,
                'username' => $post->author->username,
            ] : null,
            'url' => route('groups.posts.show', ['group' => $group, 'post' => $post]),
        ];
    }

    public function mapStudioPost(Group $group, GroupPost $post): array
    {
        return [
            'id' => (int) $post->id,
            'type' => (string) $post->type,
            'title' => (string) $post->title,
            'excerpt' => $post->excerpt,
            'content' => $post->content,
            'cover_url' => $post->cover_path,
            'status' => (string) $post->status,
            'is_pinned' => (bool) $post->is_pinned,
            'published_at' => $post->published_at?->toISOString(),
            'updated_at' => $post->updated_at?->toISOString(),
            'author' => $post->author ? [
                'id' => (int) $post->author->id,
                'name' => $post->author->name,
                'username' => $post->author->username,
            ] : null,
            'urls' => [
                'public' => $post->status === GroupPost::STATUS_PUBLISHED ? route('groups.posts.show', ['group' => $group, 'post' => $post]) : null,
                'edit' => route('studio.groups.posts.edit', ['group' => $group, 'post' => $post]),
                'publish' => route('studio.groups.posts.publish', ['group' => $group, 'post' => $post]),
                'pin' => route('studio.groups.posts.pin', ['group' => $group, 'post' => $post]),
                'archive' => route('studio.groups.posts.archive', ['group' => $group, 'post' => $post]),
            ],
        ];
    }

    private function sanitizeContent(?string $content): ?string
    {
        $trimmed = trim(strip_tags((string) ($content ?? '')));

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

    private function normalizeExcerpt(?string $excerpt, ?string $content): ?string
    {
        $trimmed = trim((string) ($excerpt ?? ''));
        if ($trimmed !== '') {
            return Str::limit($trimmed, 320, '');
        }

        $body = $this->sanitizeContent($content);

        return $body ? Str::limit($body, 280) : null;
    }

    private function makeUniqueSlug(string $source, ?int $ignorePostId = null): string
    {
        $base = Str::slug(Str::limit($source, 180, ''));
        $base = $base !== '' ? $base : 'group-post';
        $slug = $base;
        $suffix = 2;

        while (GroupPost::query()
            ->where('slug', $slug)
            ->when($ignorePostId !== null, fn ($query) => $query->where('id', '!=', $ignorePostId))
            ->exists()) {
            $slug = Str::limit($base, 172, '') . '-' . $suffix;
            $suffix++;
        }

        return $slug;
    }
}