<script setup lang="ts">
import { CloseOutlined, CloudUploadOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

import ImageLibraryPicker from '@/components/ImageLibraryPicker.vue'
import { articlesApi } from '@/lib/api'
import {
  deriveCoverFileName,
  getPrimaryCoverRequirement,
  type PlatformCoverRequirement,
} from '@/lib/cover-requirements'
import { formatError } from '@/lib/errors'
import {
  IMAGE_UPLOAD_ACCEPT,
  useImageUploadProgress,
  validateImageUploadFile,
} from '@/lib/image-webp'
import type { ImageAssetItem } from '@geo/shared-types'

type PickerUploadResult = {
  url: string
  fileName?: string | null
  assetId?: number | null
}

const props = defineProps<{
  open: boolean
  articleId: number | null
  platformIds: string[]
  currentUrl?: string | null
  currentFileName?: string | null
  currentAssetId?: number | null
  title?: string
  localTabLabel?: string
  emptyUploadTitle?: string
  emptyUploadHint?: string
  uploadHandler?: (file: File) => Promise<PickerUploadResult>
}>()

const emit = defineEmits<{
  'update:open': [value: boolean]
  confirmed: [payload: { url: string; fileName: string; assetId?: number | null }]
}>()

const { t } = useI18n()
const imageUploadProgress = useImageUploadProgress()

const fileInput = ref<HTMLInputElement | null>(null)
const activeTab = ref('local')
const workingUrl = ref('')
const workingFileName = ref('')
const sourceKind = ref<'remote' | 'local' | null>(null)
const naturalWidth = ref(0)
const naturalHeight = ref(0)
const zoom = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)
const confirmLoading = ref(false)
const transformDirty = ref(false)
const localObjectUrl = ref('')
const localFile = ref<File | null>(null)
const selectedLibraryImageId = ref<number | null>(null)

const dragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const dragOriginX = ref(0)
const dragOriginY = ref(0)
const imageLoadToken = ref(0)

const primaryRequirement = computed(() => getPrimaryCoverRequirement(props.platformIds))
const aspectRatioLocked = computed(() => Boolean(primaryRequirement.value.aspectRatio))
const modalTitle = computed(() => props.title?.trim() || t('coverPicker.title'))
const resolvedLocalTabLabel = computed(
  () => props.localTabLabel?.trim() || t('coverPicker.tabs.local'),
)
const resolvedEmptyUploadTitle = computed(
  () => props.emptyUploadTitle?.trim() || t('coverPicker.dropzoneTitle'),
)
const resolvedEmptyUploadHint = computed(
  () => props.emptyUploadHint?.trim() || t('coverPicker.dropzoneHint'),
)
const localTabLabelDisplay = computed(() =>
  workingUrl.value ? `${resolvedLocalTabLabel.value} (1)` : resolvedLocalTabLabel.value,
)
const hasImage = computed(() => Boolean(workingUrl.value))
const imageUploadBusy = computed(() => imageUploadProgress.visible)
const stageSize = computed(() => {
  const ratio =
    primaryRequirement.value.aspectRatio ||
    (naturalWidth.value > 0 && naturalHeight.value > 0
      ? naturalWidth.value / naturalHeight.value
      : 4 / 3)
  const maxWidth = 720
  const maxHeight = 420

  let width = maxWidth
  let height = width / ratio

  if (height > maxHeight) {
    height = maxHeight
    width = height * ratio
  }

  return {
    width: Math.round(width),
    height: Math.round(height),
  }
})

const displayMetrics = computed(() => {
  if (!naturalWidth.value || !naturalHeight.value) {
    return {
      width: 0,
      height: 0,
      baseScale: 1,
    }
  }

  const baseScale = Math.max(
    stageSize.value.width / naturalWidth.value,
    stageSize.value.height / naturalHeight.value,
  )
  const scaled = baseScale * zoom.value

  return {
    width: naturalWidth.value * scaled,
    height: naturalHeight.value * scaled,
    baseScale,
  }
})

const stageImageStyle = computed(() => ({
  width: `${displayMetrics.value.width}px`,
  height: `${displayMetrics.value.height}px`,
  transform: `translate(calc(-50% + ${offsetX.value}px), calc(-50% + ${offsetY.value}px))`,
}))

const stageFrameStyle = computed(() => ({
  width: `${stageSize.value.width}px`,
  height: `${stageSize.value.height}px`,
}))

