<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import FilterPanel from '../components/FilterPanel.vue';
import LifeRatingControl from '../components/LifeRatingControl.vue';
import LifeReactionUsersModal from '../components/LifeReactionUsersModal.vue';
import LoadMoreSentinel from '../components/LoadMoreSentinel.vue';
import Modal from '../components/Modal.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import StatusBadge from '../components/StatusBadge.vue';
import StatusMessage from '../components/StatusMessage.vue';
import Tabs, { type TabOption } from '../components/Tabs.vue';
import TagsSelect from '../components/TagsSelect.vue';
import {
  iconAdd,
  iconCancel,
  iconChevronDown,
  iconComment,
  iconDelete,
  iconEdit,
  iconExternal,
  iconLife,
  iconReactionFun,
  iconReactionHelpful,
  iconReactionLike,
  iconReactionThanks,
  iconReply,
  iconSave,
  iconSearch,
  iconUndo,
  iconVersion,
  iconWarning
} from '../icons';
import {
  api,
  moderationUpdateEvent,
  onAuthChange,
  type AiModerationStatus,
  type AuthUser,
  type CommentSort,
  type GameVersion,
  type Language,
  type LifeCategory,
  type LifeComment,
  type LifePost,
  type LifePostsPage,
  type LifeReactionType,
  type ModerationUpdateDetail
} from '../services/api';

type LifeCommentPageState = {
  items: LifeComment[];
  nextCursor: string | null;
  hasMore: boolean;
  total: number;
  loading: boolean;
  loadingMore: boolean;
  loaded: boolean;
  error: string;
};

type LifePostSort = 'latest' | 'oldest' | 'top-rated';
type LifeFeedScope = 'all' | 'following';

const { locale, t } = useI18n();
const posts = ref<LifePost[]>([]);
const lifeCategories = ref<LifeCategory[]>([]);
const gameVersions = ref<GameVersion[]>([]);
const languages = ref<Language[]>([]);
const currentUser = ref<AuthUser | null>(null);
const loading = ref(true);
const loadingMore = ref(false);
const authReady = ref(false);
const busy = ref(false);
const searchDraft = ref('');
const submittedSearch = ref('');
const activeCategoryId = ref('all');
const activeLanguageCode = ref('all');
const activeGameVersionId = ref('all');
const activeRateableFilter = ref('all');
const activeSort = ref<LifePostSort>('latest');
const activeFeedScope = ref<LifeFeedScope>('all');
const body = ref('');
const selectedCategoryId = ref('');
const selectedGameVersionId = ref('');
const editingPostId = ref<number | null>(null);
const postModalOpen = ref(false);
const formError = ref('');
const loadError = ref('');
const commentBodies = ref<Record<number, string>>({});
const replyBodies = ref<Record<number, string>>({});
const replyTargetId = ref<number | null>(null);
const expandedComments = ref<Record<number, boolean>>({});
const commentPages = ref<Record<number, LifeCommentPageState>>({});
const commentSorts = ref<Record<number, CommentSort>>({});
const commentBusyKey = ref('');
const commentErrors = ref<Record<string, string>>({});
const reactionPickerPostId = ref<number | null>(null);
const reactionBusyPostId = ref<number | null>(null);
const reactionErrors = ref<Record<number, string>>({});
const ratingBusyPostId = ref<number | null>(null);
const ratingErrors = ref<Record<number, string>>({});
const moderationBusyPostId = ref<number | null>(null);
const moderationErrors = ref<Record<number, string>>({});
const reactionUsersModal = ref<{ postId: number; reactionType: LifeReactionType | null } | null>(null);
const bodyInput = ref<HTMLTextAreaElement | null>(null);
const loadMoreSentinel = ref<HTMLElement | null>(null);
const lifePostPageSize = 20;
const lifeCommentPageSize = 20;
const bodyMaxLength = 2000;
const commentMaxLength = 1000;
const skeletonPostCount = 3;
const loadingMoreSkeletonCount = 2;
let removeAuthListener: (() => void) | null = null;
let feedObserver: IntersectionObserver | null = null;
let postsRequestId = 0;
const nextCursor = ref<string | null>(null);
const hasMorePosts = ref(false);
const loadMorePaused = ref(false);
const allCategoryValue = 'all';
const allLanguageValue = 'all';
const allGameVersionValue = 'all';

type LifeInitialData = {
  options: { lifeCategories: LifeCategory[]; gameVersions: GameVersion[] } | null;
  languages: Language[] | null;
  posts: LifePostsPage | null;
};

const { data: initialData } = await useAsyncData<LifeInitialData>(
  `life-feed-initial:${locale.value}`,
  async () => {
    const [optionsResult, languagesResult, postsResult] = await Promise.allSettled([
      api.options(),
      api.languages(),
      api.lifePosts({
        limit: lifePostPageSize,
        sort: 'latest'
      })
    ]);

    return {
      options:
        optionsResult.status === 'fulfilled'
          ? { lifeCategories: optionsResult.value.lifeCategories, gameVersions: optionsResult.value.gameVersions }
          : null,
      languages: languagesResult.status === 'fulfilled' ? languagesResult.value.filter((language) => language.enabled) : null,
      posts: postsResult.status === 'fulfilled' ? postsResult.value : null
    };
  },
  { default: () => ({ options: null, languages: null, posts: null }) }
);

lifeCategories.value = initialData.value.options?.lifeCategories ?? [];
gameVersions.value = initialData.value.options?.gameVersions ?? [];
languages.value = initialData.value.languages ?? [];
posts.value = initialData.value.posts?.items ?? [];
nextCursor.value = initialData.value.posts?.nextCursor ?? null;
hasMorePosts.value = initialData.value.posts?.hasMore ?? false;
const initialOptionsLoaded = ref(initialData.value.options !== null);
const initialLanguagesLoaded = ref(initialData.value.languages !== null);
const initialPostsLoaded = ref(initialData.value.posts !== null);
loading.value = !initialPostsLoaded.value;

const reactionOptions = [
  { type: 'like', icon: iconReactionLike, labelKey: 'pages.life.reactionLike' },
  { type: 'helpful', icon: iconReactionHelpful, labelKey: 'pages.life.reactionHelpful' },
  { type: 'fun', icon: iconReactionFun, labelKey: 'pages.life.reactionFun' },
  { type: 'thanks', icon: iconReactionThanks, labelKey: 'pages.life.reactionThanks' }
] as const satisfies ReadonlyArray<{ type: LifeReactionType; icon: string; labelKey: string }>;

function can(permissionKey: string) {
  return currentUser.value?.permissions.includes(permissionKey) === true;
}

const canPost = computed(() => can('life.posts.create'));
const canComment = computed(() => can('life.comments.create'));
const canLikeComments = computed(() => can('life.comments.like'));
const canReact = computed(() => can('life.reactions.set'));
const canRate = computed(() => can('life.ratings.set'));
const charactersLeft = computed(() => Math.max(0, bodyMaxLength - body.value.length));
const isEditing = computed(() => editingPostId.value !== null);
const searchQuery = computed(() => submittedSearch.value.trim());
const selectedFeedCategoryId = computed(() => {
  const categoryId = Number(activeCategoryId.value);
  return activeCategoryId.value === allCategoryValue || !Number.isInteger(categoryId) || categoryId <= 0 ? undefined : categoryId;
});
const selectedFeedLanguageCode = computed(() =>
  activeLanguageCode.value === allLanguageValue ? undefined : activeLanguageCode.value
);
const selectedFeedGameVersionId = computed(() => {
  const gameVersionId = Number(activeGameVersionId.value);
  return activeGameVersionId.value === allGameVersionValue || !Number.isInteger(gameVersionId) || gameVersionId <= 0
    ? undefined
    : gameVersionId;
});
const selectedRateableFilter = computed(() => {
  if (activeRateableFilter.value === 'rateable') {
    return true;
  }
  if (activeRateableFilter.value === 'not-rateable') {
    return false;
  }
  return null;
});
const categoryFilterOptions = computed<TabOption[]>(() => [
  { value: allCategoryValue, label: t('pages.life.allCategories') },
  ...lifeCategories.value.map((category) => ({ value: String(category.id), label: category.name }))
]);
const languageFilterOptions = computed<TabOption[]>(() => [
  { value: allLanguageValue, label: t('pages.life.allLanguages') },
  ...languages.value.map((language) => ({ value: language.code, label: language.name }))
]);
const gameVersionFilterOptions = computed(() => [
  { value: allGameVersionValue, label: t('pages.life.allVersions') },
  ...gameVersions.value.map((version) => ({ value: String(version.id), label: version.name }))
]);
const rateableFilterOptions = computed(() => [
  { value: 'all', label: t('pages.life.allRatingModes') },
  { value: 'rateable', label: t('pages.life.rateableOnly') },
  { value: 'not-rateable', label: t('pages.life.notRateableOnly') }
]);
const sortOptions = computed<Array<{ value: LifePostSort; label: string }>>(() => [
  { value: 'latest', label: t('pages.life.sortLatest') },
  { value: 'oldest', label: t('pages.life.sortOldest') },
  { value: 'top-rated', label: t('pages.life.sortTopRated') }
]);
const commentSortOptions = computed<Array<{ value: CommentSort; label: string }>>(() => [
  { value: 'oldest', label: t('pages.life.sortOldest') },
  { value: 'latest', label: t('pages.life.sortLatest') },
  { value: 'most-liked', label: t('pages.life.sortMostLiked') },
  { value: 'most-replied', label: t('pages.life.sortMostReplied') }
]);
const feedScopeOptions = computed<TabOption[]>(() => [
  { value: 'all', label: t('pages.life.allFeed') },
  { value: 'following', label: t('pages.life.followingFeed') }
]);
const defaultLifeCategoryId = computed(() => {
  const category = lifeCategories.value.find((item) => item.isDefault);
  return category ? String(category.id) : '';
});
const postModalTitle = computed(() => (isEditing.value ? t('pages.life.editPost') : t('pages.life.newPost')));
const submitLabel = computed(() => {
  if (busy.value) return isEditing.value ? t('pages.life.updating') : t('pages.life.publishing');
  return isEditing.value ? t('pages.life.update') : t('pages.life.publish');
});

