<script setup lang="ts">
import {
  CloseOutlined,
  LeftOutlined,
  SafetyCertificateOutlined,
  SearchOutlined,
} from '@ant-design/icons-vue'
import type { ComplianceCheckResult, ComplianceViolation } from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'

import ArticleEditorCanvas from '@/components/ArticleEditorCanvas.vue'
import PublishArticleModal from '@/components/PublishArticleModal.vue'
import { articlesApi, complianceApi } from '@/lib/api'
import { formatError } from '@/lib/errors'

type EditorCanvasPublic = InstanceType<typeof ArticleEditorCanvas> & {
  scrollToText?: (text: string) => boolean
}

const route = useRoute()
const router = useRouter()
const queryClient = useQueryClient()
const { t } = useI18n()

const articleId = computed(() => Number(route.params.id))
const title = ref('')
const markdown = ref('')
const initialTitle = ref('')
const initialMarkdown = ref('')
const leaveModalOpen = ref(false)
const publishModalOpen = ref(false)
const compliancePanelOpen = ref(false)
const editorComplianceResult = ref<ComplianceCheckResult | null>(null)
const editorCanvasRef = ref<EditorCanvasPublic | null>(null)

const detailQuery = useQuery({
  queryKey: computed(() => ['articles', 'detail', articleId.value]),
  enabled: computed(() => Number.isInteger(articleId.value) && articleId.value > 0),
  queryFn: () => articlesApi.detail(articleId.value),
})

watch(
  () => detailQuery.data.value,
  (detail) => {
    if (!detail) {
      return
    }

    const nextTitle = detail.title ?? ''
    const nextMarkdown = stripLeadingTitleHeading(nextTitle, detail.markdown_content ?? '')

    title.value = nextTitle
    markdown.value = nextMarkdown
    initialTitle.value = nextTitle
    initialMarkdown.value = nextMarkdown
  },
  { immediate: true },
)

const detail = computed(() => detailQuery.data.value)
const editorLocked = computed(() => detail.value?.generate_status !== 'completed')
const referencedImageAssetIds = computed(() => extractReferencedImageAssetIds(markdown.value))
const hasChanges = computed(
  () =>
    title.value.trim() !== initialTitle.value.trim() || markdown.value !== initialMarkdown.value,
)

const saveMutation = useMutation({
  mutationFn: () =>
    articlesApi.update(articleId.value, {
      title: title.value.trim(),
      markdown_content: markdown.value,
      referenced_image_asset_ids: referencedImageAssetIds.value,
    }),
  onSuccess: async (data) => {
    message.success(t('article.editor.messages.saved'))
    title.value = data.title ?? ''
    markdown.value = data.markdown_content ?? ''
    initialTitle.value = title.value
    initialMarkdown.value = markdown.value
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['articles'] }),
      queryClient.invalidateQueries({ queryKey: ['workspace'] }),
      queryClient.invalidateQueries({ queryKey: ['templates'] }),
      queryClient.invalidateQueries({ queryKey: ['articles', 'detail', articleId.value] }),
      queryClient.invalidateQueries({
        queryKey: ['articles', 'detail', articleId.value, 'publish-modal'],
      }),
    ])
  },
  onError: (error) => {
    message.error(formatError(error))
  },
})

const saveDisabled = computed(
  () =>
    editorLocked.value ||
    saveMutation.isPending.value ||
    title.value.trim() === '' ||
    !hasChanges.value,
)
const complianceCheckLoading = ref(false)
const editorComplianceViolations = computed(() =>
  compliancePanelOpen.value ? (editorComplianceResult.value?.violations ?? []) : [],
)
const complianceGroups = computed(() => groupViolations(editorComplianceResult.value?.violations ?? []))
const complianceElapsedLabel = computed(() => {
  const duration = editorComplianceResult.value?.duration_ms ?? 0
  return duration > 0 ? `${duration} ms` : '< 1 ms'
})
const complianceDecisionTone = computed(() =>
  (editorComplianceResult.value?.hit_count ?? 0) > 0 ? 'warning' : 'success',
)
const leaveSaveDisabled = computed(
  () => editorLocked.value || saveMutation.isPending.value || title.value.trim() === '',
)