watch(
  () => props.open,
  (open) => {
    if (open) {
      hydrateFromProps()
      return
    }
    stopDragging()
  },
  { immediate: true },
)

watch(
  () => [props.currentUrl, props.currentFileName],
  () => {
    if (props.open && sourceKind.value !== 'local') {
      hydrateFromProps()
    }
  },
)

watch(
  () => props.platformIds.join(','),
  () => {
    if (!props.open || !hasImage.value) {
      return
    }
    resetTransform()
  },
)

watch(
  () => [
    zoom.value,
    naturalWidth.value,
    naturalHeight.value,
    stageSize.value.width,
    stageSize.value.height,
  ],
  () => {
    clampOffsets()
  },
)

onBeforeUnmount(() => {
  stopDragging()
  revokeLocalObjectUrl()
})

function hydrateFromProps(): void {
  activeTab.value = 'local'
  transformDirty.value = false
  const nextUrl = String(props.currentUrl ?? '').trim()
  const nextFileName = String(props.currentFileName ?? '').trim() || deriveCoverFileName(nextUrl)

  workingUrl.value = nextUrl
  workingFileName.value = nextFileName
  sourceKind.value = nextUrl ? 'remote' : null
  localFile.value = null
  selectedLibraryImageId.value = props.currentAssetId ?? null

  if (!nextUrl) {
    resetStage()
    return
  }

  void loadImageMeta(nextUrl)
}

function resetStage(): void {
  naturalWidth.value = 0
  naturalHeight.value = 0
  resetTransform()
}

function resetTransform(): void {
  zoom.value = 1
  offsetX.value = 0
  offsetY.value = 0
  transformDirty.value = false
}

function clampOffsets(): void {
  const maxX = Math.max(0, (displayMetrics.value.width - stageSize.value.width) / 2)
  const maxY = Math.max(0, (displayMetrics.value.height - stageSize.value.height) / 2)

  offsetX.value = clamp(offsetX.value, -maxX, maxX)
  offsetY.value = clamp(offsetY.value, -maxY, maxY)
}

function markTransformDirty(): void {
  transformDirty.value = zoom.value !== 1 || offsetX.value !== 0 || offsetY.value !== 0
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max)
}

async function loadImageMeta(url: string): Promise<void> {
  const token = imageLoadToken.value + 1
  imageLoadToken.value = token

  try {
    const image = await loadImageElement(url)
    if (token !== imageLoadToken.value) {
      return
    }

    naturalWidth.value = image.naturalWidth
    naturalHeight.value = image.naturalHeight
    resetTransform()
  } catch {
    if (token !== imageLoadToken.value) {
      return
    }

    resetStage()
    message.warning(t('coverPicker.messages.imageLoadFailed'))
  }
}

function openFileDialog(): void {
  if (imageUploadBusy.value) {
    return
  }
  fileInput.value?.click()
}

function handleFileChange(event: Event): void {
  if (imageUploadBusy.value) {
    ;(event.target as HTMLInputElement).value = ''
    return
  }

  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  input.value = ''

  if (!file) {
    return
  }

  try {
    validateImageUploadFile(file)
  } catch (error) {
    message.warning(formatError(error))
    return
  }

  revokeLocalObjectUrl()
  const objectUrl = URL.createObjectURL(file)
  localObjectUrl.value = objectUrl
  workingUrl.value = objectUrl
  workingFileName.value = file.name
  sourceKind.value = 'local'
  localFile.value = file
  selectedLibraryImageId.value = null
  transformDirty.value = false
  void loadImageMeta(objectUrl)
}

function revokeLocalObjectUrl(): void {
  if (!localObjectUrl.value) {
    return
  }
  URL.revokeObjectURL(localObjectUrl.value)
  localObjectUrl.value = ''
}

function handleStageMouseDown(event: MouseEvent): void {
  if (!hasImage.value || confirmLoading.value) {
    return
  }

  dragging.value = true
  dragStartX.value = event.clientX
  dragStartY.value = event.clientY
  dragOriginX.value = offsetX.value
  dragOriginY.value = offsetY.value
  window.addEventListener('mousemove', handleWindowMouseMove)
  window.addEventListener('mouseup', handleWindowMouseUp)
}

function handleWindowMouseMove(event: MouseEvent): void {
  if (!dragging.value) {
    return
  }

  offsetX.value = dragOriginX.value + event.clientX - dragStartX.value
  offsetY.value = dragOriginY.value + event.clientY - dragStartY.value
  clampOffsets()
  markTransformDirty()
}

