import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import AppLayout from '../views/AppLayout.vue'
import SettingsLayout from '../views/settings/SettingsLayout.vue'
import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState,
         needsAdmin, isNeedsAdminLoaded, refreshNeedsAdmin } from '../composables/setupState'
import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins'
import { useToast } from '../composables/toast'
import { getFacilityName } from '../utils/siteSettings'
import { stagedChildren, stagedToplevel } from './routes.gen'

// Auto-discover all route modules from routes/ directory. A module's default
// export is AppLayout child routes; an optional `toplevel` export is full-screen
// routes (kiosk, print pages) that live OUTSIDE AppLayout. Both let a plugin own
// its routes, so pruning the plugin removes them with no core edit (ADR-013).
const routeModules = import.meta.glob('./routes/*.js', { eager: true })
// Staged plugin frontends (relocated to plugins/<name>/frontend/) are
// aggregated into routes.gen.js by scripts/stage-frontend.mjs. In-tree route
// files (./routes/*.js) cover plugins not yet relocated - dual-location during
// the Phase 4 transition.
const rawChildren = [
  // A route file may export only `toplevel` (no AppLayout children), so guard
  // the default with an empty array.
  ...Object.values(routeModules).flatMap(m => m.default || []),
  ...stagedChildren,
]
const pluginTopLevel = [
  ...Object.values(routeModules).flatMap(m => m.toplevel || []),
  ...stagedToplevel,
]

