<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict';
import {
  fetchGetMyNotifyMessagePage,
  fetchGetUnreadNotifyCount,
  fetchUpdateAllNotifyMessageRead,
  fetchUpdateNotifyMessageRead
} from '@/service/api';
import { useDictStore } from '@/store/modules/dict';
import { formatDateTime, formatRelativeTime } from '@/utils/datetime';

defineOptions({ name: 'NotificationBell' });

const dictStore = useDictStore();
const router = useRouter();

const PAGE_SIZE = 10;
const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000;

type TabKey = 'unread' | 'read';

interface MessageListState {
  items: Api.NotifyMessage.NotifyMessage[];
  pageNo: number;
  total: number;
  loading: boolean;
  /** 是否已按当前关键字拉过第一页（tab 懒加载 / 失效重拉用） */
  loaded: boolean;
  /** 竞态令牌：重置后递增，过期响应直接丢弃 */
  token: number;
}

function createListState(): MessageListState {
  return { items: [], pageNo: 1, total: 0, loading: false, loaded: false, token: 0 };
}

const listStates = reactive<Record<TabKey, MessageListState>>({
  unread: createListState(),
  read: createListState()
});

const unreadCount = ref(0);
const badgeLabel = computed(() => (unreadCount.value > 99 ? '99+' : String(unreadCount.value)));

const drawerOpen = ref(false);
const activeTab = ref<TabKey>('unread');
const searchKeyword = ref('');

const detailVisible = ref(false);
const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null);

interface NotifyRenderParts {
  prefix: string;
  clickable: string;
  suffix: string;
}

function keywordParam() {
  return searchKeyword.value.trim() || undefined;
}

/** 列表圆点颜色：跟随消息等级（与等级徽标同一字典色源）；取不到时回 undefined，由 CSS 兜底 */
function levelDotColor(level: number) {
  return dictStore.getDictItem(NOTIFY_MESSAGE_LEVEL_DICT_CODE, level)?.colorType ?? undefined;
}

async function refreshUnreadCount() {
  const { data, error } = await fetchGetUnreadNotifyCount();
  if (!error && typeof data === 'number') {
    unreadCount.value = data;
  }
}

function resetList(tab: TabKey) {
  const state = listStates[tab];
  state.token += 1;
  state.items = [];
  state.pageNo = 1;
  state.total = 0;
  state.loading = false;
  state.loaded = false;
}

async function loadPage(tab: TabKey) {
  const state = listStates[tab];
  if (state.loading) return;

  const token = state.token;
  state.loading = true;

  const { data, error } = await fetchGetMyNotifyMessagePage({
    pageNo: state.pageNo,
    pageSize: PAGE_SIZE,
    readStatus: tab === 'read',
    keyword: keywordParam()
  });

  if (token !== state.token) return;

  state.loading = false;
  state.loaded = true;

  if (error || !data) return;

  state.items.push(...data.list);
  state.total = data.total;
  state.pageNo += 1;
}

function hasMore(tab: TabKey) {
  const state = listStates[tab];
  return state.loaded && state.items.length < state.total;
}

function ensureLoaded(tab: TabKey) {
  const state = listStates[tab];
  if (!state.loaded && !state.loading) {
    loadPage(tab);
  }
}

const applyKeywordSearch = useDebounceFn(() => {
  if (!drawerOpen.value) return;
  resetList('unread');
  resetList('read');
  loadPage(activeTab.value);
}, 300);

watch(searchKeyword, () => {
  applyKeywordSearch();
});

watch(activeTab, tab => {
  ensureLoaded(tab);
});

type ScrollbarRefValue = { wrapRef?: HTMLElement } | null;
const unreadScrollbar = ref<ScrollbarRefValue>(null);
const readScrollbar = ref<ScrollbarRefValue>(null);