function handleWindowMouseUp(): void {
  stopDragging()
}

function stopDragging(): void {
  if (!dragging.value) {
    return
  }

  dragging.value = false
  window.removeEventListener('mousemove', handleWindowMouseMove)
  window.removeEventListener('mouseup', handleWindowMouseUp)
}

function handleZoomChange(value: number | [number, number]): void {
  const nextValue = Array.isArray(value) ? value[0] : value
  zoom.value = clamp(nextValue, 1, 2.4)
  clampOffsets()
  markTransformDirty()
}

function handleClear(): void {
  if (confirmLoading.value) {
    return
  }

  workingUrl.value = ''
  workingFileName.value = ''
  sourceKind.value = null
  localFile.value = null
  selectedLibraryImageId.value = null
  transformDirty.value = false
  resetStage()
  revokeLocalObjectUrl()
}

function handleLibrarySelect(image: ImageAssetItem): void {
  activeTab.value = 'local'
  workingUrl.value = image.url
  workingFileName.value = image.name
  sourceKind.value = 'remote'
  selectedLibraryImageId.value = image.id
  localFile.value = null
  transformDirty.value = false
  void loadImageMeta(image.url)
}

async function handleConfirm(): Promise<void> {
  if (!workingUrl.value) {
    message.warning(t('coverPicker.messages.missingImage'))
    return
  }

  if (!props.uploadHandler && !props.articleId) {
    message.error(t('coverPicker.messages.missingArticle'))
    return
  }

  if (sourceKind.value !== 'local' && !transformDirty.value && !aspectRatioLocked.value) {
    emit('confirmed', {
      url: workingUrl.value,
      fileName: workingFileName.value || deriveCoverFileName(workingUrl.value) || 'cover.png',
      assetId: selectedLibraryImageId.value,
    })
    emit('update:open', false)
    return
  }

  confirmLoading.value = true
  try {
    if (
      sourceKind.value === 'local' &&
      !transformDirty.value &&
      !aspectRatioLocked.value &&
      localFile.value
    ) {
      const fallbackFileName =
        localFile.value.name || deriveCoverFileName(workingUrl.value) || 'cover.webp'
      const uploaded = await uploadPickedFile(localFile.value, fallbackFileName)
      emit('confirmed', {
        url: uploaded.url,
        fileName: uploaded.fileName,
        assetId: uploaded.assetId,
      })
      emit('update:open', false)
      return
    }

    const blob = await renderCroppedBlob(primaryRequirement.value)
    const fileName = ensureImageExtension(
      workingFileName.value || deriveCoverFileName(workingUrl.value) || 'cover.png',
      blob.type,
    )
    const file = new File([blob], fileName, { type: blob.type })
    const uploaded = await uploadPickedFile(file, fileName)

    emit('confirmed', {
      url: uploaded.url,
      fileName: uploaded.fileName,
      assetId: uploaded.assetId,
    })
    emit('update:open', false)
  } catch (error) {
    message.error(formatError(error))
  } finally {
    confirmLoading.value = false
  }
}

async function uploadPickedFile(
  file: File,
  fallbackFileName: string,
): Promise<{ url: string; fileName: string; assetId?: number | null }> {
  if (props.uploadHandler) {
    const uploaded = await props.uploadHandler(file)
    return {
      url: uploaded.url,
      fileName: String(uploaded.fileName ?? '').trim() || fallbackFileName,
      assetId: uploaded.assetId ?? null,
    }
  }

  if (!props.articleId) {
    throw new Error(t('coverPicker.messages.missingArticle'))
  }

  const uploaded = await articlesApi.uploadImage(props.articleId, file)
  return {
    url: uploaded.url,
    fileName: fallbackFileName,
    assetId: null,
  }
}