async function loadCurrentUser() {
  authReady.value = false;

  try {
    const response = await api.me();
    currentUser.value = response.user;
  } catch {
    currentUser.value = null;
    activeFeedScope.value = 'all';
  } finally {
    authReady.value = true;
  }
}

async function loadLifeCategories() {
  try {
    const options = await api.options();
    lifeCategories.value = options.lifeCategories;
    gameVersions.value = options.gameVersions;

    if (activeCategoryId.value !== allCategoryValue && !lifeCategories.value.some((category) => String(category.id) === activeCategoryId.value)) {
      activeCategoryId.value = allCategoryValue;
    }
    if (
      activeGameVersionId.value !== allGameVersionValue &&
      !gameVersions.value.some((gameVersion) => String(gameVersion.id) === activeGameVersionId.value)
    ) {
      activeGameVersionId.value = allGameVersionValue;
    }
    if (!isEditing.value && postModalOpen.value && !selectedCategoryId.value) {
      selectedCategoryId.value = defaultLifeCategoryId.value;
    }
    if (!isEditing.value && selectedCategoryId.value && !lifeCategories.value.some((category) => String(category.id) === selectedCategoryId.value)) {
      selectedCategoryId.value = defaultLifeCategoryId.value;
    }
    if (selectedGameVersionId.value && !gameVersions.value.some((gameVersion) => String(gameVersion.id) === selectedGameVersionId.value)) {
      selectedGameVersionId.value = '';
    }
  } catch (error) {
    loadError.value = error instanceof Error && error.message ? error.message : t('errors.loadFailed');
  }
}

async function loadLanguages() {
  try {
    languages.value = (await api.languages()).filter((language) => language.enabled);

    if (
      activeLanguageCode.value !== allLanguageValue &&
      !languages.value.some((language) => language.code === activeLanguageCode.value)
    ) {
      activeLanguageCode.value = allLanguageValue;
    }
  } catch (error) {
    loadError.value = error instanceof Error && error.message ? error.message : t('errors.loadFailed');
  }
}

async function loadPosts() {
  const requestId = ++postsRequestId;
  loading.value = true;
  loadingMore.value = false;
  loadError.value = '';
  nextCursor.value = null;
  hasMorePosts.value = false;
  loadMorePaused.value = false;

  try {
    const params = {
      limit: lifePostPageSize,
      search: searchQuery.value,
      categoryId: selectedFeedCategoryId.value,
      language: selectedFeedLanguageCode.value,
      gameVersionId: selectedFeedGameVersionId.value,
      rateable: selectedRateableFilter.value,
      sort: activeSort.value
    };
    const page = activeFeedScope.value === 'following' ? await api.followingLifePosts(params) : await api.lifePosts(params);
    if (requestId !== postsRequestId) {
      return;
    }
    posts.value = page.items;
    expandedComments.value = {};
    commentPages.value = {};
    nextCursor.value = page.nextCursor;
    hasMorePosts.value = page.hasMore;
  } catch (error) {
    if (requestId === postsRequestId) {
      loadError.value = error instanceof Error && error.message ? error.message : t('errors.loadFailed');
    }
  } finally {
    if (requestId === postsRequestId) {
      loading.value = false;
    }
  }
}

async function loadMorePosts() {
  if (loading.value || loadingMore.value || loadMorePaused.value || !hasMorePosts.value) {
    return;
  }

  const cursor = nextCursor.value;
  if (!cursor) {
    hasMorePosts.value = false;
    return;
  }

  const requestId = postsRequestId;
  loadingMore.value = true;
  loadError.value = '';

  try {
    const params = {
      cursor,
      limit: lifePostPageSize,
      search: searchQuery.value,
      categoryId: selectedFeedCategoryId.value,
      language: selectedFeedLanguageCode.value,
      gameVersionId: selectedFeedGameVersionId.value,
      rateable: selectedRateableFilter.value,
      sort: activeSort.value
    };
    const page = activeFeedScope.value === 'following' ? await api.followingLifePosts(params) : await api.lifePosts(params);
    if (requestId !== postsRequestId) {
      return;
    }

    const existingIds = new Set(posts.value.map((post) => post.id));
    posts.value = [...posts.value, ...page.items.filter((post) => !existingIds.has(post.id))];
    nextCursor.value = page.nextCursor;
    hasMorePosts.value = page.hasMore;
  } catch (error) {
    if (requestId === postsRequestId) {
      loadError.value = error instanceof Error && error.message ? error.message : t('errors.loadFailed');
      loadMorePaused.value = true;
    }
  } finally {
    if (requestId === postsRequestId) {
      loadingMore.value = false;
    }
  }
}

function resetForm() {
  body.value = '';
  selectedCategoryId.value = '';
  selectedGameVersionId.value = '';
  editingPostId.value = null;
  formError.value = '';
}

function payload() {
  return {
    body: body.value.trim(),
    categoryId: selectedLifeCategoryId() ?? 0,
    gameVersionId: selectedGameVersionForPost(),
    languageCode: selectedFeedLanguageCode.value ?? null
  };
}

function selectedLifeCategoryId() {
  const categoryId = Number(selectedCategoryId.value);
  return Number.isInteger(categoryId) && categoryId > 0 ? categoryId : null;
}

function selectedGameVersionForPost() {
  const gameVersionId = Number(selectedGameVersionId.value);
  return Number.isInteger(gameVersionId) && gameVersionId > 0 ? gameVersionId : null;
}

function submitSearch() {
  const nextSearch = searchDraft.value.trim();
  if (nextSearch === submittedSearch.value && !loadError.value) {
    return;
  }

  submittedSearch.value = nextSearch;
  void loadPosts();
}

function clearSearch() {
  const hadSubmittedSearch = Boolean(submittedSearch.value);
  if (!searchDraft.value && !hadSubmittedSearch) {
    return;
  }

  searchDraft.value = '';
  submittedSearch.value = '';

  if (hadSubmittedSearch) {
    void loadPosts();
  }
}

function retryLoadMore() {
  loadMorePaused.value = false;
  void loadMorePosts();
}

function matchesCurrentFilters(post: LifePost) {
  const keyword = searchQuery.value.toLowerCase();
  const categoryId = selectedFeedCategoryId.value;
  const gameVersionId = selectedFeedGameVersionId.value;
  const rateable = selectedRateableFilter.value;
  const matchesSearch = keyword === '' || post.body.toLowerCase().includes(keyword);
  const matchesCategory = categoryId === undefined || post.category?.id === categoryId;
  const matchesGameVersion = gameVersionId === undefined || post.gameVersion?.id === gameVersionId;
  const matchesRateable = rateable === null || post.category?.isRateable === rateable;
  const matchesLanguage =
    selectedFeedLanguageCode.value === undefined || post.moderationLanguageCode === selectedFeedLanguageCode.value;
  return matchesSearch && matchesCategory && matchesGameVersion && matchesRateable && matchesLanguage;
}

function openCreatePostModal() {
  resetForm();
  selectedCategoryId.value = defaultLifeCategoryId.value;
  postModalOpen.value = true;
  void nextTick(() => bodyInput.value?.focus());
}

function closePostModal() {
  if (busy.value) {
    return;
  }

  postModalOpen.value = false;
  resetForm();
}

async function submitPost() {
  if (!body.value.trim()) {
    formError.value = t('pages.life.bodyRequired');
    bodyInput.value?.focus();
    return;
  }

  if (selectedLifeCategoryId() === null) {
    formError.value = t('pages.life.categoryRequired');
    document.getElementById('life-post-category')?.focus();
    return;
  }

  busy.value = true;
  formError.value = '';

  try {
    if (editingPostId.value !== null) {
      const updated = await api.updateLifePost(editingPostId.value, payload());
      replacePost(updated);
    } else {
      const created = await api.createLifePost(payload());
      if (activeSort.value !== 'latest' || activeFeedScope.value === 'following') {
        void loadPosts();
      } else if (matchesCurrentFilters(created)) {
        posts.value = [created, ...posts.value];
      }
    }
    resetForm();
    postModalOpen.value = false;
  } catch (error) {
    formError.value =
      error instanceof Error && error.message
        ? error.message
        : isEditing.value
          ? t('pages.life.saveFailed')
          : t('pages.life.postFailed');
  } finally {
    busy.value = false;
  }
}

