<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\ReactionType;
use App\Models\ActivityEvent;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Models\CommentReaction;
use App\Models\Story;
use App\Models\User;
use App\Models\UserMention;
use App\Services\ThumbnailPresenter;
use App\Support\AvatarUrl;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

final class CommunityActivityService
{
    public const DEFAULT_PER_PAGE = 20;

    private const FILTER_ALL = 'all';
    private const FILTER_COMMENTS = 'comments';
    private const FILTER_REPLIES = 'replies';
    private const FILTER_FOLLOWING = 'following';
    private const FILTER_MY = 'my';

    public function getFeed(?User $viewer, string $filter = self::FILTER_ALL, int $page = 1, int $perPage = self::DEFAULT_PER_PAGE, ?int $actorUserId = null): array
    {
        $normalizedFilter = $this->normalizeFilter($filter);
        $resolvedPage = max(1, $page);
        $resolvedPerPage = max(1, min(50, $perPage));

        $cacheKey = sprintf(
            'community_activity:%s:%d:%d:%d:%d',
            $normalizedFilter,
            (int) ($viewer?->id ?? 0),
            (int) ($actorUserId ?? 0),
            $resolvedPage,
            $resolvedPerPage,
        );

        return Cache::remember($cacheKey, now()->addSeconds(30), function () use ($viewer, $normalizedFilter, $resolvedPage, $resolvedPerPage, $actorUserId): array {
            return $this->buildFeed($viewer, $normalizedFilter, $resolvedPage, $resolvedPerPage, $actorUserId);
        });
    }

    public function requiresAuthentication(string $filter): bool
    {
        return in_array($this->normalizeFilter($filter), [self::FILTER_FOLLOWING, self::FILTER_MY], true);
    }

    public function normalizeFilter(string $filter): string
    {
        return match (strtolower(trim($filter))) {
            self::FILTER_COMMENTS => self::FILTER_COMMENTS,
            self::FILTER_REPLIES => self::FILTER_REPLIES,
            self::FILTER_FOLLOWING => self::FILTER_FOLLOWING,
            self::FILTER_MY => self::FILTER_MY,
            default => self::FILTER_ALL,
        };
    }