async function renderCroppedBlob(requirement: PlatformCoverRequirement): Promise<Blob> {
  const image = await loadImageElement(workingUrl.value)
  const canvas = document.createElement('canvas')
  canvas.width = requirement.outputWidth ?? Math.max(Math.round(stageSize.value.width * 2), 1)
  canvas.height = requirement.outputHeight ?? Math.max(Math.round(stageSize.value.height * 2), 1)

  const context = canvas.getContext('2d')
  if (!context) {
    throw new Error('article_image_upload_failed')
  }

  const baseScale = Math.max(
    stageSize.value.width / image.naturalWidth,
    stageSize.value.height / image.naturalHeight,
  )
  const renderedWidth = image.naturalWidth * baseScale * zoom.value
  const renderedHeight = image.naturalHeight * baseScale * zoom.value
  const outputScale = canvas.width / stageSize.value.width

  context.fillStyle = '#ffffff'
  context.fillRect(0, 0, canvas.width, canvas.height)
  context.drawImage(
    image,
    (stageSize.value.width / 2 - renderedWidth / 2 + offsetX.value) * outputScale,
    (stageSize.value.height / 2 - renderedHeight / 2 + offsetY.value) * outputScale,
    renderedWidth * outputScale,
    renderedHeight * outputScale,
  )

  const blob = await new Promise<Blob | null>((resolve) => {
    canvas.toBlob(resolve, 'image/png', 0.92)
  })

  if (!blob) {
    throw new Error('article_image_upload_failed')
  }

  return blob
}

function ensureImageExtension(fileName: string, mimeType: string): string {
  const trimmed = fileName.trim()
  const normalizedMime = mimeType.toLowerCase()
  const extension = normalizedMime.includes('webp')
    ? 'webp'
    : normalizedMime.includes('gif')
      ? 'gif'
      : normalizedMime.includes('jpeg')
        ? 'jpg'
        : 'png'

  if (/\.(png|jpe?g|gif|webp)$/i.test(trimmed)) {
    return trimmed
  }

  return `${trimmed || 'cover'}.${extension}`
}

function closeModal(): void {
  emit('update:open', false)
}

function loadImageElement(url: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const image = new Image()
    image.onload = () => resolve(image)
    image.onerror = () => reject(new Error('article_image_upload_failed'))
    image.src = url
  })
}
</script>

<template>
  <a-modal
    :open="open"
    :width="900"
    :confirm-loading="confirmLoading"
    :ok-text="t('common.confirm')"
    :cancel-text="t('common.cancel')"
    :title="modalTitle"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <div class="cover-picker">
      <input
        ref="fileInput"
        type="file"
        :accept="IMAGE_UPLOAD_ACCEPT"
        :disabled="imageUploadBusy"
        hidden
        @change="handleFileChange"
      />

      <a-tabs v-model:activeKey="activeTab" class="cover-picker__tabs">
        <a-tab-pane key="local" :tab="localTabLabelDisplay">
          <div class="cover-picker__body">
            <section class="cover-picker__workspace">
              <div class="cover-picker__stage-panel">
                <div
                  class="cover-picker__stage-frame"
                  :class="{ 'cover-picker__stage-frame--dragging': dragging }"
                  :style="stageFrameStyle"
                  @mousedown.prevent="handleStageMouseDown"
                >
                  <template v-if="hasImage">
                    <img
                      class="cover-picker__stage-image"
                      :src="workingUrl"
                      :style="stageImageStyle"
                      alt="cover preview"
                      draggable="false"
                    />
                    <div class="cover-picker__stage-outline"></div>
                  </template>

                  <button
                    v-else
                    class="cover-picker__stage-empty"
                    type="button"
                    :disabled="imageUploadBusy"
                    @click="openFileDialog"
                  >
                    <span class="cover-picker__stage-empty-icon">
                      <CloudUploadOutlined />
                    </span>
                    <strong>{{ resolvedEmptyUploadTitle }}</strong>
                    <small>{{ resolvedEmptyUploadHint }}</small>
                  </button>
                </div>
              </div>

              <div v-if="hasImage" class="cover-picker__controls">
                <span>{{ t('coverPicker.zoom') }}</span>
                <a-slider
                  class="cover-picker__slider"
                  :min="1"
                  :max="2.4"
                  :step="0.05"
                  :value="zoom"
                  :disabled="!hasImage"
                  @change="handleZoomChange"
                />
                <strong>{{ Math.round(zoom * 100) }}%</strong>
                <a-button size="small" style="margin-left: 16px" @click="resetTransform">
                  {{ t('coverPicker.actions.reset') }}
                </a-button>
              </div>

              <div v-if="hasImage" class="cover-picker__thumbs">
                <button
                  class="cover-picker__upload-tile"
                  type="button"
                  :disabled="imageUploadBusy"
                  @click="openFileDialog"
                >
                  <span class="cover-picker__upload-tile-icon">
                    <PlusOutlined />
                  </span>
                  <span>{{ t('coverPicker.actions.upload') }}</span>
                </button>

                <div v-if="hasImage" class="cover-picker__thumb cover-picker__thumb--active">
                  <img :src="workingUrl" alt="cover thumbnail" />
                  <button type="button" class="cover-picker__thumb-close" @click="handleClear">
                    <CloseOutlined />
                  </button>
                </div>
              </div>
            </section>
          </div>
        </a-tab-pane>

        <a-tab-pane key="library" :tab="t('images.libraryTab')">
          <div class="cover-picker__library">
            <ImageLibraryPicker @select="handleLibrarySelect" />
          </div>
        </a-tab-pane>
      </a-tabs>
    </div>
  </a-modal>
