<script setup lang="ts">
import { NButton, NInput, NModal, NSpin } from 'naive-ui'
import { computed, onBeforeUnmount, ref, watch } from 'vue'

import {
  deleteObject,
  getObjectContent,
  getObjectDetail,
  putObjectContent,
  renameObject,
} from '@/api/objectstorage'
import { ApiError } from '@/api/request'
import ConfirmPop from '@/components/ConfirmPop.vue'
import { extOf, previewKindOf } from '@/components/objectstorage/codeHighlight'
import { canFormat, formatSource } from '@/components/objectstorage/format'
import FileIcon from '@/components/objectstorage/FileIcon.vue'
import OfficePreview from '@/components/objectstorage/viewer/OfficePreview.vue'
import TextEditPane from '@/components/objectstorage/viewer/TextEditPane.vue'
import TextPreviewPane from '@/components/objectstorage/viewer/TextPreviewPane.vue'
import { fmtBytes, fmtTime } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import { useToast } from '@/composables/useToast'
import { useScopeStore } from '@/stores/scope'

/** 对象查看器:预览与编辑合一,左内容区 + 右信息栏。
 *  内容经面板中转直读直写(不签发 PAR);保存以 If-Match 做并发保护,
 *  412 冲突与无版本控制桶走二段确认。 */
const props = defineProps<{
  show: boolean
  cfgId: number | null
  region?: string
  bucket: string
  object: string
  /** 桶版本控制状态;未知按未开启保守处理 */
  versioningOn?: boolean
  /** 桶可见性(NoPublicAccess / ObjectRead);公共读时「复制 URI」变「复制 URL」 */
  visibility?: string
  /** 桶 namespace,拼公网对象 URL 用 */
  namespace?: string
}>()
const emit = defineEmits<{
  'update:show': [boolean]
  download: [string]
  share: [string]
  /** 保存/重命名/删除后,列表需要刷新 */
  changed: []
}>()

const message = useToast()

/** 文本预览/编辑体积上限;更大的文本请下载查看 */
const TEXT_MAX = 1024 * 1024
/** 图片与 office/pdf 的中转上限(与后端 20MB 限制一致) */
const BINARY_MAX = 20 * 1024 * 1024
/** pre 渲染字符上限,超出截断展示 */
const TEXT_SHOW_MAX = 256 * 1024

const detail = useAsync(async () => {
  if (!props.cfgId || !props.object) return null
  return getObjectDetail(props.cfgId, props.bucket, props.object, props.region)
}, false)

const ext = computed(() => extOf(props.object))
const kind = computed(() => previewKindOf(props.object))
const archived = computed(() => {
  const d = detail.data.value
  return d?.storageTier === 'Archive' && d.archivalState !== 'Restored'
})
const size = computed(() => detail.data.value?.size ?? 0)
const tooBig = computed(() =>
  kind.value === 'text' ? size.value > TEXT_MAX : size.value > BINARY_MAX,
)
const canEdit = computed(() => kind.value === 'text' && !archived.value && !tooBig.value)
const officeKind = computed(() => {
  const k = kind.value
  return k === 'docx' || k === 'xlsx' || k === 'pdf' || k === 'pptx' ? k : null
})
const blockedReason = computed(() => {
  if (kind.value === 'none') return '该格式暂不支持预览,请下载查看'
  if (archived.value) return 'Archive 对象需先恢复才能预览'
  if (tooBig.value)
    return kind.value === 'text' ? '文本超过 1 MB,请下载查看' : '文件超过 20 MB,请下载查看'
  return ''
})

// ---- 内容状态 ----
const mode = ref<'preview' | 'edit'>('preview')
const loading = ref(false)
const loadError = ref('')
/** 文本完整内容(编辑基线,不做美化改写) */
const fullText = ref('')
/** 展示文本(json 美化、超长截断) */
const previewText = ref('')
const truncated = ref(false)
const imageUrl = ref('')
const officeData = ref<ArrayBuffer | null>(null)
const officeError = ref('')
const contentEtag = ref('')
const contentType = ref('')

watch(
  () => [props.show, props.object],
  ([show]) => {
    if (!show) {
      revokeImage()
      return
    }
    reset()
    void detail.run().then(() => {
      if (!blockedReason.value) void loadContent()
    })
  },
)
onBeforeUnmount(revokeImage)