function canManage(post: LifePost) {
  return (currentUser.value?.id === post.author?.id && can('life.posts.update')) || can('life.posts.update-any');
}

function canDeletePost(post: LifePost) {
  return (currentUser.value?.id === post.author?.id && can('life.posts.delete')) || can('life.posts.delete-any');
}

function canManageComment(comment: LifeComment) {
  return !comment.deleted && ((currentUser.value?.id === comment.author?.id && can('life.comments.delete')) || can('life.comments.delete-any'));
}

function canRestoreComment(comment: LifeComment) {
  return comment.deleted && currentUser.value?.id === comment.author?.id && can('life.comments.delete');
}

function canSeeCommentModeration(comment: LifeComment) {
  return moderationStatusVisible(comment.moderationStatus) && (currentUser.value?.id === comment.author?.id || can('life.comments.delete-any'));
}

function commentKey(postId: number) {
  return `post-${postId}`;
}

function replyKey(commentId: number) {
  return `reply-${commentId}`;
}

function likeKey(commentId: number) {
  return `like-${commentId}`;
}

function commentSort(postId: number): CommentSort {
  return commentSorts.value[postId] ?? 'oldest';
}

function setCommentSort(post: LifePost, sort: CommentSort) {
  commentSorts.value = {
    ...commentSorts.value,
    [post.id]: sort
  };
  void loadComments(post, true);
}

function handleCommentSortChange(post: LifePost, event: Event) {
  if (event.target instanceof HTMLSelectElement) {
    setCommentSort(post, event.target.value as CommentSort);
  }
}

function initialCommentPage(post: LifePost): LifeCommentPageState {
  return {
    items: post.commentPreview,
    nextCursor: null,
    hasMore: post.commentCount > post.commentPreview.reduce((count, comment) => count + 1 + comment.replies.length, 0),
    total: post.commentCount,
    loading: false,
    loadingMore: false,
    loaded: false,
    error: ''
  };
}

function commentPage(post: LifePost) {
  return commentPages.value[post.id] ?? initialCommentPage(post);
}

function setCommentPage(postId: number, page: LifeCommentPageState) {
  commentPages.value = {
    ...commentPages.value,
    [postId]: page
  };
}

function commentsForPost(post: LifePost) {
  return commentPage(post).items;
}

function commentCount(post: LifePost) {
  return commentPage(post).total;
}

function reactionTotal(post: LifePost) {
  return reactionOptions.reduce((count, option) => count + (post.reactionCounts[option.type] ?? 0), 0);
}

function reactionLabel(type: LifeReactionType) {
  return t(reactionOptions.find((option) => option.type === type)?.labelKey ?? 'pages.life.react');
}

function reactionIcon(type: LifeReactionType | null) {
  return reactionOptions.find((option) => option.type === type)?.icon ?? iconReactionLike;
}

function reactionButtonLabel(post: LifePost) {
  return post.myReaction ? reactionLabel(post.myReaction) : t('pages.life.reactionLike');
}

function reactionOptionLabel(post: LifePost, type: LifeReactionType) {
  return post.myReaction === type ? t('pages.life.removeReaction') : reactionLabel(type);
}

function reactionCountLabel(post: LifePost, type: LifeReactionType) {
  return t('pages.life.reactionCountLabel', {
    reaction: reactionLabel(type),
    count: post.reactionCounts[type] ?? 0
  });
}

function openReactionUsersModal(postId: number, reactionType: LifeReactionType | null = null) {
  reactionUsersModal.value = { postId, reactionType };
}

function closeReactionUsersModal() {
  reactionUsersModal.value = null;
}

function moderationLabel(status: AiModerationStatus) {
  const labels: Record<AiModerationStatus, string> = {
    unreviewed: t('pages.life.moderationUnreviewed'),
    reviewing: t('pages.life.moderationReviewing'),
    approved: t('pages.life.moderationApproved'),
    rejected: t('pages.life.moderationRejected'),
    failed: t('pages.life.moderationFailed')
  };
  return labels[status];
}

function moderationTone(status: AiModerationStatus) {
  const tones: Record<AiModerationStatus, 'info' | 'success' | 'warning' | 'danger' | 'neutral'> = {
    unreviewed: 'neutral',
    reviewing: 'info',
    approved: 'success',
    rejected: 'danger',
    failed: 'warning'
  };
  return tones[status];
}

function moderationStatusVisible(status: AiModerationStatus) {
  return status !== 'approved';
}

function canRetryModeration(post: LifePost) {
  return post.moderationStatus !== 'approved' && post.moderationStatus !== 'reviewing' && canManage(post);
}

function moderationReasonVisible(status: AiModerationStatus, reason: string | null) {
  return (status === 'rejected' || status === 'failed') && reason !== null && reason.trim() !== '';
}

function updateLifeCommentModeration(
  items: LifeComment[],
  commentId: number,
  status: AiModerationStatus,
  languageCode: string | null,
  reason: string | null
): boolean {
  for (const comment of items) {
    if (comment.id === commentId) {
      comment.moderationStatus = status;
      comment.moderationLanguageCode = languageCode;
      comment.moderationReason = reason;
      return true;
    }

    if (updateLifeCommentModeration(comment.replies, commentId, status, languageCode, reason)) {
      return true;
    }
  }

  return false;
}

function isModerationUpdateEvent(event: Event): event is CustomEvent<ModerationUpdateDetail> {
  return event instanceof CustomEvent && event.detail?.type === 'moderation.updated';
}

function handleModerationUpdate(event: Event) {
  if (!isModerationUpdateEvent(event)) {
    return;
  }

  const { target, moderationStatus, moderationLanguageCode, moderationReason } = event.detail;
  if (target.type === 'life-post' && target.lifePostId !== null) {
    const currentPost = posts.value.find((post) => post.id === target.lifePostId);
    if (!currentPost) {
      return;
    }

    const updatedPost = {
      ...currentPost,
      moderationStatus,
      moderationLanguageCode,
      moderationReason
    };
    if (!matchesCurrentFilters(updatedPost)) {
      posts.value = posts.value.filter((post) => post.id !== updatedPost.id);
      return;
    }
    posts.value = posts.value.map((post) => (post.id === updatedPost.id ? updatedPost : post));
    return;
  }

  if (target.type !== 'life-comment' || target.lifePostId === null || target.lifeCommentId === null) {
    return;
  }

  const currentPost = posts.value.find((post) => post.id === target.lifePostId);
  if (!currentPost) {
    return;
  }

  const page = commentPage(currentPost);
  const updated = updateLifeCommentModeration(
    page.items,
    target.lifeCommentId,
    moderationStatus,
    moderationLanguageCode,
    moderationReason
  );
  if (updated) {
    setCommentPage(currentPost.id, { ...page, items: [...page.items] });
  } else if (moderationStatus === 'approved' && areCommentsExpanded(currentPost.id)) {
    void loadComments(currentPost, true);
  }
}

async function retryPostModeration(post: LifePost) {
  moderationBusyPostId.value = post.id;
  const nextErrors = { ...moderationErrors.value };
  delete nextErrors[post.id];
  moderationErrors.value = nextErrors;

  try {
    replacePost(await api.retryLifePostModeration(post.id));
  } catch (error) {
    moderationErrors.value = {
      ...moderationErrors.value,
      [post.id]: error instanceof Error && error.message ? error.message : t('pages.life.moderationRetryFailed')
    };
  } finally {
    moderationBusyPostId.value = null;
  }
}

function replacePost(updatedPost: LifePost) {
  if (!matchesCurrentFilters(updatedPost)) {
    posts.value = posts.value.filter((post) => post.id !== updatedPost.id);
    return;
  }

  const existingComments = commentPages.value[updatedPost.id];
  if (existingComments) {
    setCommentPage(updatedPost.id, {
      ...existingComments,
      total: updatedPost.commentCount
    });
  }
  posts.value = posts.value.map((post) => (post.id === updatedPost.id ? updatedPost : post));
}

function areCommentsExpanded(postId: number) {
  return expandedComments.value[postId] === true;
}

function setCommentsExpanded(postId: number, expanded: boolean) {
  expandedComments.value = {
    ...expandedComments.value,
    [postId]: expanded
  };
}

function mergeComments(existing: LifeComment[], incoming: LifeComment[]) {
  const ids = new Set(existing.map((comment) => comment.id));
  return [...existing, ...incoming.filter((comment) => !ids.has(comment.id))];
}

function replaceCommentInTree(comments: LifeComment[], updated: LifeComment): boolean {
  for (let index = 0; index < comments.length; index += 1) {
    const comment = comments[index];
    if (!comment) {
      continue;
    }
    if (comment.id === updated.id) {
      comments[index] = { ...updated, replies: comment.replies };
      return true;
    }

    if (replaceCommentInTree(comment.replies, updated)) {
      return true;
    }
  }

  return false;
}