useInfiniteScroll(
  () => unreadScrollbar.value?.wrapRef,
  () => {
    if (drawerOpen.value && hasMore('unread') && !listStates.unread.loading) {
      loadPage('unread');
    }
  },
  { distance: 48 }
);

useInfiniteScroll(
  () => readScrollbar.value?.wrapRef,
  () => {
    if (drawerOpen.value && hasMore('read') && !listStates.read.loading) {
      loadPage('read');
    }
  },
  { distance: 48 }
);

function openDrawer() {
  drawerOpen.value = true;
  // 每次打开面板都从第 1 页重拉（与后端对齐的消费口径）
  resetList('unread');
  resetList('read');
  loadPage(activeTab.value);
  refreshUnreadCount();
}

function closeDrawer() {
  drawerOpen.value = false;
}

function onDrawerClosed() {
  searchKeyword.value = '';
}

async function markRead(item: Api.NotifyMessage.NotifyMessage) {
  const { error } = await fetchUpdateNotifyMessageRead([item.id]);
  if (error) return;

  // 本地移除、不按原页号回拉，避免未读集合收缩导致的分页漂移
  const state = listStates.unread;
  const index = state.items.findIndex(row => row.id === item.id);
  if (index >= 0) {
    state.items.splice(index, 1);
    state.total = Math.max(0, state.total - 1);
  }
  unreadCount.value = Math.max(0, unreadCount.value - 1);

  // 已读列表失效，下次进入已读 tab 时从第 1 页重拉
  resetList('read');

  // 移除后剩余条目不足一页且还有更多时补拉，防止列表不再触发滚动加载
  if (state.items.length < PAGE_SIZE && hasMore('unread')) {
    loadPage('unread');
  }
}

function openDetail(row: Api.NotifyMessage.NotifyMessage) {
  // 弹框持有该行引用，正文不随未读列表移除而消失
  detailMessage.value = row;
  detailVisible.value = true;
  // 未读消息「打开即已读」：后台静默标记，避免"看一半就跑到已读"
  if (!row.readStatus) {
    markRead(row);
  }
}

function getClickableText(message: Api.NotifyMessage.NotifyMessage) {
  const params = message.templateParams;
  switch (message.templateCode) {
    case 'execution_assigned':
      return params?.executionName?.trim() || '';
    case 'task_assigned':
      return params?.taskName?.trim() || '';
    case 'project_created':
      return params?.projectName?.trim() || '';
    default:
      return '';
  }
}

function canJump(message: Api.NotifyMessage.NotifyMessage) {
  const params = message.templateParams;
  return Boolean(params?.jumpType && params.projectId && getClickableText(message));
}

function renderNotifyParts(message: Api.NotifyMessage.NotifyMessage): NotifyRenderParts {
  const content = message.templateContent || '';
  const clickable = getClickableText(message);
  if (!content || !clickable) {
    return { prefix: content, clickable: '', suffix: '' };
  }

  const startIndex = content.indexOf(clickable);
  if (startIndex < 0) {
    return { prefix: content, clickable: '', suffix: '' };
  }

  return {
    prefix: content.slice(0, startIndex),
    clickable,
    suffix: content.slice(startIndex + clickable.length)
  };
}

async function handleNotifyJump(message: Api.NotifyMessage.NotifyMessage) {
  const params = message.templateParams;
  if (!params?.jumpType || !params.projectId) return;

  const query: Record<string, string> = {
    [OBJECT_CONTEXT_QUERY_KEY]: params.projectId
  };

  let path = '';

  switch (params.jumpType) {
    case 'project':
      path = '/project/project/overview';
      break;
    case 'execution':
      path = '/project/project/execution';
      if (params.executionName) {
        query.executionName = params.executionName;
      }
      break;
    case 'task':
      path = '/project/project/execution';
      if (params.taskName) {
        query.taskName = params.taskName;
      }
      break;
    default:
      return;
  }

  detailVisible.value = false;
  drawerOpen.value = false;
  await router.push({ path, query });
}