    private function buildFeed(?User $viewer, string $filter, int $page, int $perPage, ?int $actorUserId): array
    {
        $sourceLimit = max(80, $page * $perPage * 6);
        $followingIds = $filter === self::FILTER_FOLLOWING && $viewer
            ? $viewer->following()->pluck('users.id')->map(fn ($id) => (int) $id)->all()
            : [];

        $commentModels = $this->fetchCommentModels($sourceLimit, repliesOnly: false);
        $replyModels = $this->fetchCommentModels($sourceLimit, repliesOnly: true);
        $reactionModels = $this->fetchReactionModels($sourceLimit);
        $recordedActivities = $this->fetchRecordedActivities($sourceLimit);

        $commentActivities = $commentModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'comment'));
        $replyActivities = $replyModels->map(fn (ArtworkComment $comment) => $this->mapCommentActivity($comment, 'reply'));
        $reactionActivities = $reactionModels->map(fn (CommentReaction $reaction) => $this->mapReactionActivity($reaction));
        $mentionActivities = $this->fetchMentionActivities($sourceLimit);

        $merged = $recordedActivities
            ->concat($commentActivities)
            ->concat($replyActivities)
            ->concat($reactionActivities)
            ->concat($mentionActivities)
            ->filter(function (array $activity) use ($filter, $viewer, $followingIds, $actorUserId): bool {
                $actorId = (int) ($activity['user']['id'] ?? 0);
                $mentionedUserId = (int) ($activity['mentioned_user']['id'] ?? 0);

                if ($actorUserId !== null && $actorId !== (int) $actorUserId) {
                    return false;
                }

                return match ($filter) {
                    self::FILTER_COMMENTS => $activity['type'] === 'comment',
                    self::FILTER_REPLIES => $activity['type'] === 'reply',
                    self::FILTER_FOLLOWING => in_array($actorId, $followingIds, true),
                    self::FILTER_MY => $viewer !== null
                        && ($actorId === (int) $viewer->id || ($activity['type'] === 'mention' && $mentionedUserId === (int) $viewer->id)),
                    default => true,
                };
            })
            ->sortByDesc(fn (array $activity) => $activity['sort_timestamp'] ?? $activity['created_at'])
            ->values();

        $total = $merged->count();
        $offset = ($page - 1) * $perPage;
        $pageItems = $merged->slice($offset, $perPage)->values();
        $reactionTotals = $this->loadCommentReactionTotals(
            $pageItems->pluck('comment.id')->filter()->map(fn ($id) => (int) $id)->unique()->all(),
            $viewer?->id,
        );

        $data = $pageItems->map(function (array $activity) use ($reactionTotals): array {
            $commentId = (int) ($activity['comment']['id'] ?? 0);
            if ($commentId > 0) {
                $activity['comment']['reactions'] = $reactionTotals[$commentId] ?? $this->defaultReactionTotals();
            }

            unset($activity['sort_timestamp']);

            return $activity;
        })->all();

        return [
            'data' => $data,
            'meta' => [
                'current_page' => $page,
                'last_page' => (int) max(1, ceil($total / $perPage)),
                'per_page' => $perPage,
                'total' => $total,
                'has_more' => $offset + $perPage < $total,
            ],
            'filter' => $filter,
        ];
    }

    private function fetchRecordedActivities(int $limit): Collection
    {
        $events = ActivityEvent::query()
            ->select(['id', 'actor_id', 'type', 'target_type', 'target_id', 'meta', 'created_at'])
            ->with([
                'actor' => function ($query) {
                    $query
                        ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                        ->with('profile:user_id,avatar_hash')
                        ->withCount('artworks');
                },
            ])
            ->whereHas('actor', fn ($query) => $query->where('is_active', true)->whereNull('deleted_at'))
            ->latest('created_at')
            ->limit($limit)
            ->get();

        if ($events->isEmpty()) {
            return collect();
        }

        $artworkIds = $events
            ->where('target_type', ActivityEvent::TARGET_ARTWORK)
            ->pluck('target_id')
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $storyIds = $events
            ->where('target_type', ActivityEvent::TARGET_STORY)
            ->pluck('target_id')
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $targetUserIds = $events
            ->where('target_type', ActivityEvent::TARGET_USER)
            ->pluck('target_id')
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $artworks = empty($artworkIds)
            ? collect()
            : Artwork::query()
                ->select('id', 'user_id', 'title', 'slug', 'hash', 'thumb_ext', 'published_at', 'deleted_at', 'is_public', 'is_approved')
                ->whereIn('id', $artworkIds)
                ->public()
                ->published()
                ->whereNull('deleted_at')
                ->get()
                ->keyBy('id');

        $stories = empty($storyIds)
            ? collect()
            : Story::query()
                ->select('id', 'creator_id', 'title', 'slug', 'cover_image', 'published_at', 'status')
                ->whereIn('id', $storyIds)
                ->published()
                ->get()
                ->keyBy('id');

        $targetUsers = empty($targetUserIds)
            ? collect()
            : User::query()
                ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                ->with('profile:user_id,avatar_hash')
                ->withCount('artworks')
                ->whereIn('id', $targetUserIds)
                ->where('is_active', true)
                ->whereNull('deleted_at')
                ->get()
                ->keyBy('id');

        return $events
            ->map(fn (ActivityEvent $event) => $this->mapRecordedActivity($event, $artworks, $stories, $targetUsers))
            ->filter()
            ->values();
    }

    private function fetchCommentModels(int $limit, bool $repliesOnly): Collection
    {
        return ArtworkComment::query()
            ->select([
                'id',
                'artwork_id',
                'user_id',
                'parent_id',
                'content',
                'raw_content',
                'rendered_content',
                'created_at',
                'is_approved',
            ])
            ->with([
                'user' => function ($query) {
                    $query
                        ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                        ->with('profile:user_id,avatar_hash')
                        ->withCount('artworks');
                },
                'artwork' => function ($query) {
                    $query->select('id', 'user_id', 'title', 'slug', 'hash', 'thumb_ext', 'published_at', 'deleted_at', 'is_public', 'is_approved');
                },
            ])
            ->where('is_approved', true)
            ->whereNull('deleted_at')
            ->when($repliesOnly, fn ($query) => $query->whereNotNull('parent_id'), fn ($query) => $query->whereNull('parent_id'))
            ->whereHas('user', function ($query) {
                $query->where('is_active', true)->whereNull('deleted_at');
            })
            ->whereHas('artwork', function ($query) {
                $query->public()->published()->whereNull('deleted_at');
            })
            ->latest('created_at')
            ->limit($limit)
            ->get();
    }

    private function fetchReactionModels(int $limit): Collection
    {
        return CommentReaction::query()
            ->select(['id', 'comment_id', 'user_id', 'reaction', 'created_at'])
            ->with([
                'user' => function ($query) {
                    $query
                        ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                        ->with('profile:user_id,avatar_hash')
                        ->withCount('artworks');
                },
                'comment' => function ($query) {
                    $query
                        ->select('id', 'artwork_id', 'user_id', 'parent_id', 'content', 'raw_content', 'rendered_content', 'created_at', 'is_approved')
                        ->with([
                            'user' => function ($userQuery) {
                                $userQuery
                                    ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                                    ->with('profile:user_id,avatar_hash')
                                    ->withCount('artworks');
                            },
                            'artwork' => function ($artworkQuery) {
                                $artworkQuery->select('id', 'user_id', 'title', 'slug', 'hash', 'thumb_ext', 'published_at', 'deleted_at', 'is_public', 'is_approved');
                            },
                        ]);
                },
            ])
            ->whereHas('user', function ($query) {
                $query->where('is_active', true)->whereNull('deleted_at');
            })
            ->whereHas('comment', function ($query) {
                $query
                    ->where('is_approved', true)
                    ->whereNull('deleted_at')
                    ->whereHas('user', function ($userQuery) {
                        $userQuery->where('is_active', true)->whereNull('deleted_at');
                    })
                    ->whereHas('artwork', function ($artworkQuery) {
                        $artworkQuery->public()->published()->whereNull('deleted_at');
                    });
            })
            ->latest('created_at')
            ->limit($limit)
            ->get();
    }

    private function mapCommentActivity(ArtworkComment $comment, string $type): array
    {
        $artwork = $comment->artwork;
        $iso = $comment->created_at?->toIso8601String();

        return [
            'id' => $type . ':' . $comment->id,
            'type' => $type,
            'user' => $this->buildUserPayload($comment->user),
            'comment' => $this->buildCommentPayload($comment),
            'artwork' => $this->buildArtworkPayload($artwork),
            'created_at' => $iso,
            'time_ago' => $comment->created_at?->diffForHumans(),
            'sort_timestamp' => $iso,
        ];
    }

    private function mapReactionActivity(CommentReaction $reaction): array
    {
        $comment = $reaction->comment;
        $artwork = $comment?->artwork;
        $reactionType = ReactionType::tryFrom((string) $reaction->reaction);
        $iso = $reaction->created_at?->toIso8601String();

        return [
            'id' => 'reaction:' . $reaction->id,
            'type' => 'reaction',
            'user' => $this->buildUserPayload($reaction->user),
            'comment' => $comment ? $this->buildCommentPayload($comment) : null,
            'artwork' => $this->buildArtworkPayload($artwork),
            'reaction' => [
                'slug' => $reactionType?->value ?? (string) $reaction->reaction,
                'emoji' => $reactionType?->emoji() ?? '👍',
                'label' => $reactionType?->label() ?? Str::headline((string) $reaction->reaction),
            ],
            'created_at' => $iso,
            'time_ago' => $reaction->created_at?->diffForHumans(),
            'sort_timestamp' => $iso,
        ];
    }

    private function mapRecordedActivity(ActivityEvent $event, Collection $artworks, Collection $stories, Collection $targetUsers): ?array
    {
        if ($event->type === ActivityEvent::TYPE_COMMENT && $event->target_type === ActivityEvent::TARGET_ARTWORK) {
            return null;
        }

        $artwork = $event->target_type === ActivityEvent::TARGET_ARTWORK
            ? $artworks->get((int) $event->target_id)
            : null;

        $story = $event->target_type === ActivityEvent::TARGET_STORY
            ? $stories->get((int) $event->target_id)
            : null;

        $targetUser = $event->target_type === ActivityEvent::TARGET_USER
            ? $targetUsers->get((int) $event->target_id)
            : null;

        if ($event->target_type === ActivityEvent::TARGET_ARTWORK && ! $artwork) {
            return null;
        }

        if ($event->target_type === ActivityEvent::TARGET_STORY && ! $story) {
            return null;
        }

        if ($event->target_type === ActivityEvent::TARGET_USER && ! $targetUser) {
            return null;
        }

        $iso = $event->created_at?->toIso8601String();

        return [
            'id' => 'event:' . $event->id,
            'type' => (string) $event->type,
            'user' => $this->buildUserPayload($event->actor),
            'artwork' => $this->buildArtworkPayload($artwork),
            'story' => $this->buildStoryPayload($story),
            'target_user' => $this->buildUserPayload($targetUser),
            'meta' => is_array($event->meta) ? $event->meta : [],
            'created_at' => $iso,
            'time_ago' => $event->created_at?->diffForHumans(),
            'sort_timestamp' => $iso,
        ];
    }

    private function fetchMentionActivities(int $limit): Collection
    {
        if (! Schema::hasTable('user_mentions')) {
            return collect();
        }

        return UserMention::query()
            ->select(['id', 'user_id', 'mentioned_user_id', 'artwork_id', 'comment_id', 'created_at'])
            ->with([
                'actor' => function ($query) {
                    $query
                        ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                        ->with('profile:user_id,avatar_hash')
                        ->withCount('artworks');
                },
                'mentionedUser' => function ($query) {
                    $query
                        ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                        ->with('profile:user_id,avatar_hash')
                        ->withCount('artworks');
                },
                'comment' => function ($query) {
                    $query
                        ->select('id', 'artwork_id', 'user_id', 'parent_id', 'content', 'raw_content', 'rendered_content', 'created_at', 'is_approved')
                        ->with([
                            'user' => function ($userQuery) {
                                $userQuery
                                    ->select('id', 'name', 'username', 'role', 'is_active', 'created_at')
                                    ->with('profile:user_id,avatar_hash')
                                    ->withCount('artworks');
                            },
                            'artwork' => function ($artworkQuery) {
                                $artworkQuery->select('id', 'user_id', 'title', 'slug', 'hash', 'thumb_ext', 'published_at', 'deleted_at', 'is_public', 'is_approved');
                            },
                        ]);
                },
            ])
            ->whereHas('actor', fn ($query) => $query->where('is_active', true)->whereNull('deleted_at'))
            ->whereHas('mentionedUser', fn ($query) => $query->where('is_active', true)->whereNull('deleted_at'))
            ->whereHas('comment', function ($query) {
                $query
                    ->where('is_approved', true)
                    ->whereNull('deleted_at')
                    ->whereHas('artwork', fn ($artworkQuery) => $artworkQuery->public()->published()->whereNull('deleted_at'));
            })
            ->latest('created_at')
            ->limit($limit)
            ->get()
            ->map(function (UserMention $mention): array {
                $iso = $mention->created_at?->toIso8601String();

                return [
                    'id' => 'mention:' . $mention->id,
                    'type' => 'mention',
                    'user' => $this->buildUserPayload($mention->actor),
                    'mentioned_user' => $this->buildUserPayload($mention->mentionedUser),
                    'comment' => $mention->comment ? $this->buildCommentPayload($mention->comment) : null,
                    'artwork' => $this->buildArtworkPayload($mention->comment?->artwork),
                    'created_at' => $iso,
                    'time_ago' => $mention->created_at?->diffForHumans(),
                    'sort_timestamp' => $iso,
                ];
            })
            ->values();
    }

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

        $username = (string) ($user->username ?? '');

        return [
            'id' => (int) $user->id,
            'name' => html_entity_decode((string) ($user->name ?? $username ?: 'User'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
            'username' => $username,
            'profile_url' => $username !== '' ? '/@' . $username : null,
            'avatar_url' => $user->profile?->avatar_url ?: AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 64),
            'badge' => $this->resolveBadge($user),
        ];
    }

    private function resolveBadge(User $user): ?array
    {
        if ($user->isAdmin()) {
            return ['label' => 'Admin', 'tone' => 'rose'];
        }

        if ($user->isModerator()) {
            return ['label' => 'Moderator', 'tone' => 'amber'];
        }

        if ((int) ($user->artworks_count ?? 0) > 0) {
            return ['label' => 'Creator', 'tone' => 'sky'];
        }

        return null;
    }

    private function buildArtworkPayload(?Artwork $artwork): ?array
    {
        if (! $artwork) {
            return null;
        }

        $slug = Str::slug((string) ($artwork->slug ?: $artwork->title));
        if ($slug === '') {
            $slug = (string) $artwork->id;
        }

        $thumb = ThumbnailPresenter::present($artwork, 'md');

        return [
            'id' => (int) $artwork->id,
            'title' => html_entity_decode((string) ($artwork->title ?? 'Artwork'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
            'url' => route('art.show', ['id' => (int) $artwork->id, 'slug' => $slug]),
            'thumb' => $thumb['url'] ?? null,
        ];
    }

    private function buildStoryPayload(?Story $story): ?array
    {
        if (! $story) {
            return null;
        }

        return [
            'id' => (int) $story->id,
            'title' => html_entity_decode((string) ($story->title ?? 'Story'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
            'url' => route('stories.show', ['slug' => $story->slug]),
            'cover_url' => $story->cover_url,
        ];
    }

    private function buildCommentPayload(ArtworkComment $comment): array
    {
        $artwork = $this->buildArtworkPayload($comment->artwork);
        $commentUrl = $artwork ? $artwork['url'] . '#comment-' . $comment->id : null;

        return [
            'id' => (int) $comment->id,
            'parent_id' => $comment->parent_id ? (int) $comment->parent_id : null,
            'body' => $this->excerptComment($comment),
            'body_html' => $comment->getDisplayHtml(),
            'url' => $commentUrl,
            'author' => $this->buildUserPayload($comment->user),
        ];
    }

    private function excerptComment(ArtworkComment $comment): string
    {
        $raw = $comment->raw_content ?? $comment->content ?? '';
        $plain = trim(preg_replace('/\s+/', ' ', strip_tags(html_entity_decode((string) $raw, ENT_QUOTES | ENT_HTML5, 'UTF-8'))) ?? '');

        return Str::limit($plain, 180, '…');
    }

    private function loadCommentReactionTotals(array $commentIds, ?int $viewerId): array
    {
        if ($commentIds === []) {
            return [];
        }

        $rows = DB::table('comment_reactions')
            ->whereIn('comment_id', $commentIds)
            ->selectRaw('comment_id, reaction, COUNT(*) as total')
            ->groupBy('comment_id', 'reaction')
            ->get();

        $viewerReactions = [];
        if ($viewerId) {
            $viewerReactions = DB::table('comment_reactions')
                ->whereIn('comment_id', $commentIds)
                ->where('user_id', $viewerId)
                ->get(['comment_id', 'reaction'])
                ->groupBy('comment_id')
                ->map(fn (Collection $items) => $items->pluck('reaction')->all())
                ->all();
        }

        $totalsByComment = [];
        foreach ($commentIds as $commentId) {
            $totalsByComment[(int) $commentId] = $this->defaultReactionTotals();
        }

        foreach ($rows as $row) {
            $commentId = (int) $row->comment_id;
            $slug = (string) $row->reaction;
            if (! isset($totalsByComment[$commentId][$slug])) {
                continue;
            }

            $totalsByComment[$commentId][$slug]['count'] = (int) $row->total;
        }

        foreach ($viewerReactions as $commentId => $slugs) {
            foreach ($slugs as $slug) {
                if (isset($totalsByComment[(int) $commentId][(string) $slug])) {
                    $totalsByComment[(int) $commentId][(string) $slug]['mine'] = true;
                }
            }
        }

        return $totalsByComment;
    }

    private function defaultReactionTotals(): array
    {
        $totals = [];

        foreach (ReactionType::cases() as $type) {
            $totals[$type->value] = [
                'emoji' => $type->emoji(),
                'label' => $type->label(),
                'count' => 0,
                'mine' => false,
            ];
        }

        return $totals;
    }
}
