/**
 * Client-side Router service
 *
 * Provides intra-app view toggling and cross-app HTML-fetch-based
 * content swapping so navigation between apps doesn't trigger full
 * page reloads.
 */

import { sanitizeHtml } from '../utils/sanitize'

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

export type RouteParams = Record<string, string>

export interface RouteMatch {
  pattern: string
  params: RouteParams
  path: string
  appPrefix: string
  view: string
  navSection: string | undefined
}

export interface RouteDefinition {
  /** e.g. '/forge/workflows/:id' */
  pattern: string
  /** e.g. 'workflow-detail' */
  view: string
  /** e.g. 'workflows' — used to highlight the correct sidebar item */
  navSection?: string
}

export interface AppRouteConfig {
  /** e.g. '/forge' */
  prefix: string
  /** e.g. 'data-forge-view' */
  viewAttribute: string
  /** e.g. 'data-forge-nav' */
  navAttribute: string
  activeViewClass?: string
  activeNavClass?: string
  routes: RouteDefinition[]
  defaultView?: string
}

export interface AppModule {
  init: (params?: RouteParams) => void | Promise<void>
  destroy?: () => void | Promise<void>
}

export interface RouterConfig {
  /** Selector for the main content element. Default: '#app-content' */
  contentSelector?: string
  /** Whether to intercept same-origin link clicks. Default: true */
  interceptLinks?: boolean
  /** Called when a fetch returns 401/403 */
  onAuthError?: (status: number) => void
  /** CSS class added during content swap transitions */
  transitionClass?: string
  /**
   * Sanitize fetched same-origin HTML before swapping it into the DOM.
   * Default false preserves trusted server-rendered HTML behavior.
   */
  sanitizeFetchedHtml?: boolean
  /** Called after cross-app swap to re-init shared components (sidebar, app-switcher) */
  onAfterSwap?: () => void | Promise<void>
}

export interface NavigationEvent {
  from: RouteMatch | null
  to: RouteMatch | null
  path: string
}

// ---------------------------------------------------------------------------
// Internal types
// ---------------------------------------------------------------------------

interface CompiledRoute {
  definition: RouteDefinition
  regex: RegExp
  paramNames: string[]
  appPrefix: string
}

// ---------------------------------------------------------------------------
// Route compilation
// ---------------------------------------------------------------------------

function compilePattern(prefix: string, pattern: string): { regex: RegExp; paramNames: string[] } {
  const paramNames: string[] = []
  // Remove leading prefix from pattern for matching against relative path
  let relativePattern = pattern
  if (relativePattern.startsWith(prefix)) {
    relativePattern = relativePattern.slice(prefix.length)
  }
  if (!relativePattern.startsWith('/')) {
    relativePattern = '/' + relativePattern
  }

  const regexParts = relativePattern.split('/').map(segment => {
    if (segment.startsWith(':')) {
      paramNames.push(segment.slice(1))
      return '([^/]+)'
    }
    if (segment.startsWith('*')) {
      paramNames.push(segment.slice(1))
      return '(.*)'
    }
    return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  })

  const fullPattern = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + regexParts.join('/')
  return { regex: new RegExp('^' + fullPattern + '/?$'), paramNames }
}

// ---------------------------------------------------------------------------
// Router class
// ---------------------------------------------------------------------------

export class Router {
  private apps = new Map<string, AppRouteConfig>()
  private compiledRoutes: CompiledRoute[] = []
  private moduleLoaders = new Map<string, () => Promise<AppModule>>()
  private loadedModules = new Map<string, AppModule>()
  private currentRoute: RouteMatch | null = null
  private currentAppPrefix: string | null = null
  private config: Required<RouterConfig>
  private listeners = new Map<string, Set<(e: NavigationEvent) => void>>()
  private clickHandler: ((e: MouseEvent) => void) | null = null
  private popstateHandler: (() => void) | null = null
  private started = false

  constructor(config?: RouterConfig) {
    this.config = {
      contentSelector: config?.contentSelector ?? '#app-content',
      interceptLinks: config?.interceptLinks ?? true,
      onAuthError:
        config?.onAuthError ??
        ((_status: number) => {
          window.location.href = '/login'
        }),
      transitionClass: config?.transitionClass ?? 'page__content--transitioning',
      sanitizeFetchedHtml: config?.sanitizeFetchedHtml ?? false,
      onAfterSwap:
        config?.onAfterSwap ??
        (() => {
          /* noop */
        }),
    }
  }

  // -----------------------------------------------------------------------
  // Registration
  // -----------------------------------------------------------------------