function reset() {
  mode.value = 'preview'
  // 立即清掉上一对象的元数据,右栏转入加载态,避免残留展示
  detail.data.value = null
  loading.value = false
  loadError.value = ''
  fullText.value = ''
  previewText.value = ''
  truncated.value = false
  revokeImage()
  officeData.value = null
  officeError.value = ''
  contentEtag.value = ''
  contentType.value = ''
  renaming.value = false
  resetEdit()
}

function revokeImage() {
  if (imageUrl.value) URL.revokeObjectURL(imageUrl.value)
  imageUrl.value = ''
}

async function loadContent() {
  if (!props.cfgId || loading.value) return
  loading.value = true
  loadError.value = ''
  try {
    const c = await getObjectContent(props.cfgId, props.bucket, props.object, props.region)
    contentEtag.value = c.etag
    contentType.value = c.contentType
    await applyContent(c.blob)
  } catch (e) {
    loadError.value = e instanceof Error ? e.message : '内容加载失败'
  } finally {
    loading.value = false
  }
}

async function applyContent(blob: Blob) {
  if (kind.value === 'image') {
    imageUrl.value = URL.createObjectURL(blob)
    return
  }
  if (kind.value === 'text') {
    setText(await blob.text())
    return
  }
  officeData.value = await blob.arrayBuffer()
}

function setText(raw: string) {
  fullText.value = raw
  const pretty = ext.value === 'json' ? prettyJson(raw) : raw
  truncated.value = pretty.length > TEXT_SHOW_MAX
  previewText.value = truncated.value ? pretty.slice(0, TEXT_SHOW_MAX) : pretty
}

/** JSON 美化失败时原样展示 */
function prettyJson(s: string): string {
  try {
    return JSON.stringify(JSON.parse(s), null, 2)
  } catch {
    return s
  }
}

// ---- 编辑 ----
const editText = ref('')
const saving = ref(false)
/** 非空时保存进入二段确认态,内容为需用户确认的原因 */
const pendingConfirm = ref('')
/** 412 冲突确认后置真:下次保存不带 If-Match,无条件覆盖 */
const overwrite = ref(false)
const editByteSize = computed(() => new Blob([editText.value]).size)

function resetEdit() {
  editText.value = ''
  saving.value = false
  pendingConfirm.value = ''
  overwrite.value = false
}

function startEdit() {
  editText.value = fullText.value
  pendingConfirm.value = ''
  overwrite.value = false
  mode.value = 'edit'
}

function cancelEdit() {
  mode.value = 'preview'
  resetEdit()
}

const canBeautify = computed(() => canFormat(ext.value))

/** 一键美化:json 原生,其余 prettier standalone(动态加载);语法错误只提示不改动 */
async function beautify() {
  try {
    const out = await formatSource(editText.value, ext.value)
    if (out === null) return
    if (out === editText.value) {
      message.success('已是规范格式')
      return
    }
    editText.value = out
    message.success('已格式化')
  } catch {
    message.error('格式化失败:内容存在语法错误')
  }
}

async function save() {
  if (!props.cfgId || saving.value) return
  if (!pendingConfirm.value && !props.versioningOn) {
    pendingConfirm.value = '该桶未开启版本控制,保存将直接覆盖原内容,不可恢复。'
    return
  }
  saving.value = true
  try {
    const { etag } = await putObjectContent(
      props.cfgId,
      props.bucket,
      props.object,
      editText.value,
      contentType.value || 'text/plain',
      overwrite.value ? undefined : contentEtag.value || undefined,
      props.region,
    )
    applySaved(etag)
  } catch (e) {
    onSaveError(e)
  } finally {
    saving.value = false
  }
}

function applySaved(etag: string) {
  contentEtag.value = etag
  setText(editText.value)
  message.success('已保存')
  mode.value = 'preview'
  resetEdit()
  emit('changed')
  void detail.run({ silent: true })
}

function onSaveError(e: unknown) {
  if (e instanceof ApiError && e.status === 412) {
    overwrite.value = true
    pendingConfirm.value = '对象在编辑期间已被修改(ETag 变化),继续保存将覆盖他人的更改。'
    return
  }
  message.error(e instanceof Error ? e.message : '保存失败')
}

// ---- 重命名 / 删除 / 复制 ----
const renaming = ref(false)
const newName = ref('')
const renameBusy = ref(false)