async function onNotifyLinkClick(row: Api.NotifyMessage.NotifyMessage) {
  if (!canJump(row)) return;

  if (!row.readStatus) {
    await markRead(row);
  }

  await handleNotifyJump(row);
}

async function markAllRead() {
  const { error } = await fetchUpdateAllNotifyMessageRead();
  if (error) {
    window.$message?.error('全部标记已读失败，请重试');
    return;
  }

  unreadCount.value = 0;
  resetList('unread');
  resetList('read');
  loadPage(activeTab.value);
  // 以后端真实未读数兜底校准，避免乐观置 0 与后端状态偏差（不必等 15s 轮询）
  refreshUnreadCount();
}

let pollTimer: ReturnType<typeof setInterval> | null = null;

onMounted(() => {
  // 等级徽标颜色/文案走字典：若未在登录缓存内则按编码补拉一次（已缓存时不发请求）
  dictStore.ensureDictData(NOTIFY_MESSAGE_LEVEL_DICT_CODE);
  refreshUnreadCount();
  pollTimer = setInterval(() => {
    if (document.hidden) return;
    refreshUnreadCount();
  }, UNREAD_COUNT_POLL_INTERVAL);
});

onBeforeUnmount(() => {
  if (pollTimer) {
    clearInterval(pollTimer);
    pollTimer = null;
  }
});
</script>

