<template>
  <div class="apps-view pb-6">
    <!-- Nav header -- tabs + categories + search -->
    <div class="mb-4">
      <!-- Desktop: page tabs + category tabs + search -->
      <div class="hidden md:flex items-center gap-4">
        <div class="mode-switcher flex-shrink-0">
          <button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
          <RouterLink to="/dashboard/discover" class="mode-switcher-btn">App Store</RouterLink>
          <button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'websites' }" @click="activeTab = 'websites'; router.replace({ query: { tab: 'websites' } })">Websites</button>
        </div>
        <div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
          <button
            v-for="category in categoriesWithApps"
            :key="category.id"
            @click="selectedCategory = category.id"
            class="mode-switcher-btn"
            :class="{ 'mode-switcher-btn-active': selectedCategory === category.id }"
          >{{ category.name }}</button>
        </div>
        <input
          v-model="searchQuery"
          type="text"
          :placeholder="t('apps.searchPlaceholder')"
          :aria-label="t('apps.searchLabel')"
          class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
        />
      </div>

      <!-- Mobile: search only (tabs handled by Dashboard.vue header) -->
      <div class="md:hidden">
        <input
          v-model="searchQuery"
          type="text"
          :placeholder="t('apps.searchPlaceholder')"
          :aria-label="t('apps.searchLabel')"
          class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
        />
      </div>
    </div>

    <!-- Loading Skeleton -->
    <div v-if="isLoadingApps" class="text-center py-16 pb-6">
      <div class="glass-card p-8 max-w-md mx-auto">
        <svg class="animate-spin h-8 w-8 mx-auto mb-4 text-white/70" viewBox="0 0 24 24" fill="none">
          <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
        </svg>
        <h3 class="text-lg font-semibold text-white mb-2">Loading apps</h3>
        <p class="text-white/60 text-sm">Checking the latest app status before showing launch controls.</p>
      </div>
    </div>

    <!-- Connection Error -->
    <div v-else-if="connectionError && sortedPackageEntries.length === 0" class="text-center py-12 pb-6">
      <div class="glass-card p-8 max-w-md mx-auto">
        <div class="alert-error mb-4">{{ connectionError }}</div>
        <button
          @click="connectionError = ''; store.connectWebSocket()"
          class="glass-button px-6 py-3 rounded-lg font-medium"
        >
          Retry Connection
        </button>
      </div>
    </div>

    <!-- Empty State -->
    <div v-else-if="sortedPackageEntries.length === 0 && !searchQuery" class="text-center py-16 pb-6">
      <div class="glass-card p-12 max-w-md mx-auto">
        <svg class="w-16 h-16 mx-auto text-white/40 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
        </svg>
        <h3 class="text-xl font-semibold text-white mb-2">{{ t('apps.noAppsTitle') }}</h3>
        <p class="text-white/70 mb-6">{{ t('apps.noAppsMessage') }}</p>
        <RouterLink
          to="/dashboard/marketplace"
          class="inline-block glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30"
        >
          {{ t('apps.browseAppStore') }}
        </RouterLink>
      </div>
    </div>

    <!-- No Results -->
    <div v-if="filteredPackageEntries.length === 0 && searchQuery" class="text-center py-12">
      <p class="text-white/70">{{ t('apps.noResults', { query: searchQuery }) }}</p>
    </div>

    <!-- Mobile: iPhone-style icon grid -->
    <div class="md:hidden">
      <AppIconGrid
        :apps="filteredPackageEntries as [string, PackageDataEntry][]"
        @go-to-app="goToApp"
      />
    </div>

    <!-- Desktop: Card grid -->
    <div class="hidden md:grid grid-cols-2 lg:grid-cols-3 gap-4 pb-6">
      <AppCard
        v-for="([id, pkg], index) in filteredPackageEntries"
        :key="id"
        :id="id as string"
        :pkg="pkg"
        :index="index"
        :show-stagger="showStagger"
        :is-loading="!!actions.loadingActions.value[id as string]"
        :is-installing="serverStore.isInstalling(id as string)"
        :install-progress="serverStore.installingApps.get(id as string)"
        :is-uninstalling="actions.uninstallingApps.has(id as string)"
        @go-to-app="goToApp"
        @launch="launchApp"
        @start="actions.startApp"
        @stop="actions.stopApp"
        @restart="actions.restartApp"
        @update="updateApp"
        @show-uninstall="showUninstallModal"
      />
    </div>

    <AppsUninstallModal
      :show="uninstallModal.show"
      :app-title="uninstallModal.appTitle"
      :uninstalling="actions.uninstalling.value"
      @close="closeUninstallModal"
      @confirm="onConfirmUninstall"
    />

    <!-- Action error toast -->
    <Transition name="fade">
      <div v-if="actions.actionError.value" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4" role="alert" aria-live="assertive">
        <div class="alert-error backdrop-blur-sm rounded-lg px-4 py-3 text-sm flex items-center justify-between gap-3">
          <span>{{ actions.actionError.value }}</span>
          <button @click="actions.actionError.value = ''" :aria-label="t('apps.dismissError')" class="text-red-300 hover:text-white shrink-0">&times;</button>
        </div>
      </div>
    </Transition>
  </div>
</template>

<script lang="ts">
// Module-level -- persists across component unmount/remount within same session
let appsAnimationDone = false
</script>