async function saveArticle(): Promise<boolean> {
  if (leaveSaveDisabled.value || !hasChanges.value) {
    return false
  }

  try {
    await saveMutation.mutateAsync()
    return true
  } catch {
    return false
  }
}

async function handleSave(): Promise<void> {
  if (saveDisabled.value) {
    return
  }

  await saveArticle()
}

function handleSaveShortcut(event: KeyboardEvent): void {
  if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') {
    return
  }

  event.preventDefault()
  event.stopPropagation()
  if (event.repeat || saveDisabled.value) {
    return
  }

  void handleSave()
}

async function handlePublish(): Promise<void> {
  if (editorLocked.value) {
    return
  }

  if (hasChanges.value && !saveMutation.isPending.value) {
    try {
      await saveMutation.mutateAsync()
    } catch {
      return
    }
  }

  publishModalOpen.value = true
}

async function handleComplianceCheck(): Promise<void> {
  if (editorLocked.value || complianceCheckLoading.value) {
    return
  }

  if (hasChanges.value && !saveMutation.isPending.value) {
    try {
      await saveMutation.mutateAsync()
    } catch {
      return
    }
  }

  complianceCheckLoading.value = true
  try {
    const currentDetail = (await detailQuery.refetch()).data ?? detail.value
    const versionId = currentDetail?.current_version_id ?? null
    const result = await complianceApi.checkArticle(articleId.value, {
      article_version_id: versionId,
      title: title.value.trim(),
      plaintext: markdown.value,
      target_platforms: [],
      trigger_source: 'editor_manual',
    })
    editorComplianceResult.value = result
    compliancePanelOpen.value = true
    if (result.hit_count > 0) {
      message.warning(`发现 ${result.hit_count} 条合规风险`)
    } else {
      message.success('合规检测通过')
    }
  } catch (error) {
    message.error(formatError(error))
  } finally {
    complianceCheckLoading.value = false
  }
}

function closeCompliancePanel(): void {
  compliancePanelOpen.value = false
}

function focusViolation(violation: ComplianceViolation): void {
  const ok = editorCanvasRef.value?.scrollToText?.(violation.matched_text)
  if (!ok) {
    message.info('未能在当前编辑器内容中定位该命中片段')
  }
}

function groupViolations(violations: ComplianceViolation[]) {
  const groups = new Map<string, ComplianceViolation[]>()
  for (const violation of violations) {
    const key = violation.platform_code || 'all'
    const list = groups.get(key) ?? []
    list.push(violation)
    groups.set(key, list)
  }
  return Array.from(groups.entries()).map(([key, items]) => ({ key, items }))
}

function violationGroupTitle(key: string): string {
  return key === 'all' ? '全部平台共用规则' : `平台规则：${key}`
}

function violationKey(violation: ComplianceViolation): string {
  return [
    violation.id || 0,
    violation.source,
    violation.platform_code ?? 'all',
    violation.start_offset,
    violation.end_offset,
    violation.matched_text,
  ].join(':')
}

function levelColor(level: ComplianceViolation['level']): string {
  switch (level) {
    case 'block':
      return 'red'
    case 'high':
      return 'orange'
    case 'medium':
      return 'gold'
    default:
      return 'blue'
  }
}

function levelLabel(level: ComplianceViolation['level']): string {
  switch (level) {
    case 'block':
      return '重点风险'
    case 'high':
      return '高风险'
    case 'medium':
      return '中风险'
    default:
      return '提示'
  }
}

function sourceLabel(source: ComplianceViolation['source']): string {
  switch (source) {
    case 'dict':
      return '词库'
    case 'regex':
      return '正则'
    case 'llm':
      return 'LLM'
    default:
      return source
  }
}

async function uploadEditorImage(file: File): Promise<string> {
  const result = await articlesApi.uploadImage(articleId.value, file)
  return result.url
}

