<template>
  <v-dialog v-model="visible" max-width="520" :persistent="blockingRecovery || restarting">
    <v-card>
      <v-card-title class="upgrade-recovery-title">
        <span>{{ t('core.common.upgradeRecovery.title') }}</span>
      </v-card-title>

      <v-card-text>
        <p class="mb-3">
          {{
            t('core.common.upgradeRecovery.description', {
              coreVersion,
              dashboardVersion,
            })
          }}
        </p>
        <v-alert type="warning" variant="tonal" density="comfortable" class="mb-3">
          {{ t('core.common.upgradeRecovery.hint') }}
        </v-alert>
        <v-progress-linear
          v-if="restarting"
          indeterminate
          color="primary"
          class="mb-2"
        />
        <div v-if="statusMessage" class="text-medium-emphasis">
          {{ statusMessage }}
        </div>
      </v-card-text>

      <v-card-actions>
        <v-spacer />
        <v-btn v-if="!blockingRecovery" variant="text" :disabled="restarting" @click="dismiss">
          {{ t('core.common.upgradeRecovery.laterButton') }}
        </v-btn>
        <v-btn
          color="primary"
          variant="flat"
          prepend-icon="mdi-restart"
          :loading="restarting"
          @click="restartCore"
        >
          {{ t('core.common.upgradeRecovery.restartButton') }}
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script setup lang="ts">
import axios, { type AxiosRequestConfig } from 'axios';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';

import {
  UPGRADE_RECOVERY_EVENT,
  UPGRADE_RECOVERY_TOKEN_KEY,
  type ApiEnvelope,
  type VersionData,
} from '@/api/v1';
import { useI18n } from '@/i18n/composables';

type StartTimeData = {
  start_time?: number | string | null;
};

type RecoveryEventDetail = VersionData & {
  blocking?: boolean;
};

const { t } = useI18n();
const route = useRoute();

const visible = ref(false);
const restarting = ref(false);
const blockingRecovery = ref(false);
const statusMessage = ref('');
const coreVersion = ref('');
const dashboardVersion = ref('');
const initialStartTime = ref<number | string | null>(null);

let restartTimer: ReturnType<typeof setInterval> | null = null;
let detecting = false;
const recoveryClient = axios.create();

function normalizeVersion(version?: string | null) {
  return (version || '').trim().replace(/^v/i, '');
}

function displayVersion(version?: string | null) {
  const normalized = normalizeVersion(version);
  return normalized ? `v${normalized}` : 'unknown';
}

function versionsMismatch(core?: string | null, dashboard?: string | null) {
  const normalizedCore = normalizeVersion(core);
  const normalizedDashboard = normalizeVersion(dashboard);
  return Boolean(
    normalizedCore &&
      normalizedDashboard &&
      normalizedCore !== normalizedDashboard,
  );
}

function isMissingApiKeyResponse(response: {
  data?: { message?: string | null } | string;
}) {
  const message =
    typeof response.data === 'string'
      ? response.data
      : response.data?.message || '';
  return message.toLowerCase().includes('missing api key');
}

function getDismissKey() {
  return `astrbot-upgrade-recovery-dismissed:${coreVersion.value}:${dashboardVersion.value}`;
}

function recoveryRequestConfig(validateStatus = false): AxiosRequestConfig {
  const headers: Record<string, string> = {};
  const token =
    localStorage.getItem('token') ||
    sessionStorage.getItem(UPGRADE_RECOVERY_TOKEN_KEY);
  const locale = localStorage.getItem('astrbot-locale');
  if (token) {
    headers.Authorization = `Bearer ${token}`;
  }
  if (locale) {
    headers['Accept-Language'] = locale;
  }
  return {
    headers,
    ...(validateStatus ? { validateStatus: () => true } : {}),
  };
}

async function fetchLegacyStartTime() {
  const response = await recoveryClient.get<ApiEnvelope<StartTimeData>>(
    '/api/stat/start-time',
    recoveryRequestConfig(),
  );
  return response.data?.data?.start_time ?? null;
}

function clearRestartTimer() {
  if (restartTimer !== null) {
    clearInterval(restartTimer);
    restartTimer = null;
  }
}