<template>
  <button
    class="notification-bell__trigger"
    type="button"
    :aria-label="unreadCount > 0 ? `通知，${unreadCount} 条未读` : '通知'"
    @click="openDrawer"
  >
    <SvgIcon icon="mdi:bell-outline" class="notification-bell__icon" />
    <span v-if="unreadCount > 0" class="notification-bell__badge">{{ badgeLabel }}</span>
  </button>

  <ElDrawer v-model="drawerOpen" size="480px" @closed="onDrawerClosed">
    <template #header>
      <div class="notification-bell__header-main">
        <span class="notification-bell__title">
          通知
          <span v-if="unreadCount > 0" class="notification-bell__title-count">未读 {{ unreadCount }}</span>
        </span>
        <ElButton v-if="unreadCount > 0" link size="small" @click="markAllRead">全部已读</ElButton>
      </div>
    </template>

    <div class="notification-bell__panel">
      <div class="notification-bell__search">
        <ElInput v-model="searchKeyword" placeholder="搜索通知" clearable>
          <template #prefix>
            <SvgIcon icon="mdi:magnify" />
          </template>
        </ElInput>
      </div>

      <ElTabs v-model="activeTab" class="notification-bell__tabs">
        <ElTabPane name="unread">
          <template #label>
            <span class="notification-bell__tab-label">
              未读
              <span class="notification-bell__tab-count">{{ badgeLabel }}</span>
            </span>
          </template>
          <ElScrollbar ref="unreadScrollbar" class="notification-bell__scroll">
            <ul v-if="listStates.unread.items.length > 0" class="notification-bell__list">
              <li
                v-for="row in listStates.unread.items"
                :key="row.id"
                class="notification-bell__row is-unread"
                @click="openDetail(row)"
              >
                <span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
                <div class="notification-bell__row-body">
                  <div class="notification-bell__row-title">
                    <template v-if="canJump(row)">
                      <template v-for="(part, index) in [renderNotifyParts(row)]" :key="`${row.id}-${index}`">
                        <span>{{ part.prefix }}</span>
                        <button
                          v-if="part.clickable"
                          type="button"
                          class="notification-bell__inline-link"
                          @click.stop="onNotifyLinkClick(row)"
                        >
                          {{ part.clickable }}
                        </button>
                        <span>{{ part.suffix }}</span>
                      </template>
                    </template>
                    <template v-else>
                      {{ row.templateContent }}
                    </template>
                  </div>
                  <div class="notification-bell__row-meta">
                    <DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
                    <span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
                  </div>
                </div>
              </li>
            </ul>
            <div v-else class="notification-bell__empty">
              {{ listStates.unread.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无未读通知' }}
            </div>
            <div v-if="listStates.unread.items.length > 0" class="notification-bell__footer-hint">
              {{ listStates.unread.loading ? '加载中…' : hasMore('unread') ? '滚动加载更多…' : '— 已经到底了 —' }}
            </div>
          </ElScrollbar>
        </ElTabPane>

        <ElTabPane name="read">
          <template #label>
            <span class="notification-bell__tab-label">已读</span>
          </template>
          <ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
            <ul v-if="listStates.read.items.length > 0" class="notification-bell__list">
              <li
                v-for="row in listStates.read.items"
                :key="row.id"
                class="notification-bell__row"
                @click="openDetail(row)"
              >
                <span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
                <div class="notification-bell__row-body">
                  <div class="notification-bell__row-title">
                    <template v-if="canJump(row)">
                      <template v-for="(part, index) in [renderNotifyParts(row)]" :key="`${row.id}-${index}`">
                        <span>{{ part.prefix }}</span>
                        <button
                          v-if="part.clickable"
                          type="button"
                          class="notification-bell__inline-link"
                          @click.stop="onNotifyLinkClick(row)"
                        >
                          {{ part.clickable }}
                        </button>
                        <span>{{ part.suffix }}</span>
                      </template>
                    </template>
                    <template v-else>
                      {{ row.templateContent }}
                    </template>
                  </div>
                  <div class="notification-bell__row-meta">
                    <DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
                    <span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
                  </div>
                </div>
              </li>
            </ul>
            <div v-else class="notification-bell__empty">
              {{ listStates.read.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无已读通知' }}
            </div>
            <div v-if="listStates.read.items.length > 0" class="notification-bell__footer-hint">
              {{ listStates.read.loading ? '加载中…' : hasMore('read') ? '滚动加载更多…' : '— 已经到底了 —' }}
            </div>
          </ElScrollbar>
        </ElTabPane>
      </ElTabs>
    </div>

    <template #footer>
      <ElButton @click="closeDrawer">关闭</ElButton>
    </template>
  </ElDrawer>

  <ElDialog v-model="detailVisible" width="520px" align-center class="notification-bell__detail">
    <template #header>
      <div class="notification-bell__detail-head">
        <span class="notification-bell__detail-sender">{{ detailMessage?.templateNickname || '系统通知' }}</span>
        <DictTag
          v-if="detailMessage"
          :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE"
          :value="detailMessage.level"
          size="small"
          round
        />
      </div>
    </template>
    <div v-if="detailMessage" class="notification-bell__detail-body">
      <div class="notification-bell__detail-content">
        <template v-if="canJump(detailMessage)">
          <template v-for="(part, index) in [renderNotifyParts(detailMessage)]" :key="`detail-${index}`">
            <span>{{ part.prefix }}</span>
            <button
              v-if="part.clickable"
              type="button"
              class="notification-bell__inline-link"
              @click.stop="onNotifyLinkClick(detailMessage)"
            >
              {{ part.clickable }}
            </button>
            <span>{{ part.suffix }}</span>
          </template>
        </template>
        <template v-else>
          {{ detailMessage.templateContent }}
        </template>
      </div>
      <div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
    </div>
    <template #footer>
      <ElButton @click="detailVisible = false">关闭</ElButton>
    </template>
  </ElDialog>
</template>

<style scoped>
.notification-bell__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  margin: 0 4px;
  border: none;
  border-radius: 8px;
  background-color: transparent;
  color: var(--el-text-color-regular);
  cursor: pointer;
  transition:
    background-color 160ms ease,
    color 160ms ease;
}

.notification-bell__trigger:hover {
  background-color: var(--el-fill-color-light);
  color: var(--el-color-primary);
}

.notification-bell__trigger:focus-visible {
  outline: 2px solid var(--el-color-primary);
  outline-offset: 2px;
}

.notification-bell__icon {
  font-size: 20px;
}

.notification-bell__badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border: 1px solid #fff;
  border-radius: 999px;
  background-color: var(--el-color-danger);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  animation: notification-badge-pulse 1.6s ease-in-out infinite;
}

/* 扩散波纹：跟随心跳节奏向外晕开，增强未读提醒的醒目度 */
.notification-bell__badge::before {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 999px;
  background-color: var(--el-color-danger);
  animation: notification-badge-ping 1.6s ease-out infinite;
  z-index: -1;
}

@keyframes notification-badge-pulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.18);
  }
}