async function handlePublished(): Promise<void> {
  await Promise.all([
    detailQuery.refetch(),
    queryClient.invalidateQueries({ queryKey: ['articles'] }),
    queryClient.invalidateQueries({ queryKey: ['workspace'] }),
  ])
}

const isFreeCreateEmpty = computed(
  () =>
    detail.value?.source_type === 'free_create' &&
    title.value.trim() === '' &&
    markdown.value.trim() === '',
)

const isFreeCreateInitiallyEmpty = computed(
  () =>
    detail.value?.source_type === 'free_create' &&
    initialTitle.value.trim() === '' &&
    initialMarkdown.value.trim() === '',
)

function handleBack(): void {
  if (saveMutation.isPending.value) {
    return
  }

  if (isFreeCreateEmpty.value) {
    void cleanupEmptyFreeCreate()
    return
  }

  if (hasChanges.value) {
    leaveModalOpen.value = true
    return
  }

  navigateBack()
}

async function cleanupEmptyFreeCreate(): Promise<void> {
  try {
    await articlesApi.remove(articleId.value)
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ['articles'] }),
      queryClient.invalidateQueries({ queryKey: ['workspace'] }),
    ])
  } catch {
    // ignore cleanup errors
  }
  navigateBack()
}

function navigateBack(): void {
  const returnTo = normalizeReturnTo(route.query.return_to)
  if (returnTo) {
    void router.push(returnTo)
    return
  }

  if (window.history.length > 1) {
    void router.back()
    return
  }

  void router.push('/articles/templates')
}

function normalizeReturnTo(value: unknown): string | null {
  const raw = Array.isArray(value) ? value[0] : value
  if (typeof raw !== 'string') {
    return null
  }
  const trimmed = raw.trim()
  if (!trimmed || !trimmed.startsWith('/') || trimmed.startsWith('//')) {
    return null
  }
  return trimmed
}

function handleStay(): void {
  leaveModalOpen.value = false
}

function handleDiscardLeave(): void {
  leaveModalOpen.value = false
  if (isFreeCreateInitiallyEmpty.value) {
    void cleanupEmptyFreeCreate()
    return
  }
  navigateBack()
}

async function handleSaveAndLeave(): Promise<void> {
  const saved = await saveArticle()
  if (!saved) {
    return
  }

  leaveModalOpen.value = false
  navigateBack()
}