function startRename() {
  newName.value = props.object
  renaming.value = true
}

async function doRename() {
  if (!props.cfgId || !newName.value.trim() || newName.value === props.object) return
  renameBusy.value = true
  try {
    await renameObject(props.cfgId, props.bucket, props.object, newName.value.trim(), props.region)
    message.success('已重命名')
    emit('update:show', false)
    emit('changed')
  } catch (e) {
    message.error(e instanceof Error ? e.message : '重命名失败')
  } finally {
    renameBusy.value = false
  }
}

async function doDelete() {
  if (!props.cfgId) return
  try {
    await deleteObject(props.cfgId, props.bucket, props.object, props.region)
    message.success('对象已删除')
    emit('update:show', false)
    emit('changed')
  } catch (e) {
    message.error(e instanceof Error ? e.message : '删除失败')
  }
}

/** Archive 层(任意恢复态):PAR 对其无效,分享一律禁用 */
const isArchiveTier = computed(() => detail.data.value?.storageTier === 'Archive')
/** 公共读桶:对象有公网直链,「复制 URI」升级为「复制 URL」 */
const isPublicBucket = computed(() => props.visibility === 'ObjectRead')

const scope = useScopeStore()

/** 公共读桶对象的公网直链;对象名整体转义(/ → %2F,OCI 控制台同款) */
function publicUrl(): string {
  const region = props.region || scope.region
  return `https://objectstorage.${region}.oraclecloud.com/n/${props.namespace}/b/${props.bucket}/o/${encodeURIComponent(props.object)}`
}

async function copyUri() {
  const pub = isPublicBucket.value
  await navigator.clipboard.writeText(pub ? publicUrl() : `oci://${props.bucket}/${props.object}`)
  message.success(pub ? 'URL 已复制' : 'URI 已复制')
}

const kvRows = computed(() => {
  const d = detail.data.value
  if (!d) return []
  return [
    { k: '大小', v: `${fmtBytes(d.size)}(${d.size.toLocaleString()} B)` },
    { k: 'Content-Type', v: d.contentType || '—', mono: true },
    { k: 'ETag', v: d.etag || '—', mono: true },
    { k: 'MD5', v: d.contentMd5 || '—', mono: true },
    { k: '存储层', v: d.storageTier || 'Standard' },
    { k: '版本控制', v: props.versioningOn ? '已开启' : '未开启' },
    { k: '修改时间', v: fmtTime(d.timeModified) },
  ]
})
</script>