  registerApp(appConfig: AppRouteConfig): void {
    this.apps.set(appConfig.prefix, appConfig)
    // Compile routes — longest patterns first for correct matching
    const compiled: CompiledRoute[] = appConfig.routes.map(def => {
      const { regex, paramNames } = compilePattern(appConfig.prefix, def.pattern)
      return { definition: def, regex, paramNames, appPrefix: appConfig.prefix }
    })
    // Sort: more specific (more segments / fewer wildcards) first
    compiled.sort((a, b) => {
      const aSegments = a.definition.pattern.split('/').length
      const bSegments = b.definition.pattern.split('/').length
      return bSegments - aSegments
    })
    this.compiledRoutes.push(...compiled)
    // Re-sort all compiled routes: longer prefix first, then more segments
    this.compiledRoutes.sort((a, b) => {
      if (b.appPrefix.length !== a.appPrefix.length) {
        return b.appPrefix.length - a.appPrefix.length
      }
      return b.definition.pattern.split('/').length - a.definition.pattern.split('/').length
    })
  }

  registerModule(prefix: string, loader: () => Promise<AppModule>): void {
    this.moduleLoaders.set(prefix, loader)
  }

  // -----------------------------------------------------------------------
  // Route matching
  // -----------------------------------------------------------------------

  matchRoute(path: string): RouteMatch | null {
    for (const route of this.compiledRoutes) {
      const match = route.regex.exec(path)
      if (match) {
        const params: RouteParams = {}
        route.paramNames.forEach((name, i) => {
          params[name] = decodeURIComponent(match[i + 1] ?? '')
        })
        return {
          pattern: route.definition.pattern,
          params,
          path,
          appPrefix: route.appPrefix,
          view: route.definition.view,
          navSection: route.definition.navSection,
        }
      }
    }
    // Try matching just app prefix (default view)
    for (const [prefix, appConfig] of this.apps) {
      if (path === prefix || path === prefix + '/') {
        return {
          pattern: prefix,
          params: {},
          path,
          appPrefix: prefix,
          view: appConfig.defaultView ?? 'dashboard',
          navSection: undefined,
        }
      }
    }
    return null
  }

  getCurrentRoute(): RouteMatch | null {
    return this.currentRoute
  }

  // -----------------------------------------------------------------------
  // Navigation
  // -----------------------------------------------------------------------

  async navigate(path: string, opts?: { pushState?: boolean; replace?: boolean }): Promise<void> {
    const pushState = opts?.pushState ?? true
    const replace = opts?.replace ?? false
    const newRoute = this.matchRoute(path)

    if (!newRoute) {
      // Not a route we handle — do a full page navigation
      window.location.href = path
      return
    }

    const oldRoute = this.currentRoute
    const sameApp = oldRoute !== null && oldRoute.appPrefix === newRoute.appPrefix

    if (sameApp) {
      await this.navigateIntraApp(newRoute, pushState, replace)
    } else {
      await this.navigateCrossApp(newRoute, pushState, replace)
    }

    this.setCurrentRoute(newRoute)
    this.emit('navigate', { from: oldRoute, to: newRoute, path })
  }

  // -----------------------------------------------------------------------
  // Intra-app navigation (same app prefix)
  // -----------------------------------------------------------------------

  private async navigateIntraApp(
    route: RouteMatch,
    pushState: boolean,
    replace: boolean
  ): Promise<void> {
    const appConfig = this.apps.get(route.appPrefix)
    if (appConfig) {
      this.showAppView(appConfig, route.view, route.navSection)
    }

    if (pushState) {
      this.pushOrReplace(route.path, replace)
    }

    // Keep route state in sync before init; app modules read getCurrentRoute().
    this.setCurrentRoute(route)

    // Re-init the module with new params
    const mod = this.loadedModules.get(route.appPrefix)
    if (mod) {
      await mod.init(route.params)
    }

    window.scrollTo(0, 0)
  }

  // -----------------------------------------------------------------------
  // Cross-app navigation (different app prefix)
  // -----------------------------------------------------------------------