function stripLeadingTitleHeading(titleValue: string, markdownValue: string): string {
  const normalizedTitle = titleValue.trim()
  if (!normalizedTitle) {
    return markdownValue
  }

  const normalizedMarkdown = markdownValue.replace(/^\uFEFF/, '')
  const match = normalizedMarkdown.match(/^#\s+(.+?)\s*(\r?\n|$)/)
  if (!match) {
    return markdownValue
  }

  if (match[1].trim() !== normalizedTitle) {
    return markdownValue
  }

  return normalizedMarkdown.slice(match[0].length).replace(/^\s*\r?\n/, '')
}

function extractReferencedImageAssetIds(markdownValue: string): number[] {
  const ids = new Set<number>()
  const matcher = /data-asset-id=(?:"|')(\d+)(?:"|')/g

  for (const match of markdownValue.matchAll(matcher)) {
    const id = Number.parseInt(match[1] ?? '', 10)
    if (Number.isInteger(id) && id > 0) {
      ids.add(id)
    }
  }

  return [...ids]
}

onMounted(() => {
  window.addEventListener('keydown', handleSaveShortcut)
})

onBeforeUnmount(() => {
  window.removeEventListener('keydown', handleSaveShortcut)
})
</script>

<template>
  <div class="article-editor-view">
    <header class="article-editor-view__topbar">
      <button class="article-editor-view__back" type="button" @click="handleBack">
        <LeftOutlined />
        <span>{{ t('common.back') }}</span>
      </button>

      <div class="article-editor-view__actions">
        <a-button
          :disabled="editorLocked"
          :loading="complianceCheckLoading"
          @click="handleComplianceCheck"
        >
          <template #icon><SafetyCertificateOutlined /></template>
          合规检测
        </a-button>
        <a-button
          :disabled="saveDisabled"
          :loading="saveMutation.isPending.value"
          title="Ctrl/Cmd + S"
          aria-keyshortcuts="Control+S Meta+S"
          @click="handleSave"
        >
          {{ t('common.save') }}
        </a-button>
        <a-button type="primary" :disabled="editorLocked" @click="handlePublish">
          {{ t('article.editor.publish') }}
        </a-button>
      </div>
    </header>

    <div v-if="detailQuery.isPending.value" class="article-editor-view__loading">
      <a-skeleton active :paragraph="{ rows: 14 }" />
    </div>

    <template v-else-if="detail">
      <a-alert
        v-if="editorLocked"
        class="article-editor-view__alert"
        type="info"
        show-icon
        :message="t('article.editor.messages.locked')"
      />
      <div
        class="article-editor-view__layout"
        :class="{ 'article-editor-view__layout--with-compliance': compliancePanelOpen }"
      >
        <section class="article-editor-view__main">
          <ArticleEditorCanvas
            ref="editorCanvasRef"
            :key="String(articleId)"
            v-model:title="title"
            v-model="markdown"
            :article-id="articleId"
            :disabled="editorLocked"
            :upload-image="uploadEditorImage"
            :compliance-violations="editorComplianceViolations"
          />
        </section>

        <aside
          v-if="compliancePanelOpen"
          class="article-editor-view__compliance-panel"
          aria-label="内容合规检测"
        >
          <div class="article-editor-view__compliance-panel-head">
            <h2>内容合规检测</h2>
            <a-button
              type="text"
              shape="circle"
              aria-label="关闭合规检测"
              @click="closeCompliancePanel"
            >
              <template #icon><CloseOutlined /></template>
            </a-button>
          </div>

          <template v-if="editorComplianceResult">
            <a-alert
              show-icon
              :type="complianceDecisionTone"
              :message="
                editorComplianceResult.hit_count > 0
                  ? `发现 ${editorComplianceResult.hit_count} 条风险，仅供编辑参考`
                  : '当前版本未发现合规风险'
              "
              :description="
                editorComplianceResult.truncated
                  ? '命中明细已达到保存上限，请优先处理高风险词条后重新检测。'
                  : undefined
              "
            />

            <div class="article-editor-view__compliance-meta">
              <a-tag :color="editorComplianceResult.hit_count > 0 ? 'orange' : 'green'">
                {{ editorComplianceResult.hit_count > 0 ? '发现风险' : '检测通过' }}
              </a-tag>
              <span>耗时 {{ complianceElapsedLabel }}</span>
            </div>

            <a-empty v-if="!complianceGroups.length" description="没有命中项" />

            <section
              v-for="group in complianceGroups"
              v-else
              :key="group.key"
              class="article-editor-view__violation-group"
            >
              <h3>{{ violationGroupTitle(group.key) }}</h3>
              <article
                v-for="violation in group.items"
                :key="violationKey(violation)"
                class="article-editor-view__violation"
              >
                <div class="article-editor-view__violation-head">
                  <a-tag :color="levelColor(violation.level)">
                    {{ levelLabel(violation.level) }}
                  </a-tag>
                  <a-tag>{{ sourceLabel(violation.source) }}</a-tag>
                  <strong>{{ violation.matched_text }}</strong>
                </div>
                <p v-if="violation.dictionary_name || violation.term_pattern">
                  {{ violation.dictionary_name || '词库规则' }}
                  <span v-if="violation.term_pattern"> · {{ violation.term_pattern }}</span>
                </p>
                <p v-if="violation.hint">{{ violation.hint }}</p>
                <div class="article-editor-view__violation-actions">
                  <a-button size="small" @click="focusViolation(violation)">
                    <template #icon><SearchOutlined /></template>
                    定位
                  </a-button>
                </div>
              </article>
            </section>
          </template>

          <a-empty v-else description="还没有检测结果" />
        </aside>
      </div>
    </template>

    <a-empty v-else :description="t('common.noData')" />

    <a-modal
      :open="leaveModalOpen"
      :title="t('article.editor.leaveConfirm.title')"
      :closable="false"
      :mask-closable="false"
      @cancel="handleStay"
    >
      <p class="article-editor-view__leave-copy">
        {{ t('article.editor.leaveConfirm.description') }}
      </p>

      <template #footer>
        <a-button @click="handleStay">
          {{ t('common.cancel') }}
        </a-button>
        <a-button @click="handleDiscardLeave">
          {{ t('article.editor.leaveConfirm.discard') }}
        </a-button>
        <a-button
          type="primary"
          :loading="saveMutation.isPending.value"
          :disabled="leaveSaveDisabled"
          @click="handleSaveAndLeave"
        >
          {{ t('article.editor.leaveConfirm.saveAndLeave') }}
        </a-button>
      </template>
    </a-modal>

    <PublishArticleModal
      v-model:open="publishModalOpen"
      :article-id="detail?.id ?? null"
      @published="handlePublished"
    />
  </div>
</template>

<style scoped>
.article-editor-view {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.article-editor-view__topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.article-editor-view__back {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #111827;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}

.article-editor-view__back :deep(.anticon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(144, 157, 181, 0.24);
}

.article-editor-view__actions {
  display: flex;
  gap: 12px;
}

.article-editor-view__main {
  height: calc(100vh - 180px);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(216, 225, 237, 0.9);
  border-radius: 28px;
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.05);
  backdrop-filter: blur(14px);
}

.article-editor-view__loading {
  padding: 24px;
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(216, 225, 237, 0.9);
  border-radius: 28px;
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.05);
  backdrop-filter: blur(14px);
}

.article-editor-view__alert {
  border-radius: 20px;
}

.article-editor-view__layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  min-width: 0;
}

.article-editor-view__layout--with-compliance {
  grid-template-columns: minmax(0, 1fr) minmax(360px, 420px);
  align-items: stretch;
}

.article-editor-view__compliance-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: calc(100vh - 180px);
  overflow-y: auto;
  padding: 18px;
  border: 1px solid rgba(216, 225, 237, 0.9);
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.05);
  backdrop-filter: blur(14px);
}