async function loadComments(post: LifePost, reset = false) {
  const existing = commentPage(post);
  if (existing.loading || existing.loadingMore || (!reset && existing.loaded && !existing.hasMore)) {
    return;
  }

  const cursor = reset || !existing.loaded ? null : existing.nextCursor;
  setCommentPage(post.id, {
    ...existing,
    items: reset || !existing.loaded ? [] : existing.items,
    loading: reset || !existing.loaded,
    loadingMore: !reset && existing.loaded,
    error: ''
  });

  try {
    const page = await api.lifeComments(post.id, {
      limit: lifeCommentPageSize,
      cursor,
      language: selectedFeedLanguageCode.value,
      sort: commentSort(post.id)
    });
    const nextItems = reset || !existing.loaded ? page.items : mergeComments(existing.items, page.items);
    setCommentPage(post.id, {
      items: nextItems,
      nextCursor: page.nextCursor,
      hasMore: page.hasMore,
      total: page.total,
      loading: false,
      loadingMore: false,
      loaded: true,
      error: ''
    });
    post.commentCount = page.total;
  } catch (error) {
    setCommentPage(post.id, {
      ...existing,
      loading: false,
      loadingMore: false,
      error: error instanceof Error && error.message ? error.message : t('errors.loadFailed')
    });
  }
}

function toggleComments(post: LifePost) {
  const expanded = !areCommentsExpanded(post.id);
  setCommentsExpanded(post.id, expanded);
  if (expanded) {
    void loadComments(post);
  }
}

function isCommentBusy(key: string) {
  return commentBusyKey.value === key;
}

function isReactionBusy(postId: number) {
  return reactionBusyPostId.value === postId;
}

function isRatingBusy(postId: number) {
  return ratingBusyPostId.value === postId;
}

function commentAuthorName(comment: LifeComment) {
  return comment.author?.displayName ?? t('pages.life.byUnknown');
}

function commentInitial(comment: LifeComment) {
  const name = commentAuthorName(comment);
  return name.slice(0, 1).toUpperCase();
}

function setCommentError(key: string, message: string) {
  commentErrors.value = { ...commentErrors.value, [key]: message };
}

function clearCommentError(key: string) {
  const nextErrors = { ...commentErrors.value };
  delete nextErrors[key];
  commentErrors.value = nextErrors;
}

function updateCommentPage(post: LifePost, updater: (page: LifeCommentPageState) => LifeCommentPageState) {
  setCommentPage(post.id, updater(commentPage(post)));
}

function setReactionError(postId: number, message: string) {
  reactionErrors.value = { ...reactionErrors.value, [postId]: message };
}

function clearReactionError(postId: number) {
  const nextErrors = { ...reactionErrors.value };
  delete nextErrors[postId];
  reactionErrors.value = nextErrors;
}

function setRatingError(postId: number, message: string) {
  ratingErrors.value = { ...ratingErrors.value, [postId]: message };
}

function clearRatingError(postId: number) {
  const nextErrors = { ...ratingErrors.value };
  delete nextErrors[postId];
  ratingErrors.value = nextErrors;
}

function canUseReactions() {
  return canReact.value && reactionBusyPostId.value === null;
}

function canLikeComment(comment: LifeComment) {
  return canLikeComments.value && !comment.deleted && comment.moderationStatus === 'approved';
}

function canRetryCommentModeration(comment: LifeComment) {
  return (
    !comment.deleted &&
    comment.moderationStatus !== 'approved' &&
    comment.moderationStatus !== 'reviewing' &&
    (currentUser.value?.id === comment.author?.id || can('life.comments.delete-any'))
  );
}

function commentLikeLabel(comment: LifeComment) {
  return comment.myLiked ? t('pages.life.unlikeComment') : t('pages.life.likeComment');
}

function canUseRatings(post: LifePost) {
  return canRate.value && ratingBusyPostId.value === null && post.moderationStatus === 'approved' && post.category?.isRateable === true;
}

function closeReactionPicker() {
  reactionPickerPostId.value = null;
}

function toggleReactionPicker(postId: number) {
  if (!canUseReactions()) {
    return;
  }

  clearReactionError(postId);
  reactionPickerPostId.value = reactionPickerPostId.value === postId ? null : postId;
}

function closeReactionPickerFromDocument(event: MouseEvent) {
  if (reactionPickerPostId.value === null || !(event.target instanceof Element)) {
    return;
  }

  if (!event.target.closest('.life-reactions')) {
    closeReactionPicker();
  }
}

function closeReactionPickerFromKeyboard(event: KeyboardEvent) {
  if (event.key === 'Escape' && reactionPickerPostId.value !== null) {
    closeReactionPicker();
  }
}

function handleReactionContextMenu(event: MouseEvent, postId: number) {
  event.preventDefault();
  toggleReactionPicker(postId);
}

function handleReactionKeydown(event: KeyboardEvent, postId: number) {
  if (event.key !== 'ContextMenu' && !(event.shiftKey && event.key === 'F10')) {
    return;
  }

  event.preventDefault();
  toggleReactionPicker(postId);
}

async function toggleDefaultReaction(post: LifePost) {
  await toggleReaction(post, 'like');
}

async function toggleReaction(post: LifePost, reactionType: LifeReactionType) {
  if (!canUseReactions() || post.moderationStatus !== 'approved') {
    return;
  }

  reactionBusyPostId.value = post.id;
  clearReactionError(post.id);

  try {
    const updatedPost =
      post.myReaction === reactionType
        ? await api.deleteLifeReaction(post.id)
        : await api.setLifeReaction(post.id, reactionType);
    replacePost(updatedPost);
    reactionPickerPostId.value = null;
  } catch (error) {
    setReactionError(post.id, error instanceof Error && error.message ? error.message : t('pages.life.reactionFailed'));
  } finally {
    reactionBusyPostId.value = null;
  }
}

async function toggleRating(post: LifePost, rating: number) {
  if (!canUseRatings(post)) {
    return;
  }

  ratingBusyPostId.value = post.id;
  clearRatingError(post.id);

  try {
    const updatedPost = post.myRating === rating ? await api.deleteLifeRating(post.id) : await api.setLifeRating(post.id, rating);
    replacePost(updatedPost);
    if (activeSort.value === 'top-rated') {
      void loadPosts();
    }
  } catch (error) {
    setRatingError(post.id, error instanceof Error && error.message ? error.message : t('pages.life.ratingFailed'));
  } finally {
    ratingBusyPostId.value = null;
  }
}

function startEdit(post: LifePost) {
  editingPostId.value = post.id;
  body.value = post.body;
  selectedCategoryId.value = post.category ? String(post.category.id) : '';
  selectedGameVersionId.value = post.gameVersion ? String(post.gameVersion.id) : '';
  formError.value = '';
  postModalOpen.value = true;
  void nextTick(() => bodyInput.value?.focus());
}

async function deletePost(post: LifePost) {
  if (!window.confirm(t('pages.life.deleteConfirm'))) {
    return;
  }

  loadError.value = '';

  try {
    await api.deleteLifePost(post.id);
    posts.value = posts.value.filter((item) => item.id !== post.id);
    if (editingPostId.value === post.id) {
      resetForm();
      postModalOpen.value = false;
    }
  } catch (error) {
    loadError.value = error instanceof Error && error.message ? error.message : t('pages.life.deleteFailed');
  }
}

function startReply(comment: LifeComment) {
  replyTargetId.value = comment.id;
  clearCommentError(replyKey(comment.id));
}

function cancelReply(commentId: number) {
  replyTargetId.value = null;
  replyBodies.value[commentId] = '';
  clearCommentError(replyKey(commentId));
}

async function submitComment(post: LifePost) {
  const key = commentKey(post.id);
  const nextBody = (commentBodies.value[post.id] ?? '').trim();
  if (!nextBody) {
    setCommentError(key, t('pages.life.commentRequired'));
    return;
  }

  commentBusyKey.value = key;
  clearCommentError(key);

  try {
    const comment = await api.createLifeComment(post.id, {
      body: nextBody,
      languageCode: selectedFeedLanguageCode.value ?? post.moderationLanguageCode
    });
    const nextTotal = commentCount(post) + 1;
    post.commentCount = nextTotal;
    updateCommentPage(post, (page) => ({
      ...page,
      items: mergeComments(page.items, [comment]),
      total: nextTotal,
      loaded: page.loaded || areCommentsExpanded(post.id)
    }));
    commentBodies.value[post.id] = '';
    setCommentsExpanded(post.id, true);
    if (commentSort(post.id) !== 'oldest') {
      void loadComments(post, true);
    }
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.commentFailed'));
  } finally {
    commentBusyKey.value = '';
  }
}

async function submitReply(post: LifePost, comment: LifeComment) {
  const key = replyKey(comment.id);
  const nextBody = (replyBodies.value[comment.id] ?? '').trim();
  if (!nextBody) {
    setCommentError(key, t('pages.life.commentRequired'));
    return;
  }

  commentBusyKey.value = key;
  clearCommentError(key);

  try {
    const reply = await api.createLifeCommentReply(post.id, comment.id, {
      body: nextBody,
      languageCode: selectedFeedLanguageCode.value ?? comment.moderationLanguageCode ?? post.moderationLanguageCode
    });
    const nextTotal = commentCount(post) + 1;
    post.commentCount = nextTotal;
    comment.replies.push(reply);
    comment.replyCount += 1;
    updateCommentPage(post, (page) => ({
      ...page,
      total: nextTotal
    }));
    setCommentsExpanded(post.id, true);
    cancelReply(comment.id);
    if (commentSort(post.id) === 'most-replied') {
      void loadComments(post, true);
    }
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.replyFailed'));
  } finally {
    commentBusyKey.value = '';
  }
}

