import { client } from '@civility/workers'
import { createLayoutRouter } from '@civility/ui'
import { getShare } from '@civility/share'
import { setMapNav } from './utils/nav.ts'
import { parseBounds, pointInBounds } from './utils/bounds.ts'
import app, { APP_ID } from './store/app.ts'
import { asZodType, SharedList } from './store/schema.ts'
import './routes/map.ts'
import './components/m-map.ts'
import './components/m-bookmark-header.ts'
import './components/m-bookmarks-header.ts'
import './components/m-header.ts'
import './components/m-list-header.ts'
import './components/m-lightbox.ts'
import './components/m-welcome.ts'
import './routes/search.ts'
import './routes/settings.ts'
import './routes/settings-downloads.ts'
import './routes/settings-about.ts'
import './routes/bookmarks.ts'
import './routes/share-view.ts'
import './routes/bookmark.ts'
import './routes/list.ts'

// A share link is `<app-url>?id=<id>&server=<server>`. When those params are
// present, render the shared location read-only — no account, no router or
// service-worker boot (the local store still initializes as a module side
// effect) — instead of the full app. Static-host friendly: no server-side
// rewrite needed, and hash routing is untouched (this only inspects
// `location.search`).
const shareParams = new URLSearchParams(globalThis.location.search)
const shareId = shareParams.get('id')
const shareServer = shareParams.get('server')

const importId = shareParams.get('import')
const importBounds = shareParams.get('bounds')

if (shareId && shareServer) {
  renderShareView(shareId, shareServer, {
    lat: shareParams.get('lat'),
    lng: shareParams.get('lng'),
    zoom: shareParams.get('zoom'),
    transit: shareParams.get('transit'),
  })
} else {
  applyLocationParams(shareParams)
  bootApp()
  if (importId && shareServer) {
    void importSharedList(importId, shareServer, importBounds)
  }
}

/**
 * `?import=<shareId>&server=<server>&bounds=<w>,<s>,<e>,<n>` — the handoff
 * from the public list viewer's "Add to Bookmarks". The share is re-fetched
 * and re-validated here rather than handed over from the viewer, because the
 * viewer never boots the app store and the payload came from an untrusted
 * server either way. `bounds` is optional — present only when the visitor
 * chose "visible now" over "all" in the viewer's import dialog — and narrows
 * which locations get imported to whatever was on screen at that moment.
 */
async function importSharedList(
  id: string,
  server: string,
  boundsParam: string | null,
): Promise<void> {
  const { pathname, hash } = globalThis.location
  history.replaceState(null, '', `${pathname}${hash}`)
  try {
    const list = await getShare(APP_ID, id, {
      server,
      schema: asZodType(SharedList),
    })
    const bounds = parseBounds(boundsParam)
    const bookmarks = bounds
      ? list.bookmarks.filter((b) => pointInBounds(b.coordinates, bounds))
      : list.bookmarks
    const { listId, skipped } = await app.importSharedList({
      ...list,
      bookmarks,
    })
    if (skipped > 0) {
      globalThis.alert(
        skipped === 1
          ? '1 location was already in your bookmarks and was skipped.'
          : `${skipped} locations were already in your bookmarks and were skipped.`,
      )
    }
    globalThis.location.hash = `#!/list/${listId}`
  } catch (e) {
    console.error('Could not import shared list:', e)
    globalThis.alert(
      e instanceof Error
        ? `Could not import that list: ${e.message}`
        : 'Could not import that list.',
    )
  }
}

/**
 * `?lat=&lng=&zoom=&name=&address=` opens the app on a location — the handoff
 * from the public share viewer's "Open in MapsApp" link. Queued before boot so
 * the map picks it up on its first `load`, and stripped from the URL so a
 * refresh doesn't re-drop the pin. Coordinates are validated here because the
 * link may have been hand-edited or passed around.
 */
function applyLocationParams(params: URLSearchParams): void {
  const lat = Number(params.get('lat'))
  const lng = Number(params.get('lng'))
  if (!params.has('lat') || !params.has('lng')) return
  if (!Number.isFinite(lat) || lat < -90 || lat > 90) return
  if (!Number.isFinite(lng) || lng < -180 || lng > 180) return

  const zoom = Number(params.get('zoom'))
  setMapNav({
    lat,
    lng,
    zoom: Number.isFinite(zoom) && zoom > 0 && zoom <= 22 ? zoom : 14,
    marker: true,
    name: params.get('name') ?? undefined,
    address: params.get('address') ?? undefined,
  })
  const { pathname, hash } = globalThis.location
  history.replaceState(null, '', `${pathname}${hash}`)
}