.article-editor-view__compliance-panel-head {
  position: sticky;
  top: -18px;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: -18px -18px 16px;
  padding: 16px 16px 14px 18px;
  border-bottom: 1px solid #eef2f8;
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(12px);
}

.article-editor-view__compliance-panel-head h2 {
  margin: 0;
  color: #111827;
  font-size: 16px;
  font-weight: 800;
}

.article-editor-view__leave-copy {
  margin: 0;
  color: #475467;
  line-height: 1.75;
}

.article-editor-view__compliance-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin: 16px 0 18px;
  color: #667085;
  font-size: 12px;
}

.article-editor-view__violation-group {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 18px;
}

.article-editor-view__violation-group h3 {
  margin: 0;
  color: #111827;
  font-size: 14px;
  font-weight: 700;
}

.article-editor-view__violation {
  padding: 14px;
  border: 1px solid #e6edf5;
  border-radius: 12px;
  background: #fbfcff;
}

.article-editor-view__violation-head {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.article-editor-view__violation-head strong {
  color: #111827;
}

.article-editor-view__violation p {
  margin: 10px 0 0;
  color: #667085;
  line-height: 1.65;
}

.article-editor-view__violation-actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.article-editor-view__main {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

@media (max-width: 1200px) {
  .article-editor-view__layout--with-compliance {
    grid-template-columns: minmax(0, 1fr) minmax(320px, 360px);
  }
}

@media (max-width: 960px) {
  .article-editor-view__layout--with-compliance {
    grid-template-columns: minmax(0, 1fr);
  }

  .article-editor-view__compliance-panel {
    height: min(560px, calc(100vh - 180px));
  }
}

@media (max-width: 768px) {
  .article-editor-view__topbar {
    flex-direction: column;
    align-items: stretch;
  }

  .article-editor-view__actions {
    width: 100%;
  }

  .article-editor-view__actions :deep(.ant-btn) {
    flex: 1;
  }
}
</style>