// Gather the settings pages (spread across plugin route files) and nest them
// under a single two-pane shell so the grouped rail stays put while the right
// pane swaps. Slides is a sidebar page that happens to live at /settings/slides;
// keep it full-width (not inside the settings rail).
const SETTINGS_STANDALONE = new Set(['settings/slides'])
const settingsChildren = []
const otherChildren = []
for (const route of rawChildren) {
  const path = route.path
  if (path === 'settings') {
    // Old index becomes the shell's default child (keeps name 'settings').
    settingsChildren.unshift({ ...route, path: '' })
  } else if (typeof path === 'string' && path.startsWith('settings/') && !SETTINGS_STANDALONE.has(path)) {
    settingsChildren.push({ ...route, path: path.replace(/^settings\//, '') })
  } else {
    otherChildren.push(route)
  }
}
const appChildren = [
  ...otherChildren,
  {
    path: 'settings',
    component: SettingsLayout,
    meta: { requiresAuth: true, requiresAdmin: true },
    children: settingsChildren,
  },
]

const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/Login.vue'),
    meta: { guest: true }
  },
  // Forced/self-service password change (standalone, authenticated)
  {
    path: '/change-password',
    name: 'change-password',
    component: () => import('../views/ChangePassword.vue'),
    meta: { requiresAuth: true }
  },
  // First-run setup wizard (standalone, admin-only)
  {
    path: '/setup',
    name: 'setup',
    component: () => import('../views/SetupWizard.vue'),
    meta: { requiresAuth: true, requiresAdmin: true }
  },
  // Standalone full-screen dashboards (no sidebar, no auth required)
  {
    path: '/shopfloor',
    name: 'shopfloor',
    component: () => import('../views/ShopfloorDashboard.vue')
  },
  // Plugin-owned full-screen routes (kiosk, TV, plugin print pages) are
  // contributed by each plugin's route file via its `toplevel` export.
  ...pluginTopLevel,
  // Core print pages (standalone, no sidebar/header) - span multiple asset
  // types or are core-owned, so they stay here.
  {
    path: '/print/machine-badge/:id',
    name: 'print-machine-badge',
    component: () => import('../views/print/MachineBadge.vue')
  },
  // Shared asset label/code generator for any asset type (public, like the
  // other /print/* routes). assettype = machine|computer|printer|
  // network_device|measuring_tool; id = the asset's plugin id.
  {
    path: '/print/asset-label/:assettype/:id',
    name: 'print-asset-label',
    component: () => import('../views/print/AssetLabel.vue')
  },
  // Batch label sheets (ULINE 6-up / mini 72-up) for one asset type.
  {
    path: '/print/asset-label-batch/:assettype',
    name: 'print-asset-label-batch',
    component: () => import('../views/print/AssetLabelBatch.vue')
  },
  {
    path: '/',
    component: AppLayout,
    children: appChildren
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// --- Document titles: "<Facility> ShopDB - <Page>" ---------------------------
// Facility name comes from public settings (cached after first fetch); the
// page label comes from meta.title when a route sets one, else a prettified
// route name with spellings for the odd ones.
const TITLE_SPELLINGS = {
  'pcs': 'PCs',
  'usb': 'USB Devices',
  'geenforce': 'GE-Enforce',
  'knowledgebase': 'Knowledge Base',
  'printedparts': '3D Printed Parts',
  'parts-kiosk': 'Parts Kiosk',
  'tv': 'TV Slideshow',
  'shopfloor': 'Shopfloor Dashboard',
  'measuringtools': 'Measuring Tools',
  'networkdevices': 'Network',
}

function pageTitleFor(route) {
  if (route.meta?.title) return route.meta.title
  const name = String(route.name || '')
  if (!name) return ''
  const base = name.replace(/-(new|edit|detail)$/, '')
  if (TITLE_SPELLINGS[base]) return TITLE_SPELLINGS[base]
  return base.split('-').map(word =>
    word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
}

router.afterEach(async (to) => {
  let siteTitle = 'ShopDB'
  try {
    const facility = await getFacilityName()
    if (facility && facility !== 'ShopDB') siteTitle = `${facility} ShopDB`
  } catch (titleError) { /* settings unavailable - plain ShopDB */ }
  const page = pageTitleFor(to)
  document.title = page && page !== 'Home' ? `${siteTitle} - ${page}` : siteTitle
})

// A deploy replaces the hashed chunk files, so a tab that was open across it
// still asks for chunks that no longer exist. The dynamic import rejects, the
// navigation aborts, and to the user the site simply stops navigating. Reload
// once to pick up the new index.html; the sessionStorage flag stops a reload
// loop if the chunk is missing for any other reason.
const CHUNK_RELOAD_KEY = 'shopdb-chunk-reloaded'

function isChunkLoadError(error) {
  const text = `${error?.message || ''} ${error?.name || ''}`
  return /dynamically imported module|Importing a module script failed|ChunkLoadError|Loading chunk/i.test(text)
}

function reloadOnceForStaleChunk(error) {
  if (!isChunkLoadError(error)) return false
  if (sessionStorage.getItem(CHUNK_RELOAD_KEY)) return false
  sessionStorage.setItem(CHUNK_RELOAD_KEY, '1')
  window.location.reload()
  return true
}

router.onError(error => {
  reloadOnceForStaleChunk(error)
})

// Vite raises this for a failed module preload, which does not always surface
// as a router error.
window.addEventListener('vite:preloadError', event => {
  if (reloadOnceForStaleChunk(event?.payload || event)) event.preventDefault?.()
})

// A navigation that completes proves the current chunks are good, so clear the
// flag - otherwise the one allowed reload is spent for the rest of the session.
router.afterEach(() => {
  sessionStorage.removeItem(CHUNK_RELOAD_KEY)
})

// Navigation guard
router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()

  // FIRST RUN: until an admin exists, nothing else in the app is useful. Without
  // this, a fresh instance serves an anonymous dashboard and the operator has no
  // indication that setup is unfinished (they must guess their way to /login).
  // Unauthenticated only, so it costs an established site nothing.
  if (!authStore.isAuthenticated && to.path !== '/login') {
    if (!isNeedsAdminLoaded()) {
      // Bounded like the plugin gate: a hung request must not hold navigation
      // open. refreshNeedsAdmin fails open on error.
      await Promise.race([
        refreshNeedsAdmin(),
        new Promise(resolve => setTimeout(resolve, 4000)),
      ])
    }
    if (needsAdmin.value) {
      return next({ path: '/login', query: { firstrun: '1' } })
    }
  }

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    // Remember where they were headed so login can send them back there.
    return next({ path: '/login', query: { redirect: to.fullPath } })
  }
  if (to.meta.requiresAdmin && !authStore.isAdmin) {
    return next('/')
  }
  // A route may gate on a named permission instead of full admin (e.g. the
  // slide manager -> slides.manage). Admins pass via hasPermission.
  if (to.meta.requiresPermission && !authStore.hasPermission(to.meta.requiresPermission)) {
    return next('/')
  }
  if (to.meta.guest && authStore.isAuthenticated) {
    return next('/')
  }

  // Forced password change: an admin-set temporary password must be replaced
  // before the user reaches the rest of the app. Let them log out.
  if (authStore.isAuthenticated && authStore.mustChangePassword
      && to.path !== '/change-password' && to.path !== '/login') {
    return next('/change-password')
  }

  // Plugin gating: a disabled backend plugin's frontend routes are dead ends.
  // The enabled list is fetched once and cached; fail-open on error so a blip
  // cannot brick navigation. Works unauthenticated (endpoint is jwt-optional).
  if (to.meta.plugin) {
    await loadEnabledPlugins()
    if (!isPluginEnabled(to.meta.plugin)) {
      useToast().info(`The ${to.meta.plugin} feature is not enabled.`)
      return next('/')
    }
  }

  // First-run: steer a fresh admin into the setup wizard (once, until finished).
  if (authStore.isAuthenticated && authStore.isAdmin && to.path !== '/setup') {
    if (!isSetupLoaded()) {
      // Bounded like the plugin gate: a hung request during an app-pool restart
      // must not hold the navigation open. Its own catch already fails open.
      await Promise.race([
        refreshSetupState(),
        new Promise(resolve => setTimeout(resolve, 4000)),
      ])
    }
    if (!setupComplete.value && !setupSkipped.value) {
      return next('/setup')
    }
  }

  next()
})

export default router