<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute, RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useServerStore } from '@/stores/server'
import { useAppLauncherStore } from '@/stores/appLauncher'
import { type PackageDataEntry, type PackageState } from '@/types/api'
import AppCard from './apps/AppCard.vue'
import AppIconGrid from './apps/AppIconGrid.vue'
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
import { useAppsActions } from './apps/useAppsActions'
import {
  type AppsTab, filterEntriesForTab, isWebOnlyApp, isWebsitePackage, opensInTab, resolveRuntimeLaunchUrl,
  WEB_ONLY_APPS, buildAllCategories, useCategoriesWithApps,
} from './apps/appsConfig'

const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const store = useAppStore()
const serverStore = useServerStore()
const actions = useAppsActions()

// Only stagger-animate on first mount
const showStagger = !appsAnimationDone

// Tabs
const activeTab = ref<AppsTab>(
  route.query.tab === 'websites' || route.query.tab === 'services' ? 'websites' : 'apps'
)

// Search (debounced)
const searchQuery = ref('')
const debouncedSearchQuery = ref('')
let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined
watch(searchQuery, (val) => {
  clearTimeout(searchDebounceTimer)
  searchDebounceTimer = setTimeout(() => { debouncedSearchQuery.value = val }, 150)
})
onBeforeUnmount(() => { clearTimeout(searchDebounceTimer) })

// Category filter
const selectedCategory = ref('all')

const ALL_CATEGORIES = computed(() => buildAllCategories(t))

// Merge real packages from store with web-only app bookmarks + installing placeholders
const packages = computed(() => {
  const realPackages = store.packages || {}
  const merged: Record<string, PackageDataEntry> = { ...WEB_ONLY_APPS, ...realPackages }

  // Inject placeholder entries for apps being installed that aren't in backend data yet
  for (const [appId, progress] of serverStore.installingApps) {
    if (!merged[appId]) {
      merged[appId] = {
        state: 'installing' as PackageState,
        manifest: {
          id: appId,
          title: progress.title,
          version: '',
          description: { short: '', long: '' },
          'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '',
          'support-site': '', 'marketing-site': '', 'donation-url': null,
        },
        'static-files': { license: '', instructions: '', icon: '' },
      }
    }
  }

  return merged
})

const categoriesWithApps = useCategoriesWithApps(packages, ALL_CATEGORIES)

const isLoadingApps = computed(() => !store.hasLoadedInitialData && !connectionError.value)

// Connection error state
const connectionError = ref('')
let connectionTimer: ReturnType<typeof setTimeout> | undefined

onMounted(() => {
  appsAnimationDone = true
  if (!store.isConnected) {
    connectionTimer = setTimeout(() => {
        if (!store.hasLoadedInitialData && sortedPackageEntries.value.length === 0) {
        connectionError.value = 'Unable to connect to server. Check that the backend is running.'
      }
    }, 15000)
  }
})

onBeforeUnmount(() => {
  if (connectionTimer) clearTimeout(connectionTimer)
})

// Sorted entries: web-only first, then alphabetical by title
const sortedPackageEntries = computed(() => {
  const entries = Object.entries(packages.value)
  const filtered = filterEntriesForTab(entries, activeTab.value, selectedCategory.value)
  return filtered.sort(([idA, a], [idB, b]) => {
    const aWeb = isWebOnlyApp(idA) ? 0 : 1
    const bWeb = isWebOnlyApp(idB) ? 0 : 1
    if (aWeb !== bWeb) return aWeb - bWeb
    return (a.manifest?.title ?? '').localeCompare(b.manifest?.title ?? '', undefined, { sensitivity: 'base' })
  })
})

const filteredPackageEntries = computed(() => {
  if (!debouncedSearchQuery.value) return sortedPackageEntries.value
  const q = debouncedSearchQuery.value.toLowerCase()
  return sortedPackageEntries.value.filter(([id, pkg]) =>
    (pkg.manifest?.title ?? '').toLowerCase().includes(q) ||
    (pkg.manifest?.description?.short ?? '').toLowerCase().includes(q) ||
    id.toLowerCase().includes(q)
  )
})

// Uninstall modal
const uninstallModal = ref({ show: false, appId: '', appTitle: '' })

function showUninstallModal(id: string, pkg: PackageDataEntry) {
  uninstallModal.value = { show: true, appId: id, appTitle: pkg.manifest.title }
}

function closeUninstallModal() {
  uninstallModal.value.show = false
}

async function onConfirmUninstall() {
  const { appId } = uninstallModal.value
  // Close the modal immediately so the user can fire off concurrent
  // uninstalls. Each AppCard surfaces its own live stage label while
  // its uninstall is in flight.
  uninstallModal.value.show = false
  await actions.confirmUninstall(appId)
}

function goToApp(id: string) {
  router.push(`/dashboard/apps/${id}`).catch(() => {})
}

function launchApp(id: string) {
  const pkg = packages.value[id]
  if (pkg && isWebsitePackage(id, pkg)) {
    const url = resolveRuntimeLaunchUrl(pkg)
    if (url) window.open(url, '_blank', 'noopener,noreferrer')
    return
  }
  if (pkg && opensInTab(id)) {
    const url = resolveRuntimeLaunchUrl(pkg)
    if (url) {
      window.open(url, '_blank', 'noopener,noreferrer')
      return
    }
  }
  useAppLauncherStore().openSession(id)
}

async function updateApp(id: string) {
  try {
    await serverStore.updatePackage(id)
  } catch (err) {
    actions.actionError.value = `Failed to update ${id}: ${err instanceof Error ? err.message : 'Unknown error'}`
  }
}
</script>