@keyframes notification-badge-ping {
  0% {
    transform: scale(1);
    opacity: 0.6;
  }
  70%,
  100% {
    transform: scale(1.9);
    opacity: 0;
  }
}

.notification-bell__panel {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.notification-bell__header-main {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
  margin-right: 8px;
}

.notification-bell__title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--el-text-color-primary);
  font-size: 16px;
  font-weight: 600;
}

.notification-bell__title-count {
  padding: 1px 8px;
  border-radius: 999px;
  background-color: var(--el-color-danger);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
}

.notification-bell__search {
  padding: 0 0 4px;
}

.notification-bell__tabs {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
}

.notification-bell__tabs :deep(.el-tabs__content) {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.notification-bell__tabs :deep(.el-tab-pane) {
  height: 100%;
}

.notification-bell__tab-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.notification-bell__tab-count {
  padding: 0 7px;
  border-radius: 999px;
  background-color: var(--el-fill-color);
  color: var(--el-text-color-secondary);
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
}

.notification-bell__tabs :deep(.el-tabs__item.is-active) .notification-bell__tab-count {
  background-color: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
}

.notification-bell__scroll {
  height: 100%;
}

.notification-bell__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.notification-bell__row {
  position: relative;
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr);
  align-items: flex-start;
  gap: 10px;
  padding: 12px 4px;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 120ms ease;
}

.notification-bell__row + .notification-bell__row {
  border-top: 1px dashed var(--el-border-color-lighter);
}

.notification-bell__row:hover {
  background-color: var(--el-fill-color-light);
}

.notification-bell__row-dot {
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: 50%;
  background-color: transparent;
  justify-self: center;
}

.notification-bell__row.is-unread .notification-bell__row-dot {
  background-color: var(--el-color-primary);
}

.notification-bell__row-body {
  min-width: 0;
}

.notification-bell__row-title {
  color: var(--el-text-color-regular);
  font-size: 14px;
  line-height: 1.5;
}

.notification-bell__row.is-unread .notification-bell__row-title {
  color: var(--el-text-color-primary);
  font-weight: 500;
}

.notification-bell__inline-link {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--el-color-primary);
  cursor: pointer;
  font: inherit;
}

.notification-bell__inline-link:hover {
  text-decoration: underline;
}

.notification-bell__row-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

.notification-bell__row-time {
  color: var(--el-text-color-secondary);
  font-size: 12px;
}

.notification-bell__empty {
  padding: 48px 16px;
  text-align: center;
  color: var(--el-text-color-secondary);
  font-size: 13px;
}

.notification-bell__footer-hint {
  padding: 12px 0 4px;
  text-align: center;
  color: var(--el-text-color-secondary);
  font-size: 12px;
  user-select: none;
}

.notification-bell__detail-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.notification-bell__detail-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-right: 8px;
  min-width: 0;
}

.notification-bell__detail-sender {
  min-width: 0;
  overflow: hidden;
  color: var(--el-text-color-primary);
  font-size: 16px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notification-bell__detail-content {
  color: var(--el-text-color-regular);
  font-size: 14px;
  line-height: 1.7;
  white-space: pre-wrap;
  word-break: break-word;
}

.notification-bell__detail-time {
  color: var(--el-text-color-secondary);
  font-size: 12px;
}
</style>
