<script setup lang="ts">
import { nextTick, onBeforeUnmount, reactive, ref, useId, watch } from 'vue'

type ConfirmDialogTone = 'primary' | 'warning' | 'danger'

interface ConfirmDialogOptions {
  title: string
  message: string
  confirmText?: string
  cancelText?: string
  tone?: ConfirmDialogTone
  closeOnOverlay?: boolean
}

const visible = ref(false)
const dialogRef = ref<HTMLElement>()
const cancelButtonRef = ref<HTMLButtonElement>()
const titleId = `app-confirm-title-${useId()}`
const messageId = `app-confirm-message-${useId()}`
const options = reactive<Required<ConfirmDialogOptions>>({
  title: '请确认操作',
  message: '',
  confirmText: '确定',
  cancelText: '取消',
  tone: 'warning',
  closeOnOverlay: false,
})

let resolver: ((confirmed: boolean) => void) | null = null
let previousFocus: HTMLElement | null = null

function settle(confirmed: boolean) {
  visible.value = false
  const currentResolver = resolver
  resolver = null
  currentResolver?.(confirmed)
}

function open(nextOptions: ConfirmDialogOptions) {
  if (resolver) settle(false)
  Object.assign(options, {
    confirmText: '确定',
    cancelText: '取消',
    tone: 'warning',
    closeOnOverlay: false,
    ...nextOptions,
  })
  visible.value = true
  return new Promise<boolean>((resolve) => {
    resolver = resolve
  })
}

function handleOverlayClick() {
  if (options.closeOnOverlay) settle(false)
}

function handleKeydown(event: KeyboardEvent) {
  if (event.key === 'Escape') {
    event.preventDefault()
    settle(false)
    return
  }
  if (event.key !== 'Tab') return

  const focusableElements = Array.from(
    dialogRef.value?.querySelectorAll<HTMLElement>('button:not([disabled])') ?? [],
  )
  const firstElement = focusableElements[0]
  const lastElement = focusableElements[focusableElements.length - 1]
  if (!firstElement || !lastElement) return

  if (event.shiftKey && document.activeElement === firstElement) {
    event.preventDefault()
    lastElement.focus()
  } else if (!event.shiftKey && document.activeElement === lastElement) {
    event.preventDefault()
    firstElement.focus()
  }
}

watch(visible, async (isVisible) => {
  if (isVisible) {
    previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
    await nextTick()
    cancelButtonRef.value?.focus()
    return
  }
  await nextTick()
  previousFocus?.focus()
  previousFocus = null
})

onBeforeUnmount(() => {
  resolver?.(false)
  resolver = null
})

defineExpose({ open })
</script>

<template>
  <Teleport to="body">
    <Transition name="app-confirm">
      <div v-if="visible" class="app-confirm-overlay" @mousedown.self="handleOverlayClick">
        <section
          ref="dialogRef"
          class="app-confirm-dialog"
          :class="`is-${options.tone}`"
          role="alertdialog"
          :aria-modal="true"
          :aria-labelledby="titleId"
          :aria-describedby="messageId"
          @keydown="handleKeydown"
        >
          <header class="app-confirm-header">
            <div class="app-confirm-heading">
              <span class="app-confirm-icon" aria-hidden="true">
                <i :class="options.tone === 'primary' ? 'fa fa-question-circle' : 'fa fa-exclamation-triangle'" />
              </span>
              <h2 :id="titleId">{{ options.title }}</h2>
            </div>
            <button class="app-confirm-close" type="button" aria-label="关闭确认弹窗" @click="settle(false)">
              <i class="fa fa-times" aria-hidden="true" />
            </button>
          </header>

          <div class="app-confirm-body">
            <p :id="messageId">{{ options.message }}</p>
          </div>

          <footer class="app-confirm-actions">
            <button ref="cancelButtonRef" class="app-confirm-button is-cancel" type="button" @click="settle(false)">
              {{ options.cancelText }}
            </button>
            <button class="app-confirm-button is-confirm" type="button" @click="settle(true)">
              {{ options.confirmText }}
            </button>
          </footer>
        </section>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped lang="scss">