  private async navigateCrossApp(
    route: RouteMatch,
    pushState: boolean,
    replace: boolean
  ): Promise<void> {
    const contentEl = document.querySelector<HTMLElement>(this.config.contentSelector)
    if (!contentEl) {
      window.location.href = route.path
      return
    }

    // Destroy current module
    const currentMod = this.currentAppPrefix ? this.loadedModules.get(this.currentAppPrefix) : null
    if (currentMod?.destroy) {
      await currentMod.destroy()
    }

    // Transition start
    contentEl.classList.add(this.config.transitionClass)

    if (!this.isSameOriginPath(route.path)) {
      contentEl.classList.remove(this.config.transitionClass)
      window.location.href = route.path
      return
    }

    let newHtml: string
    try {
      const response = await fetch(route.path, { credentials: 'same-origin' })
      if (response.status === 401 || response.status === 403) {
        contentEl.classList.remove(this.config.transitionClass)
        this.config.onAuthError(response.status)
        return
      }
      // Server redirected (e.g. unauthenticated → /login) — follow it fully
      if (response.redirected) {
        contentEl.classList.remove(this.config.transitionClass)
        window.location.href = response.url
        return
      }
      if (!response.ok) {
        contentEl.classList.remove(this.config.transitionClass)
        window.location.href = route.path
        return
      }
      newHtml = await response.text()
    } catch {
      contentEl.classList.remove(this.config.transitionClass)
      window.location.href = route.path
      return
    }

    // Parse new HTML
    const parser = new DOMParser()
    const newDoc = parser.parseFromString(newHtml, 'text/html')
    const newContent = newDoc.querySelector<HTMLElement>(this.config.contentSelector)

    if (!newContent) {
      contentEl.classList.remove(this.config.transitionClass)
      window.location.href = route.path
      return
    }

    // Swap sidebar at page__shell level
    const shell = document.querySelector('.page__shell')
    if (shell) {
      // Remove existing sidebar + backdrop
      const oldSidebar = shell.querySelector(':scope > aside[data-sidebar]')
      const oldBackdrop = shell.querySelector(':scope > .sidebar__backdrop')
      if (oldSidebar) oldSidebar.remove()
      if (oldBackdrop) oldBackdrop.remove()

      // Insert new sidebar + backdrop from fetched page
      const newShell = newDoc.querySelector('.page__shell')
      if (newShell) {
        const newSidebar = newShell.querySelector(':scope > aside[data-sidebar]')
        const newBackdrop = newShell.querySelector(':scope > .sidebar__backdrop')
        const mainEl = shell.querySelector('main')
        if (newSidebar && mainEl) {
          const sidebar = this.prepareFetchedElement(newSidebar)
          if (sidebar) {
            shell.insertBefore(sidebar, mainEl)
          }
        }
        if (newBackdrop && mainEl) {
          const backdrop = this.prepareFetchedElement(newBackdrop)
          if (backdrop) {
            shell.insertBefore(backdrop, mainEl)
          }
        }
      }
    }

    // Swap content
    contentEl.innerHTML = this.prepareFetchedHtml(newContent.innerHTML)
    // Copy class attribute from new content
    contentEl.className = newContent.className

    // Update document metadata
    document.body.className = newDoc.body.className
    document.title = newDoc.title

    // Update web-config
    const oldWebConfig = document.getElementById('web-config')
    const newWebConfig = newDoc.getElementById('web-config')
    if (oldWebConfig && newWebConfig) {
      oldWebConfig.textContent = newWebConfig.textContent
    }

    if (pushState) {
      this.pushOrReplace(route.path, replace)
    }

    // Keep route state in sync before init; app modules read getCurrentRoute().
    this.setCurrentRoute(route)

    // Load and init new module
    await this.loadAndInitModule(route)

    // Re-init shared components (sidebar, app-switcher)
    await this.config.onAfterSwap()

    // Transition end
    contentEl.classList.remove(this.config.transitionClass)
    window.scrollTo(0, 0)
  }

  // -----------------------------------------------------------------------
  // Module loading
  // -----------------------------------------------------------------------

  private async loadAndInitModule(route: RouteMatch): Promise<void> {
    let mod = this.loadedModules.get(route.appPrefix)
    if (!mod) {
      const loader = this.moduleLoaders.get(route.appPrefix)
      if (loader) {
        mod = await loader()
        this.loadedModules.set(route.appPrefix, mod)
      }
    }

    if (mod) {
      // Show correct view before init
      const appConfig = this.apps.get(route.appPrefix)
      if (appConfig) {
        this.showAppView(appConfig, route.view, route.navSection)
      }
      await mod.init(route.params)
    }
  }

  // -----------------------------------------------------------------------
  // View toggling (replaces per-app showView + setActiveNav)
  // -----------------------------------------------------------------------