function countCommentTree(comment: LifeComment): number {
  return 1 + comment.replies.reduce((total, reply) => total + countCommentTree(reply), 0);
}

function removeCommentFromTree(comments: LifeComment[], id: number): number {
  for (let index = 0; index < comments.length; index += 1) {
    const comment = comments[index];
    if (!comment) {
      continue;
    }
    if (comment.id === id) {
      const removedCount = countCommentTree(comment);
      comments.splice(index, 1);
      return removedCount;
    }

    const removedCount = removeCommentFromTree(comment.replies, id);
    if (removedCount > 0) {
      return removedCount;
    }
  }

  return 0;
}

function markOwnCommentDeleted(comments: LifeComment[], id: number): boolean {
  for (const comment of comments) {
    if (comment.id === id) {
      comment.deleted = true;
      return true;
    }

    if (markOwnCommentDeleted(comment.replies, id)) {
      return true;
    }
  }

  return false;
}

async function deleteComment(post: LifePost, comment: LifeComment) {
  if (!window.confirm(t('pages.life.deleteCommentConfirm'))) {
    return;
  }

  const key = replyKey(comment.id);
  clearCommentError(key);

  try {
    await api.deleteLifeComment(comment.id);
    if (currentUser.value?.id === comment.author?.id) {
      markOwnCommentDeleted(commentsForPost(post), comment.id);
      updateCommentPage(post, (page) => ({
        ...page,
        items: [...page.items]
      }));
    } else {
      const removedCount = removeCommentFromTree(commentsForPost(post), comment.id);
      if (removedCount > 0) {
        const nextTotal = Math.max(0, commentCount(post) - removedCount);
        post.commentCount = nextTotal;
        updateCommentPage(post, (page) => ({
          ...page,
          items: [...page.items],
          total: nextTotal
        }));
      }
    }
    if (replyTargetId.value === comment.id) {
      cancelReply(comment.id);
    }
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.deleteCommentFailed'));
  }
}

async function restoreComment(post: LifePost, comment: LifeComment) {
  const key = replyKey(comment.id);
  commentBusyKey.value = key;
  clearCommentError(key);

  try {
    const restored = await api.restoreLifeComment(comment.id);
    replaceCommentInTree(commentsForPost(post), restored);
    updateCommentPage(post, (page) => ({
      ...page,
      items: [...page.items]
    }));
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.restoreCommentFailed'));
  } finally {
    commentBusyKey.value = '';
  }
}

async function retryCommentModeration(post: LifePost, comment: LifeComment) {
  const key = replyKey(comment.id);
  commentBusyKey.value = key;
  clearCommentError(key);

  try {
    const updated = await api.retryLifeCommentModeration(comment.id);
    replaceCommentInTree(commentsForPost(post), updated);
    updateCommentPage(post, (page) => ({
      ...page,
      items: [...page.items]
    }));
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.moderationRetryFailed'));
  } finally {
    commentBusyKey.value = '';
  }
}

async function toggleCommentLike(post: LifePost, comment: LifeComment) {
  if (!canLikeComment(comment)) {
    return;
  }

  const key = likeKey(comment.id);
  commentBusyKey.value = key;
  clearCommentError(key);

  try {
    const updated = comment.myLiked ? await api.deleteLifeCommentLike(comment.id) : await api.setLifeCommentLike(comment.id);
    replaceCommentInTree(commentsForPost(post), updated);
    updateCommentPage(post, (page) => ({
      ...page,
      items: [...page.items]
    }));
    if (commentSort(post.id) === 'most-liked') {
      void loadComments(post, true);
    }
  } catch (error) {
    setCommentError(key, error instanceof Error && error.message ? error.message : t('pages.life.commentLikeFailed'));
  } finally {
    commentBusyKey.value = '';
  }
}

function formatPostTime(value: string) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return '';
  }

  return new Intl.DateTimeFormat(locale.value, {
    dateStyle: 'medium',
    timeStyle: 'short'
  }).format(date);
}

function authorInitial(post: LifePost) {
  const name = post.author?.displayName.trim() || t('pages.life.byUnknown');
  return name.slice(0, 1).toUpperCase();
}

function disconnectFeedObserver() {
  feedObserver?.disconnect();
  feedObserver = null;
}

function observeLoadMore() {
  disconnectFeedObserver();

  if (loading.value || loadingMore.value || loadMorePaused.value || !hasMorePosts.value || !loadMoreSentinel.value) {
    return;
  }

  if (typeof IntersectionObserver === 'undefined') {
    void loadMorePosts();
    return;
  }

  feedObserver = new IntersectionObserver(
    (entries) => {
      if (entries.some((entry) => entry.isIntersecting)) {
        void loadMorePosts();
      }
    },
    { rootMargin: '360px 0px' }
  );
  feedObserver.observe(loadMoreSentinel.value);
}

watch([loadMoreSentinel, hasMorePosts, loading, loadingMore, loadMorePaused], observeLoadMore, { flush: 'post' });
watch(activeCategoryId, () => {
  void loadPosts();
});
watch(activeLanguageCode, () => {
  expandedComments.value = {};
  commentPages.value = {};
  void loadPosts();
});
watch(activeGameVersionId, () => {
  void loadPosts();
});
watch(activeRateableFilter, () => {
  void loadPosts();
});
watch(activeSort, () => {
  void loadPosts();
});
watch(activeFeedScope, () => {
  void loadPosts();
});
watch(locale, () => {
  void loadLanguages();
  void loadLifeCategories();
  void loadPosts();
});

onMounted(() => {
  document.addEventListener('click', closeReactionPickerFromDocument);
  document.addEventListener('keydown', closeReactionPickerFromKeyboard);
  window.addEventListener(moderationUpdateEvent, handleModerationUpdate);
  void (async () => {
    await loadCurrentUser();
    if (!initialLanguagesLoaded.value) {
      await loadLanguages();
      initialLanguagesLoaded.value = true;
    }
    if (!initialOptionsLoaded.value) {
      await loadLifeCategories();
      initialOptionsLoaded.value = true;
    }
    if (!initialPostsLoaded.value || currentUser.value) {
      await loadPosts();
      initialPostsLoaded.value = true;
    }
  })();
  removeAuthListener = onAuthChange(() => {
    void (async () => {
      await loadCurrentUser();
      await loadPosts();
    })();
  });
});

onUnmounted(() => {
  document.removeEventListener('click', closeReactionPickerFromDocument);
  document.removeEventListener('keydown', closeReactionPickerFromKeyboard);
  window.removeEventListener(moderationUpdateEvent, handleModerationUpdate);
  disconnectFeedObserver();
  removeAuthListener?.();
});
</script>