.app-confirm-overlay {
  position: fixed;
  z-index: 2000;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 20px;
  box-sizing: border-box;
  background: rgba(15, 23, 42, 0.44);
}

.app-confirm-dialog {
  position: relative;
  width: min(480px, 100%);
  overflow: hidden;
  background: #fff;
  border: 1px solid #dfe3ea;
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
}

.app-confirm-header {
  display: flex;
  min-height: 52px;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 10px 0 20px;
  border-bottom: 1px solid #e7eaf0;
}

.app-confirm-heading {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 10px;

  h2 {
    margin: 0;
    overflow: hidden;
    color: #273142;
    font-size: 15px;
    font-weight: 650;
    line-height: 1.4;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.app-confirm-close {
  display: inline-grid;
  width: 32px;
  height: 32px;
  flex: 0 0 32px;
  place-items: center;
  padding: 0;
  color: #7b8495;
  background: transparent;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease;

  &:hover {
    color: #273142;
    background: #f2f4f7;
  }

  &:focus-visible {
    outline: 2px solid rgba(91, 80, 242, 0.45);
    outline-offset: 1px;
  }
}

.app-confirm-icon {
  display: inline-grid;
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  place-items: center;
  color: #a15c07;
  background: #fff8e6;
  border: 1px solid #f3dfad;
  border-radius: 6px;
  font-size: 13px;
}

.app-confirm-dialog.is-danger .app-confirm-icon {
  color: #c43232;
  background: #fff1f1;
  border-color: #f2c7c7;
}

.app-confirm-dialog.is-primary .app-confirm-icon {
  color: #4f46e5;
  background: #f3f2ff;
  border-color: #d9d6ff;
}

.app-confirm-body {
  padding: 16px 20px 18px;

  p {
    margin: 0;
    color: #5f6878;
    font-size: 13px;
    line-height: 1.7;
  }
}

.app-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 14px;
  background: #f8f9fb;
  border-top: 1px solid #e7eaf0;
}

.app-confirm-button {
  height: 34px;
  min-width: 72px;
  padding: 0 13px;
  color: #344054;
  font-size: 13px;
  font-weight: 500;
  background: #fff;
  border: 1px solid #cfd5df;
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease;

  &:hover {
    color: #273142;
    background: #f2f4f7;
    border-color: #b9c1cd;
  }

  &:focus-visible {
    outline: 2px solid rgba(91, 80, 242, 0.45);
    outline-offset: 1px;
  }

  &.is-confirm {
    color: #fff;
    background: #a15c07;
    border-color: #a15c07;

    &:hover {
      background: #844b06;
      border-color: #844b06;
    }
  }
}

.app-confirm-dialog.is-danger .app-confirm-button.is-confirm {
  background: #c43232;
  border-color: #c43232;

  &:hover {
    background: #a92828;
    border-color: #a92828;
  }
}

.app-confirm-dialog.is-primary .app-confirm-button.is-confirm {
  background: #4f46e5;
  border-color: #4f46e5;

  &:hover {
    background: #4338ca;
    border-color: #4338ca;
  }
}

.app-confirm-enter-active,
.app-confirm-leave-active {
  transition: opacity 0.18s ease;

  .app-confirm-dialog {
    transition: opacity 0.18s ease, transform 0.18s ease;
  }
}

.app-confirm-enter-from,
.app-confirm-leave-to {
  opacity: 0;

  .app-confirm-dialog {
    opacity: 0;
    transform: translateY(4px);
  }
}

@media (max-width: 520px) {
  .app-confirm-overlay {
    padding: 12px;
  }

  .app-confirm-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .app-confirm-button {
    height: auto;
    min-width: 0;
    min-height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .app-confirm-enter-active,
  .app-confirm-leave-active,
  .app-confirm-enter-active .app-confirm-dialog,
  .app-confirm-leave-active .app-confirm-dialog {
    transition: none;
  }
}
</style>