</template>

<style scoped>
.cover-picker {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.cover-picker__tabs :deep(.ant-tabs-nav) {
  margin-bottom: 20px;
}

.cover-picker__body {
  display: block;
}

.cover-picker__workspace,
.cover-picker__library {
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

.cover-picker__workspace {
  padding: 0;
}

.cover-picker__library {
  padding: 0;
  height: 520px;
}

.cover-picker__stage-panel {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0;
  margin-top: 0;
  border-radius: 4px;
  background: #f7f8fa;
  min-height: 420px;
}

.cover-picker__stage-frame {
  position: relative;
  overflow: hidden;
  border-radius: 4px;
  background: transparent;
  box-shadow: none;
  cursor: grab;
  user-select: none;
}

.cover-picker__stage-frame--dragging {
  cursor: grabbing;
}

.cover-picker__stage-image {
  position: absolute;
  top: 50%;
  left: 50%;
  max-width: none;
  object-fit: cover;
  will-change: transform;
}

.cover-picker__stage-outline {
  position: absolute;
  inset: 0;
  border: 3px solid #3b5bff;
  pointer-events: none;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.65),
    0 0 0 999px rgba(255, 255, 255, 0.22);
}

.cover-picker__stage-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  height: 100%;
  border: 0;
  background: transparent;
  color: #475467;
  cursor: pointer;
}

.cover-picker__stage-empty:disabled,
.cover-picker__upload-tile:disabled {
  opacity: 0.56;
  cursor: not-allowed;
}

.cover-picker__ai-placeholder strong,
.cover-picker__stage-empty strong {
  margin: 0;
  color: #111827;
  font-size: 16px;
  font-weight: 800;
}

.cover-picker__ai-placeholder p,
.cover-picker__stage-empty small {
  font-size: 13px;
  color: #667085;
}

.cover-picker__stage-empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  border-radius: 999px;
  background: #e9ecef;
  color: #fff;
  font-size: 40px;
  margin-bottom: 8px;
}

.cover-picker__upload-tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 999px;
  background: rgba(53, 93, 255, 0.12);
  color: #355dff;
  font-size: 24px;
}

.cover-picker__controls {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 18px;
  color: #475467;
  font-size: 13px;
}

.cover-picker__slider {
  flex: 1;
  margin: 0;
}

.cover-picker__thumbs {
  display: flex;
  align-items: stretch;
  gap: 14px;
  margin-top: 20px;
  min-height: 124px;
}

.cover-picker__upload-tile,
.cover-picker__thumb {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  width: 124px;
  height: 124px;
  padding: 14px;
  border-radius: 20px;
  border: 1px solid rgba(219, 228, 242, 0.92);
  background: #fff;
  color: #475467;
}

.cover-picker__upload-tile {
  align-items: center;
  cursor: pointer;
}

.cover-picker__thumb {
  position: relative;
  align-items: center;
  box-shadow: inset 0 0 0 2px rgba(53, 93, 255, 0.88);
}

.cover-picker__thumb img {
  width: 100%;
  height: 100%;
  border-radius: 12px;
  object-fit: cover;
}

.cover-picker__thumb-close {
  position: absolute;
  top: -8px;
  right: -8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, 0.45);
  color: #ffffff !important;
  border: none;
  cursor: pointer;
  font-size: 11px;
  backdrop-filter: blur(4px);
  transition:
    background-color 0.2s,
    transform 0.2s;
  z-index: 10;
}

.cover-picker__thumb-close :deep(.anticon),
.cover-picker__thumb-close :deep(svg) {
  color: #ffffff !important;
  fill: #ffffff !important;
}

.cover-picker__thumb-close:hover {
  background-color: rgba(0, 0, 0, 0.65);
  transform: scale(1.05);
}

@media (max-width: 960px) {
  .cover-picker__thumbs {
    flex-wrap: wrap;
  }

  .cover-picker__remove {
    margin-left: 0;
  }
}
</style>