function renderShareView(
  id: string,
  server: string,
  view: {
    lat: string | null
    lng: string | null
    zoom: string | null
    transit: string | null
  },
): void {
  document.querySelector('footer')?.remove()
  document.querySelector('m-map')?.remove()
  document.querySelector('m-welcome')?.remove()
  const main = document.querySelector('main')
  if (!main) return
  main.innerHTML = ''
  const el = document.createElement('r-share-view')
  el.setAttribute('share-id', id)
  el.setAttribute('server', server)
  // `lat`/`lng`/`zoom`/`transit` — a list share published with "current view"
  // (`app.publishList`'s `useCurrentView`) so the viewer opens on the
  // sharer's own framing instead of zoomed out to fit every location.
  if (view.lat) el.setAttribute('lat', view.lat)
  if (view.lng) el.setAttribute('lng', view.lng)
  if (view.zoom) el.setAttribute('zoom', view.zoom)
  if (view.transit) el.setAttribute('transit', view.transit)
  main.appendChild(el)
}

function bootApp(): void {
  client.init()
  client.watchForUpdates()
  initRouter()
}

interface NavMeta {
  navActive?: string
}

function initRouter(): void {
  const { ready } = createLayoutRouter<NavMeta>({
    router: { selectorAttrib: 'data-route', useHash: true },
    defaultRoute: '/',
    landmarks: {
      header: 'body > header',
      main: 'main',
    },
    afterMount: (ctx, view) => {
      const { path } = ctx as { path: string }

      // The persistent `m-map` only backs `/` — `/list/{id}` has its own
      // mini-map (`r-list`) now, and every other route has no map at all.
      const mapEl = document.querySelector<HTMLElement>('m-map')
      if (mapEl) mapEl.hidden = path !== '/'

      document.querySelectorAll('ui-bottom-bar a').forEach((link) => {
        if (
          view.meta?.navActive &&
          link.getAttribute('href') === view.meta.navActive
        ) {
          link.setAttribute('aria-current', 'page')
        } else {
          link.removeAttribute('aria-current')
        }
      })
    },
    routes: {
      '/': {
        landmarks: { main: 'r-home' },
        meta: { navActive: '/' },
      },
      '/bookmark/{id}': {
        landmarks: {
          main: (ctx) => ({
            tag: 'r-bookmark',
            attrs: { 'bookmark-id': ctx.params.id },
          }),
          header: (ctx) => ({
            tag: 'm-bookmark-header',
            attrs: { 'bookmark-id': ctx.params.id },
          }),
        },
      },
      // The list's own detail page — a mini-map plus its bookmarks, not the
      // shared `m-map` (which stays hidden here, see `afterMount`).
      '/list/{id}': {
        landmarks: {
          main: (ctx) => ({
            tag: 'r-list',
            attrs: { 'list-id': ctx.params.id },
          }),
          header: (ctx) => ({
            tag: 'm-list-header',
            attrs: { 'list-id': ctx.params.id },
          }),
        },
      },
      '/search': {
        landmarks: {
          main: 'r-search',
          header: () => ({
            tag: 'm-header',
            attrs: { heading: 'Search' },
          }),
        },
        meta: { navActive: '/search' },
      },
      '/settings': {
        landmarks: {
          main: 'r-settings',
          header: () => ({
            tag: 'm-header',
            attrs: { heading: 'Settings' },
          }),
        },
        meta: { navActive: '/settings' },
      },
      '/settings/downloads': {
        landmarks: {
          main: 'r-settings-downloads',
          header: () => ({
            tag: 'm-header',
            attrs: {
              heading: 'Offline Maps',
              back: '',
              'fallback-href': '#!/settings',
            },
          }),
        },
      },
      '/settings/about': {
        landmarks: {
          main: 'r-settings-about',
          header: () => ({
            tag: 'm-header',
            attrs: {
              heading: 'About',
              back: '',
              'fallback-href': '#!/settings',
            },
          }),
        },
      },
      '/bookmarks': {
        landmarks: {
          main: 'r-bookmarks',
          header: () => ({ tag: 'm-bookmarks-header' }),
        },
        meta: { navActive: '/bookmarks' },
      },
    },
  })

  ready()
}