<template>
  <NModal
    :show="show"
    preset="card"
    :mask-closable="mode !== 'edit'"
    :style="{ width: '1080px', maxWidth: 'calc(100vw - 24px)' }"
    @update:show="emit('update:show', $event)"
  >
    <template #header>
      <span class="mono flex min-w-0 items-center gap-2 text-[13.5px]">
        <FileIcon :name="object" :size="18" />
        <span class="truncate" :title="object">{{ object }}</span>
        <span
          v-if="mode === 'edit'"
          class="flex-none rounded bg-wash px-1.5 py-px text-[10.5px] font-medium text-ink-3"
        >
          编辑
        </span>
      </span>
    </template>

    <div class="flex flex-col gap-4 md:flex-row">
      <!-- 左:内容区 -->
      <div
        class="flex h-[65vh] min-w-0 flex-1 flex-col rounded-lg border border-line-soft bg-wash/50 p-3"
      >
        <div
          v-if="detail.loading.value || loading"
          class="flex flex-1 items-center justify-center"
        >
          <NSpin size="small" />
        </div>
        <div
          v-else-if="blockedReason"
          class="flex flex-1 items-center justify-center text-xs text-ink-3"
        >
          {{ blockedReason }}
        </div>
        <div v-else-if="loadError" class="flex flex-1 flex-col items-center justify-center gap-2">
          <div class="text-xs text-err">{{ loadError }}</div>
          <NButton size="small" @click="loadContent">重试</NButton>
        </div>
        <template v-else-if="mode === 'edit'">
          <div
            class="relative min-h-0 flex-1 overflow-hidden rounded-lg border border-line bg-white transition-colors focus-within:border-accent"
          >
            <TextEditPane v-model="editText" :object-name="object" :disabled="saving" />
          </div>
          <div class="mt-1.5 flex-none text-xs text-ink-3">
            UTF-8 文本 · {{ fmtBytes(editByteSize) }} · 保存经面板中转写回,If-Match 防并发覆盖
          </div>
        </template>
        <img
          v-else-if="imageUrl"
          :src="imageUrl"
          :alt="object"
          class="mx-auto max-h-full min-h-0 rounded object-contain"
          @error="loadError = '图片加载失败,请下载查看'; revokeImage()"
        />
        <TextPreviewPane
          v-else-if="kind === 'text' && (previewText || truncated || !loading)"
          :text="previewText"
          :ext="ext"
          :truncated="truncated"
        />
        <template v-else-if="officeData && officeKind">
          <div
            v-if="officeError"
            class="flex flex-1 items-center justify-center text-xs text-err"
          >
            {{ officeError }}
          </div>
          <div v-else class="min-h-0 flex-1 overflow-auto rounded bg-white">
            <OfficePreview :kind="officeKind" :data="officeData" @error="officeError = $event" />
          </div>
          <div v-if="officeKind === 'pptx' && !officeError" class="mt-1.5 flex-none text-xs text-ink-3">
            pptx 预览为实验能力,复杂版式可能失真
          </div>
        </template>
      </div>

      <!-- 右:信息与操作 -->
      <div class="flex w-full flex-none flex-col gap-3 md:w-72">
        <div v-if="detail.loading.value" class="flex justify-center py-10">
          <NSpin size="small" />
        </div>
        <template v-else>
        <div class="flex flex-col gap-0.5">
          <div
            v-for="row in kvRows"
            :key="row.k"
            class="flex justify-between gap-3 border-b border-line-soft py-1.5 text-[12px]"
          >
            <span class="flex-none text-ink-3">{{ row.k }}</span>
            <span class="min-w-0 text-right break-all" :class="row.mono ? 'mono' : ''">
              {{ row.v }}
            </span>
          </div>
        </div>

        <template v-if="mode === 'edit'">
          <div
            v-if="pendingConfirm"
            class="rounded-lg bg-warn/12 px-3 py-2 text-xs leading-relaxed text-warn"
          >
            {{ pendingConfirm }}
          </div>
          <NButton v-if="canBeautify" size="small" :disabled="saving" @click="beautify">
            美化格式
          </NButton>
          <div class="flex gap-2">
            <NButton
              size="small"
              type="primary"
              class="flex-1"
              :loading="saving"
              @click="save"
            >
              {{ pendingConfirm ? '确认覆盖并保存' : '保存' }}
            </NButton>
            <NButton size="small" :disabled="saving" @click="cancelEdit">取消</NButton>
          </div>
        </template>

        <div v-else-if="renaming" class="flex flex-col gap-2">
          <NInput v-model:value="newName" size="small" class="mono" />
          <div class="flex gap-2">
            <NButton size="small" type="primary" :loading="renameBusy" @click="doRename">
              确定
            </NButton>
            <NButton size="small" :disabled="renameBusy" @click="renaming = false">取消</NButton>
          </div>
        </div>

        <template v-else>
          <div :title="archived ? 'Archive 对象需先恢复才能下载' : ''">
            <NButton
              size="small"
              type="primary"
              block
              :disabled="archived"
              @click="emit('download', object)"
            >
              下载
            </NButton>
          </div>
          <div class="grid grid-cols-2 gap-1.5">
            <NButton v-if="canEdit" size="small" @click="startEdit">编辑</NButton>
            <div :title="isArchiveTier ? 'Archive 层对象不支持预签名分享' : ''">
              <NButton size="small" block :disabled="isArchiveTier" @click="emit('share', object)">
                分享
              </NButton>
            </div>
            <NButton size="small" @click="copyUri">
              {{ isPublicBucket ? '复制 URL' : '复制 URI' }}
            </NButton>
            <NButton size="small" @click="startRename">重命名</NButton>
          </div>
          <ConfirmPop
            title="删除该对象？"
            content="未开版本控制时不可恢复。"
            confirm-text="删除"
            :on-confirm="doDelete"
          >
            <template #trigger>
              <NButton size="small" type="error" ghost block>删除</NButton>
            </template>
          </ConfirmPop>
        </template>
        </template>
      </div>
    </div>
  </NModal>
</template>