<template>
  <section class="page-stack life-page">
    <PageHeader :title="t('pages.life.title')" :subtitle="t('pages.life.subtitle')">
      <template #kicker>{{ t('pages.life.kicker') }}</template>
    </PageHeader>

    <FilterPanel class="life-toolbar">
      <form class="life-toolbar__search" role="search" @submit.prevent="submitSearch">
        <div class="field life-toolbar__field">
          <label for="life-search">{{ t('pages.life.search') }}</label>
          <div class="life-search-control">
            <input id="life-search" v-model="searchDraft" type="search" :placeholder="t('pages.life.searchPlaceholder')" />
            <button
              v-if="searchDraft || submittedSearch"
              class="life-search-control__clear"
              type="button"
              :aria-label="t('pages.life.clearSearch')"
              @click="clearSearch"
            >
              <Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
            </button>
          </div>
        </div>
        <button class="ui-button ui-button--ghost" type="submit">
          <Icon :icon="iconSearch" class="ui-icon" aria-hidden="true" />
          {{ t('common.search') }}
        </button>
      </form>

      <div class="life-toolbar__filters">
        <div class="field life-toolbar__select">
          <label for="life-version-filter">{{ t('pages.life.versionFilter') }}</label>
          <select id="life-version-filter" v-model="activeGameVersionId">
            <option v-for="option in gameVersionFilterOptions" :key="option.value" :value="option.value">
              {{ option.label }}
            </option>
          </select>
        </div>
        <div class="field life-toolbar__select">
          <label for="life-rateable-filter">{{ t('pages.life.ratingFilter') }}</label>
          <select id="life-rateable-filter" v-model="activeRateableFilter">
            <option v-for="option in rateableFilterOptions" :key="option.value" :value="option.value">
              {{ option.label }}
            </option>
          </select>
        </div>
        <div class="field life-toolbar__select">
          <label for="life-sort">{{ t('pages.life.sort') }}</label>
          <select id="life-sort" v-model="activeSort">
            <option v-for="option in sortOptions" :key="option.value" :value="option.value">
              {{ option.label }}
            </option>
          </select>
        </div>
      </div>

      <div class="life-toolbar__actions">
        <button class="ui-button ui-button--primary" :disabled="!authReady" type="button" @click="openCreatePostModal">
          <Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
          {{ t('pages.life.newPost') }}
        </button>
      </div>
    </FilterPanel>

    <StatusMessage v-if="loadError" variant="danger" :duration="0">{{ loadError }}</StatusMessage>

    <Modal
      :open="postModalOpen"
      :title="postModalTitle"
      :subtitle="t('pages.life.composerPrompt')"
      :close-label="t('common.close')"
      size="wide"
      @close="closePostModal"
    >
      <div v-if="!authReady" class="life-composer__auth-skeleton" aria-hidden="true">
        <Skeleton variant="box" height="112px" />
        <Skeleton width="42%" />
      </div>

      <form v-else-if="canPost" class="life-form" @submit.prevent="submitPost">
        <div class="field">
          <label for="life-post-body">{{ t('pages.life.bodyLabel') }}</label>
          <textarea
            id="life-post-body"
            ref="bodyInput"
            v-model="body"
            :maxlength="bodyMaxLength"
            :placeholder="t('pages.life.bodyPlaceholder')"
            required
          ></textarea>
          <span class="life-form__counter">{{ t('pages.life.charactersLeft', { count: charactersLeft }) }}</span>
        </div>

        <div class="field">
          <label for="life-post-category">{{ t('pages.life.category') }}</label>
          <TagsSelect
            id="life-post-category"
            v-model="selectedCategoryId"
            :options="lifeCategories"
            :multiple="false"
            :placeholder="t('pages.life.categoryPlaceholder')"
            :search-placeholder="t('pages.life.searchCategories')"
            dropdown-strategy="fixed"
          />
        </div>

        <div class="field">
          <label for="life-post-version">{{ t('pages.life.gameVersion') }}</label>
          <TagsSelect
            id="life-post-version"
            v-model="selectedGameVersionId"
            :options="gameVersions"
            :multiple="false"
            :placeholder="t('pages.life.versionPlaceholder')"
            :search-placeholder="t('pages.life.searchVersions')"
            dropdown-strategy="fixed"
          />
        </div>

        <p v-if="formError" class="life-form__error" role="alert">{{ formError }}</p>

        <div class="life-form__actions">
          <button class="ui-button ui-button--primary" :disabled="busy || !body.trim()" type="submit">
            <Icon :icon="isEditing ? iconSave : iconLife" class="ui-icon" aria-hidden="true" />
            {{ submitLabel }}
          </button>
          <button class="ui-button ui-button--ghost" :disabled="busy" type="button" @click="closePostModal">
            <Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
            {{ t('common.cancel') }}
          </button>
        </div>
      </form>

      <div v-else class="life-auth-note">
        <p>{{ currentUser ? t('pages.life.verifyPrompt') : t('pages.life.loginPrompt') }}</p>
        <RouterLink v-if="!currentUser" class="ui-button ui-button--primary" :to="{ path: '/login', query: { redirect: '/life' } }">
          {{ t('nav.login') }}
        </RouterLink>
      </div>
    </Modal>

    <LifeReactionUsersModal
      v-if="reactionUsersModal"
      :post-id="reactionUsersModal.postId"
      :initial-reaction-type="reactionUsersModal.reactionType"
      @close="closeReactionUsersModal"
    />

    <Tabs
      v-if="currentUser"
      id="life-feed-scope"
      v-model="activeFeedScope"
      :tabs="feedScopeOptions"
      :label="t('pages.life.feedScope')"
    />
    <Tabs id="life-language-filter" v-model="activeLanguageCode" :tabs="languageFilterOptions" :label="t('pages.life.languages')" />
    <Tabs id="life-category-filter" v-model="activeCategoryId" :tabs="categoryFilterOptions" :label="t('pages.life.category')" />

    <section class="life-feed" :aria-busy="loading || loadingMore" :aria-label="t('pages.life.kicker')">
        <div v-if="loading" class="life-feed__list" :aria-label="t('pages.life.loading')">
          <article v-for="index in skeletonPostCount" :key="index" class="life-post life-post--skeleton">
            <div class="life-post__header">
              <Skeleton variant="box" width="46px" height="46px" />
              <div class="life-post__byline">
                <Skeleton width="138px" />
                <Skeleton width="96px" />
              </div>
            </div>
            <Skeleton width="94%" />
            <Skeleton width="76%" />
            <Skeleton width="52%" />
          </article>
        </div>

        <div v-else-if="posts.length" class="life-feed__list">
          <article v-for="post in posts" :key="post.id" class="life-post">
            <header class="life-post__header">
              <div class="life-post__avatar" aria-hidden="true">{{ authorInitial(post) }}</div>
              <div class="life-post__byline">
                <RouterLink v-if="post.author" class="user-profile-link" :to="`/profile/${post.author.id}`">
                  {{ post.author.displayName }}
                </RouterLink>
                <strong v-else>{{ t('pages.life.byUnknown') }}</strong>
                <span>
                  <time :datetime="post.createdAt">{{ formatPostTime(post.createdAt) }}</time>
                  <template v-if="post.updatedAt !== post.createdAt"> - {{ t('pages.life.edited') }}</template>
                </span>
              </div>

              <div class="life-post__actions">
                <RouterLink class="life-icon-button" :to="`/life/${post.id}`" :aria-label="t('pages.life.viewPost')">
                  <Icon :icon="iconExternal" class="ui-icon" aria-hidden="true" />
                  <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.viewPost') }}</span>
                </RouterLink>
                <button v-if="canManage(post)" class="life-icon-button" type="button" :aria-label="t('pages.life.editPost')" @click="startEdit(post)">
                  <Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
                  <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.editPost') }}</span>
                </button>
                <button
                  v-if="canDeletePost(post)"
                  class="life-icon-button life-icon-button--danger"
                  type="button"
                  :aria-label="t('pages.life.deletePost')"
                  @click="deletePost(post)"
                >
                  <Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
                  <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deletePost') }}</span>
                </button>
              </div>
            </header>

            <p class="life-post__body">{{ post.body }}</p>

            <div v-if="post.category || post.gameVersion" class="life-post__tags" :aria-label="t('pages.life.postMeta')">
              <span v-if="post.category" class="life-post__tag">{{ post.category.name }}</span>
              <span v-if="post.gameVersion" class="life-post__tag life-post__tag--version">
                <Icon :icon="iconVersion" class="ui-icon" aria-hidden="true" />
                {{ post.gameVersion.name }}
              </span>
            </div>
            <details v-if="post.gameVersion?.changeLog" class="life-version-note">
              <summary>{{ t('pages.life.changeLog') }}</summary>
              <p>{{ post.gameVersion.changeLog }}</p>
            </details>

            <div class="life-post__engagement">
              <div class="life-post__engagement-actions">
                <LifeRatingControl
                  v-if="post.category?.isRateable"
                  :rating-average="post.ratingAverage"
                  :rating-count="post.ratingCount"
                  :my-rating="post.myRating"
                  :disabled="!canUseRatings(post)"
                  :busy="isRatingBusy(post.id)"
                  @rate="toggleRating(post, $event)"
                />

                <div class="life-reactions">
                  <div class="life-reaction-control">
                    <button
                      class="life-icon-button life-reaction-trigger"
                      :class="{ 'is-active': post.myReaction !== null }"
                      type="button"
                      :aria-controls="`life-reactions-${post.id}`"
                      :aria-expanded="reactionPickerPostId === post.id"
                      :aria-label="reactionButtonLabel(post)"
                      :disabled="!canReact || post.moderationStatus !== 'approved' || reactionBusyPostId !== null"
                      @click="toggleDefaultReaction(post)"
                      @contextmenu="handleReactionContextMenu($event, post.id)"
                      @keydown="handleReactionKeydown($event, post.id)"
                    >
                      <Icon :icon="reactionIcon(post.myReaction)" class="ui-icon" aria-hidden="true" />
                      <span class="life-action-tooltip" role="tooltip">{{ reactionButtonLabel(post) }}</span>
                    </button>

                    <button
                      class="life-icon-button life-reaction-menu-button"
                      type="button"
                      :aria-controls="`life-reactions-${post.id}`"
                      :aria-expanded="reactionPickerPostId === post.id"
                      :aria-label="t('pages.life.chooseReaction')"
                      :disabled="!canReact || post.moderationStatus !== 'approved' || reactionBusyPostId !== null"
                      @click="toggleReactionPicker(post.id)"
                      @contextmenu="handleReactionContextMenu($event, post.id)"
                      @keydown="handleReactionKeydown($event, post.id)"
                    >
                      <Icon :icon="iconChevronDown" class="ui-icon" aria-hidden="true" />
                      <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.chooseReaction') }}</span>
                    </button>
                  </div>

                  <div
                    v-if="reactionPickerPostId === post.id && canReact"
                    :id="`life-reactions-${post.id}`"
                    class="life-reaction-picker"
                    role="group"
                    :aria-label="t('pages.life.reactionMenu')"
                  >
                    <button
                      v-for="option in reactionOptions"
                      :key="option.type"
                      class="life-reaction-option"
                      :class="{ 'is-active': post.myReaction === option.type }"
                      type="button"
                      :aria-pressed="post.myReaction === option.type"
                      :aria-label="reactionOptionLabel(post, option.type)"
                      :disabled="post.moderationStatus !== 'approved' || isReactionBusy(post.id)"
                      @click="toggleReaction(post, option.type)"
                    >
                      <Icon :icon="option.icon" class="ui-icon" aria-hidden="true" />
                      <span>{{ reactionLabel(option.type) }}</span>
                    </button>
                  </div>
                </div>

                <button
                  class="life-icon-button"
                  type="button"
                  :aria-controls="`life-comments-${post.id}`"
                  :aria-expanded="areCommentsExpanded(post.id)"
                  :aria-label="areCommentsExpanded(post.id) ? t('pages.life.hideComments') : t('pages.life.comment')"
                  :disabled="post.moderationStatus !== 'approved'"
                  @click="toggleComments(post)"
                >
                  <Icon :icon="iconComment" class="ui-icon" aria-hidden="true" />
                  <span class="life-action-tooltip" role="tooltip">
                    {{ areCommentsExpanded(post.id) ? t('pages.life.hideComments') : t('pages.life.comment') }}
                  </span>
                </button>

                <div class="life-post__review-actions">
                  <StatusBadge
                    v-if="moderationStatusVisible(post.moderationStatus)"
                    :label="moderationLabel(post.moderationStatus)"
                    :tone="moderationTone(post.moderationStatus)"
                    compact
                  />
                  <button
                    v-if="canRetryModeration(post)"
                    class="life-icon-button life-review-button"
                    type="button"
                    :aria-label="moderationBusyPostId === post.id ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry')"
                    :disabled="moderationBusyPostId === post.id"
                    @click="retryPostModeration(post)"
                  >
                    <Icon :icon="iconWarning" class="ui-icon" aria-hidden="true" />
                    <span class="life-action-tooltip" role="tooltip">
                      {{ moderationBusyPostId === post.id ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry') }}
                    </span>
                  </button>
                </div>
              </div>

              <div class="life-post__metrics">
                <button
                  v-if="reactionTotal(post) > 0"
                  class="life-reaction-summary life-reaction-summary--button"
                  type="button"
                  :aria-label="t('pages.life.reactionsCount', { count: reactionTotal(post) })"
                  @click="openReactionUsersModal(post.id)"
                >
                  <template v-for="option in reactionOptions" :key="option.type">
                    <span
                      v-if="post.reactionCounts[option.type] > 0"
                      class="life-reaction-summary__item"
                      :aria-label="reactionCountLabel(post, option.type)"
                    >
                      <Icon :icon="option.icon" class="ui-icon" aria-hidden="true" />
                      {{ post.reactionCounts[option.type] }}
                      <span class="life-action-tooltip" role="tooltip">{{ reactionCountLabel(post, option.type) }}</span>
                    </span>
                  </template>
                </button>

                <button
                  class="life-metric-button"
                  type="button"
                  :aria-controls="`life-comments-${post.id}`"
                  :aria-expanded="areCommentsExpanded(post.id)"
                  :aria-label="t('pages.life.commentsCount', { count: commentCount(post) })"
                  :disabled="post.moderationStatus !== 'approved'"
                  @click="toggleComments(post)"
                >
                  <Icon :icon="iconComment" class="ui-icon" aria-hidden="true" />
                  <span>{{ commentCount(post) }}</span>
                  <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.commentsCount', { count: commentCount(post) }) }}</span>
                </button>
              </div>
            </div>

            <p v-if="canManage(post) && moderationReasonVisible(post.moderationStatus, post.moderationReason)" class="life-moderation-detail">
              <strong>{{ t('pages.life.moderationReason') }}</strong>
              <span>{{ post.moderationReason }}</span>
            </p>

            <p v-if="ratingErrors[post.id]" class="life-form__error" role="alert">{{ ratingErrors[post.id] }}</p>
            <p v-if="moderationErrors[post.id]" class="life-form__error" role="alert">{{ moderationErrors[post.id] }}</p>
            <p v-if="reactionErrors[post.id]" class="life-form__error" role="alert">{{ reactionErrors[post.id] }}</p>

            <section
              v-if="areCommentsExpanded(post.id)"
              :id="`life-comments-${post.id}`"
              class="life-comments"
              :aria-label="t('pages.life.comments')"
            >
              <div class="life-comments__header">
                <div>
                  <h3>{{ t('pages.life.comments') }}</h3>
                  <span>{{ commentCount(post) }}</span>
                </div>
                <label class="life-comments__sort">
                  <span>{{ t('pages.life.sort') }}</span>
                  <select :value="commentSort(post.id)" @change="handleCommentSortChange(post, $event)">
                    <option v-for="option in commentSortOptions" :key="option.value" :value="option.value">
                      {{ option.label }}
                    </option>
                  </select>
                </label>
              </div>

              <form v-if="canComment" class="life-comment-form" @submit.prevent="submitComment(post)">
                <div class="field">
                  <label :for="`life-comment-${post.id}`">{{ t('pages.life.comment') }}</label>
                  <textarea
                    :id="`life-comment-${post.id}`"
                    v-model="commentBodies[post.id]"
                    :maxlength="commentMaxLength"
                    :placeholder="t('pages.life.commentPlaceholder')"
                  ></textarea>
                </div>
                <p v-if="commentErrors[commentKey(post.id)]" class="life-form__error" role="alert">
                  {{ commentErrors[commentKey(post.id)] }}
                </p>
                <button
                  class="ui-button ui-button--ghost ui-button--small"
                  :disabled="isCommentBusy(commentKey(post.id)) || !(commentBodies[post.id] ?? '').trim()"
                  type="submit"
                >
                  <Icon :icon="iconComment" class="ui-icon" aria-hidden="true" />
                  {{ isCommentBusy(commentKey(post.id)) ? t('pages.life.postingComment') : t('pages.life.postComment') }}
                </button>
              </form>

              <div v-if="commentPage(post).loading && !commentsForPost(post).length" class="life-comment-list" :aria-label="t('pages.life.loadingComments')">
                <article v-for="index in 2" :key="`life-comments-loading-${post.id}-${index}`" class="life-comment">
                  <div class="life-comment__main">
                    <Skeleton variant="box" width="36px" height="36px" />
                    <div class="life-comment__content">
                      <Skeleton width="132px" />
                      <Skeleton width="86%" />
                    </div>
                  </div>
                </article>
              </div>

              <p v-else-if="commentPage(post).error" class="life-form__error" role="alert">{{ commentPage(post).error }}</p>

              <div v-else-if="commentsForPost(post).length" class="life-comment-list">
                <article
                  v-for="comment in commentsForPost(post)"
                  :key="comment.id"
                  class="life-comment"
                  :class="{ 'is-deleted': comment.deleted }"
                >
                  <div class="life-comment__main">
                    <div class="life-comment__avatar" aria-hidden="true">{{ commentInitial(comment) }}</div>
                    <div class="life-comment__content">
                      <div class="life-comment__meta">
                        <RouterLink v-if="comment.author" class="user-profile-link" :to="`/profile/${comment.author.id}`">
                          {{ comment.author.displayName }}
                        </RouterLink>
                        <strong v-else>{{ commentAuthorName(comment) }}</strong>
                        <time :datetime="comment.createdAt">{{ formatPostTime(comment.createdAt) }}</time>
                        <StatusBadge
                          v-if="canSeeCommentModeration(comment)"
                          :label="moderationLabel(comment.moderationStatus)"
                          :tone="moderationTone(comment.moderationStatus)"
                          compact
                        />
                      </div>
                      <p class="life-comment__body">{{ comment.body }}</p>
                      <p
                        v-if="canSeeCommentModeration(comment) && moderationReasonVisible(comment.moderationStatus, comment.moderationReason)"
                        class="life-moderation-detail life-moderation-detail--comment"
                      >
                        <strong>{{ t('pages.life.moderationReason') }}</strong>
                        <span>{{ comment.moderationReason }}</span>
                      </p>

                      <div v-if="!comment.deleted || canRestoreComment(comment)" class="life-comment__actions">
                        <button
                          v-if="!comment.deleted"
                          class="life-icon-button life-icon-button--flat"
                          type="button"
                          :aria-label="commentLikeLabel(comment)"
                          :aria-pressed="comment.myLiked"
                          :disabled="!canLikeComment(comment) || isCommentBusy(likeKey(comment.id))"
                          @click="toggleCommentLike(post, comment)"
                        >
                          <Icon :icon="iconReactionLike" class="ui-icon" aria-hidden="true" />
                          <span class="life-comment__action-count">{{ comment.likeCount }}</span>
                          <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.commentLikeCount', { count: comment.likeCount }) }}</span>
                        </button>
                        <button
                          v-if="!comment.deleted && canComment"
                          class="life-icon-button life-icon-button--flat"
                          type="button"
                          :aria-label="t('pages.life.reply')"
                          @click="startReply(comment)"
                        >
                          <Icon :icon="iconReply" class="ui-icon" aria-hidden="true" />
                          <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.reply') }}</span>
                        </button>
                        <button
                          v-if="canManageComment(comment)"
                          class="life-icon-button life-icon-button--flat life-icon-button--danger"
                          type="button"
                          :aria-label="t('pages.life.deleteComment')"
                          @click="deleteComment(post, comment)"
                        >
                          <Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
                          <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
                        </button>
                        <button
                          v-if="canRestoreComment(comment)"
                          class="life-icon-button life-icon-button--flat"
                          type="button"
                          :aria-label="t('pages.life.restoreComment')"
                          :disabled="isCommentBusy(replyKey(comment.id))"
                          @click="restoreComment(post, comment)"
                        >
                          <Icon :icon="iconUndo" class="ui-icon" aria-hidden="true" />
                          <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.restoreComment') }}</span>
                        </button>
                        <button
                          v-if="canRetryCommentModeration(comment)"
                          class="life-icon-button life-icon-button--flat"
                          type="button"
                          :aria-label="isCommentBusy(replyKey(comment.id)) ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry')"
                          :disabled="isCommentBusy(replyKey(comment.id))"
                          @click="retryCommentModeration(post, comment)"
                        >
                          <Icon :icon="iconWarning" class="ui-icon" aria-hidden="true" />
                          <span class="life-action-tooltip" role="tooltip">
                            {{ isCommentBusy(replyKey(comment.id)) ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry') }}
                          </span>
                        </button>
                      </div>

                      <p v-if="commentErrors[likeKey(comment.id)]" class="life-form__error" role="alert">
                        {{ commentErrors[likeKey(comment.id)] }}
                      </p>
                      <p v-if="commentErrors[replyKey(comment.id)]" class="life-form__error" role="alert">
                        {{ commentErrors[replyKey(comment.id)] }}
                      </p>

                      <form
                        v-if="canComment && replyTargetId === comment.id"
                        class="life-comment-form life-comment-form--reply"
                        @submit.prevent="submitReply(post, comment)"
                      >
                        <div class="field">
                          <label :for="`life-reply-${comment.id}`">{{ t('pages.life.reply') }}</label>
                          <textarea
                            :id="`life-reply-${comment.id}`"
                            v-model="replyBodies[comment.id]"
                            :maxlength="commentMaxLength"
                            :placeholder="t('pages.life.commentReplyPlaceholder')"
                          ></textarea>
                        </div>
                        <div class="life-form__actions">
                          <button
                            class="ui-button ui-button--ghost ui-button--small"
                            :disabled="isCommentBusy(replyKey(comment.id)) || !(replyBodies[comment.id] ?? '').trim()"
                            type="submit"
                          >
                            <Icon :icon="iconReply" class="ui-icon" aria-hidden="true" />
                            {{ isCommentBusy(replyKey(comment.id)) ? t('pages.life.postingReply') : t('pages.life.postReply') }}
                          </button>
                          <button class="ui-button ui-button--ghost ui-button--small" type="button" @click="cancelReply(comment.id)">
                            <Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
                            {{ t('pages.life.cancelReply') }}
                          </button>
                        </div>
                      </form>

                      <div v-if="comment.replies.length" class="life-comment-replies">
                        <article
                          v-for="reply in comment.replies"
                          :key="reply.id"
                          class="life-comment life-comment--reply"
                          :class="{ 'is-deleted': reply.deleted }"
                        >
                          <div class="life-comment__avatar" aria-hidden="true">{{ commentInitial(reply) }}</div>
                          <div class="life-comment__content">
                            <div class="life-comment__meta">
                              <RouterLink v-if="reply.author" class="user-profile-link" :to="`/profile/${reply.author.id}`">
                                {{ reply.author.displayName }}
                              </RouterLink>
                              <strong v-else>{{ commentAuthorName(reply) }}</strong>
                              <time :datetime="reply.createdAt">{{ formatPostTime(reply.createdAt) }}</time>
                              <StatusBadge
                                v-if="canSeeCommentModeration(reply)"
                                :label="moderationLabel(reply.moderationStatus)"
                                :tone="moderationTone(reply.moderationStatus)"
                                compact
                              />
                            </div>
                            <p class="life-comment__body">{{ reply.body }}</p>
                            <p
                              v-if="canSeeCommentModeration(reply) && moderationReasonVisible(reply.moderationStatus, reply.moderationReason)"
                              class="life-moderation-detail life-moderation-detail--comment"
                            >
                              <strong>{{ t('pages.life.moderationReason') }}</strong>
                              <span>{{ reply.moderationReason }}</span>
                            </p>
                            <div v-if="!reply.deleted || canRestoreComment(reply)" class="life-comment__actions">
                              <button
                                v-if="!reply.deleted"
                                class="life-icon-button life-icon-button--flat"
                                type="button"
                                :aria-label="commentLikeLabel(reply)"
                                :aria-pressed="reply.myLiked"
                                :disabled="!canLikeComment(reply) || isCommentBusy(likeKey(reply.id))"
                                @click="toggleCommentLike(post, reply)"
                              >
                                <Icon :icon="iconReactionLike" class="ui-icon" aria-hidden="true" />
                                <span class="life-comment__action-count">{{ reply.likeCount }}</span>
                                <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.commentLikeCount', { count: reply.likeCount }) }}</span>
                              </button>
                              <button
                                v-if="canManageComment(reply)"
                                class="life-icon-button life-icon-button--flat life-icon-button--danger"
                                type="button"
                                :aria-label="t('pages.life.deleteComment')"
                                @click="deleteComment(post, reply)"
                              >
                                <Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
                                <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
                              </button>
                              <button
                                v-if="canRestoreComment(reply)"
                                class="life-icon-button life-icon-button--flat"
                                type="button"
                                :aria-label="t('pages.life.restoreComment')"
                                :disabled="isCommentBusy(replyKey(reply.id))"
                                @click="restoreComment(post, reply)"
                              >
                                <Icon :icon="iconUndo" class="ui-icon" aria-hidden="true" />
                                <span class="life-action-tooltip" role="tooltip">{{ t('pages.life.restoreComment') }}</span>
                              </button>
                              <button
                                v-if="canRetryCommentModeration(reply)"
                                class="life-icon-button life-icon-button--flat"
                                type="button"
                                :aria-label="isCommentBusy(replyKey(reply.id)) ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry')"
                                :disabled="isCommentBusy(replyKey(reply.id))"
                                @click="retryCommentModeration(post, reply)"
                              >
                                <Icon :icon="iconWarning" class="ui-icon" aria-hidden="true" />
                                <span class="life-action-tooltip" role="tooltip">
                                  {{ isCommentBusy(replyKey(reply.id)) ? t('pages.life.moderationRetrying') : t('pages.life.moderationRetry') }}
                                </span>
                              </button>
                            </div>
                            <p v-if="commentErrors[likeKey(reply.id)]" class="life-form__error" role="alert">
                              {{ commentErrors[likeKey(reply.id)] }}
                            </p>
                            <p v-if="commentErrors[replyKey(reply.id)]" class="life-form__error" role="alert">
                              {{ commentErrors[replyKey(reply.id)] }}
                            </p>
                          </div>
                        </article>
                      </div>
                    </div>
                  </div>
                </article>
              </div>

              <p v-else class="life-comments__empty">{{ t('pages.life.noComments') }}</p>

              <LoadMoreSentinel
                :active="commentPage(post).hasMore && !commentPage(post).loading"
                :disabled="commentPage(post).loadingMore"
                @load="loadComments(post)"
              />
            </section>
          </article>

          <article v-for="index in loadingMore ? loadingMoreSkeletonCount : 0" :key="`life-more-${index}`" class="life-post life-post--skeleton">
            <div class="life-post__header">
              <Skeleton variant="box" width="46px" height="46px" />
              <div class="life-post__byline">
                <Skeleton width="138px" />
                <Skeleton width="96px" />
              </div>
            </div>
            <Skeleton width="94%" />
            <Skeleton width="76%" />
            <Skeleton width="52%" />
          </article>

          <div v-if="hasMorePosts" ref="loadMoreSentinel" class="life-feed__sentinel" aria-hidden="true"></div>
          <div v-if="loadMorePaused && hasMorePosts" class="life-feed__retry">
            <button class="ui-button ui-button--ghost ui-button--small" type="button" @click="retryLoadMore">
              <Icon :icon="iconWarning" class="ui-icon" aria-hidden="true" />
              {{ t('pages.life.retryFeed') }}
            </button>
          </div>
        </div>

        <div v-else class="life-empty">
          <Icon :icon="searchQuery ? iconSearch : iconLife" class="life-empty__icon" aria-hidden="true" />
          <div class="life-empty__copy">
            <h2>{{ searchQuery ? t('pages.life.searchEmpty') : t('pages.life.empty') }}</h2>
            <p>{{ searchQuery ? t('pages.life.searchEmptyHint') : t('pages.life.emptyHint') }}</p>
          </div>
          <button v-if="searchQuery" class="ui-button ui-button--ghost" type="button" @click="clearSearch">
            <Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
            {{ t('pages.life.clearSearch') }}
          </button>
          <button v-else class="ui-button ui-button--primary" :disabled="!authReady" type="button" @click="openCreatePostModal">
            <Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
            {{ t('pages.life.newPost') }}
          </button>
        </div>
    </section>
  </section>
</template>