  showAppView(appConfig: AppRouteConfig, view: string, navSection?: string): void {
    const viewAttr = appConfig.viewAttribute
    const navAttr = appConfig.navAttribute
    const activeViewClass = appConfig.activeViewClass ?? `${appConfig.prefix.slice(1)}-view--active`
    const activeNavClass = appConfig.activeNavClass ?? 'sidebar__item--active'

    // Toggle view panels
    document.querySelectorAll<HTMLElement>(`[${viewAttr}]`).forEach(el => {
      const matches = el.getAttribute(viewAttr) === view
      el.classList.toggle(activeViewClass, matches)
      if (matches) {
        el.removeAttribute('hidden')
      } else {
        el.setAttribute('hidden', 'true')
      }
    })

    // Toggle nav items
    const activeNav = navSection ?? view
    document.querySelectorAll<HTMLElement>(`[${navAttr}]`).forEach(el => {
      el.classList.toggle(activeNavClass, el.getAttribute(navAttr) === activeNav)
    })
  }

  // -----------------------------------------------------------------------
  // Link interception & popstate
  // -----------------------------------------------------------------------

  start(): void {
    if (this.started) return
    this.started = true

    if (this.config.interceptLinks) {
      this.clickHandler = (e: MouseEvent) => {
        // Skip if modifier keys (open in new tab behavior)
        if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return
        // Skip right/middle click
        if (e.button !== 0) return

        const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[href]')
        if (!anchor) return

        // Skip external links
        if (anchor.origin !== window.location.origin) return
        // Skip target="_blank" or similar
        if (anchor.target === '_blank' || anchor.target === '_parent' || anchor.target === '_top')
          return
        // Skip data-no-router links
        if (anchor.hasAttribute('data-no-router')) return
        // Skip download links
        if (anchor.hasAttribute('download')) return

        const path = anchor.pathname
        // Only intercept if we have a matching route
        if (this.matchRoute(path)) {
          e.preventDefault()
          void this.navigate(path, { pushState: true })
        }
      }
      document.addEventListener('click', this.clickHandler)
    }

    this.popstateHandler = () => {
      void this.navigate(window.location.pathname, { pushState: false })
    }
    window.addEventListener('popstate', this.popstateHandler)

    // Resolve the current route on start and initialize the matching module
    const route = this.matchRoute(window.location.pathname)
    if (route) {
      this.currentRoute = route
      this.currentAppPrefix = route.appPrefix
      void this.loadAndInitModule(route)
    }
  }

  destroy(): void {
    if (this.clickHandler) {
      document.removeEventListener('click', this.clickHandler)
      this.clickHandler = null
    }
    if (this.popstateHandler) {
      window.removeEventListener('popstate', this.popstateHandler)
      this.popstateHandler = null
    }
    this.started = false
    this.listeners.clear()
  }

  // -----------------------------------------------------------------------
  // Events
  // -----------------------------------------------------------------------

  on(event: 'navigate', handler: (e: NavigationEvent) => void): () => void {
    let handlers = this.listeners.get(event)
    if (!handlers) {
      handlers = new Set()
      this.listeners.set(event, handlers)
    }
    handlers.add(handler)
    return () => {
      handlers.delete(handler)
    }
  }

  private emit(event: string, data: NavigationEvent): void {
    const handlers = this.listeners.get(event)
    if (handlers) {
      for (const handler of handlers) {
        handler(data)
      }
    }
  }

  // -----------------------------------------------------------------------
  // History helpers
  // -----------------------------------------------------------------------

  private pushOrReplace(path: string, replace: boolean): void {
    // Save scroll position for the current entry
    const scrollState = { scrollX: window.scrollX, scrollY: window.scrollY }
    window.history.replaceState(
      { ...window.history.state, ...scrollState },
      '',
      window.location.href
    )
    if (replace) {
      window.history.replaceState({}, '', path)
    } else {
      window.history.pushState({}, '', path)
    }
  }

  private setCurrentRoute(route: RouteMatch): void {
    this.currentRoute = route
    this.currentAppPrefix = route.appPrefix
  }

  private isSameOriginPath(path: string): boolean {
    try {
      return new URL(path, window.location.origin).origin === window.location.origin
    } catch {
      return false
    }
  }

  private prepareFetchedHtml(html: string): string {
    if (!this.config.sanitizeFetchedHtml) {
      return html
    }
    return sanitizeHtml(html, { allowSvg: true })
  }

  private prepareFetchedElement<T extends Element>(element: T): T | null {
    if (!this.config.sanitizeFetchedHtml) {
      return element
    }

    const template = document.createElement('template')
    template.innerHTML = this.prepareFetchedHtml(element.outerHTML)
    return template.content.firstElementChild as T | null
  }

  // -----------------------------------------------------------------------
  // Clear loaded module cache (useful for testing)
  // -----------------------------------------------------------------------

  clearModuleCache(): void {
    this.loadedModules.clear()
  }
}

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

export function createRouter(config?: RouterConfig): Router {
  return new Router(config)
}