function dismiss() {
  sessionStorage.setItem(getDismissKey(), '1');
  sessionStorage.removeItem(UPGRADE_RECOVERY_TOKEN_KEY);
  blockingRecovery.value = false;
  visible.value = false;
}

function reloadWithCacheBuster() {
  const url = new URL(window.location.href);
  url.searchParams.set('_r', Date.now().toString());
  window.location.replace(url.toString());
}

function waitForRestart() {
  clearRestartTimer();
  let attempts = 0;
  restartTimer = setInterval(async () => {
    attempts += 1;
    try {
      const nextStartTime = await fetchLegacyStartTime();
      if (
        nextStartTime !== null &&
        String(nextStartTime) !== String(initialStartTime.value)
      ) {
        clearRestartTimer();
        sessionStorage.removeItem(UPGRADE_RECOVERY_TOKEN_KEY);
        reloadWithCacheBuster();
      }
    } catch (_error) {
      // The backend may be temporarily unavailable during restart.
    }

    if (attempts >= 90) {
      clearRestartTimer();
      restarting.value = false;
      statusMessage.value = t('core.common.upgradeRecovery.failed');
    }
  }, 1000);
}

async function restartCore() {
  restarting.value = true;
  statusMessage.value = t('core.common.upgradeRecovery.restarting');
  try {
    initialStartTime.value =
      initialStartTime.value ?? (await fetchLegacyStartTime());
    await recoveryClient.post<ApiEnvelope<unknown>>(
      '/api/stat/restart-core',
      undefined,
      recoveryRequestConfig(),
    );
    statusMessage.value = t('core.common.upgradeRecovery.waiting');
    waitForRestart();
  } catch (_error) {
    restarting.value = false;
    statusMessage.value = t('core.common.upgradeRecovery.failed');
  }
}

async function showRecoveryDialog(versionData: VersionData, blocking = false) {
  if (visible.value || restarting.value) {
    return;
  }
  if (!versionsMismatch(versionData.version, versionData.dashboard_version)) {
    return;
  }

  coreVersion.value = displayVersion(versionData.version);
  dashboardVersion.value = displayVersion(versionData.dashboard_version);
  if (!blocking && sessionStorage.getItem(getDismissKey())) {
    return;
  }

  blockingRecovery.value = blocking;
  initialStartTime.value = await fetchLegacyStartTime().catch(() => null);
  visible.value = true;
}

function handleRecoveryEvent(event: Event) {
  const versionData = (event as CustomEvent<RecoveryEventDetail>).detail || {};
  void showRecoveryDialog(versionData, !!versionData.blocking);
}

async function detectUpgradeMismatch() {
  if (detecting || visible.value || restarting.value) {
    return;
  }
  detecting = true;
  try {
    const v1Response = await recoveryClient.get<ApiEnvelope<unknown>>(
      '/api/v1/auth/setup-status',
      recoveryRequestConfig(true),
    );
    if (!isMissingApiKeyResponse(v1Response)) {
      return;
    }

    const legacyResponse = await recoveryClient.get<ApiEnvelope<VersionData>>(
      '/api/stat/version',
      recoveryRequestConfig(true),
    );
    if (legacyResponse.status === 401 || legacyResponse.status >= 400) {
      return;
    }

    await showRecoveryDialog(legacyResponse.data?.data || {});
  } catch (_error) {
    // This recovery dialog is best-effort and should never block the app.
  } finally {
    detecting = false;
  }
}

onMounted(() => {
  window.addEventListener(UPGRADE_RECOVERY_EVENT, handleRecoveryEvent);
  void detectUpgradeMismatch();
});

watch(
  () => route.fullPath,
  () => {
    void detectUpgradeMismatch();
  },
);

onBeforeUnmount(() => {
  window.removeEventListener(UPGRADE_RECOVERY_EVENT, handleRecoveryEvent);
  clearRestartTimer();
});
</script>

<style scoped>
.upgrade-recovery-title {
  align-items: center;
  display: flex;
  white-space: normal;
  word-break: break-word;
}
</style>
