import { SNAPSHOT_MAX_AGE_MS, SNAPSHOT_STALE_AFTER_MS } from './src/config.js';
import { loadSnapshot } from './src/api.js';
import { applyPreferences, loadPreferences, updatePreference } from './src/preferences.js';
import { parseHash, routeHref } from './src/router.js';
import { closeSearchSuggest, commitSearchSuggest, moveSearchSuggest, renderView, renderWeather, syncMediaSliders } from './src/render.js';
import { loadWeather } from './src/weather.js';

const initialRoute = parseHash(globalThis.location ? location.hash : '');
let activeLoad = null;
let activeWeatherLoad = null;
let expiryTimer = null;

const state = {
  preferences: loadPreferences(),
  route: initialRoute,
  guides: [],
  categories: [],
  types: [],
  query: '',
  loading: true,
  refreshing: false,
  failed: false,
  offline: !navigator.onLine,
  source: 'network',
  savedAt: 0,
  stale: false,
  networkError: null,
  weather: null,
  weatherLoading: true,
  weatherError: false,
  weatherRegions: [],
  weatherRegionId: 0,
  catalogQuery: '',
  groupFilter: initialRoute.name === 'all' ? (initialRoute.category || 'ALL') : 'ALL',
  returnFocusId: '',
  focusNextHeading: initialRoute.name !== 'home'
};

function viewModel() {
  const age = state.savedAt ? Date.now() - state.savedAt : 0;
  return {
    ...state,
    stale: state.stale || age > SNAPSHOT_STALE_AFTER_MS,
    expired: Boolean(state.savedAt && age > SNAPSHOT_MAX_AGE_MS),
    locale: state.preferences.locale,
    preferences: state.preferences
  };
}

function updateStickyHeight() {
  const bar = document.getElementById('emergencyBar');
  const header = document.querySelector('.site-header');
  const barHeight = bar && !bar.hidden ? bar.getBoundingClientRect().height : 0;
  const headerHeight = header && document.documentElement.classList.contains('js') ? header.getBoundingClientRect().height : 0;
  document.documentElement.style.setProperty('--sticky-height', `${Math.ceil(barHeight + headerHeight)}px`);
}

function enforceSnapshotAge() {
  if (!state.guides.length || !state.savedAt || Date.now() - state.savedAt <= SNAPSHOT_MAX_AGE_MS) return;
  state.guides = [];
  state.source = 'expired';
  state.failed = true;
  state.networkError = new Error('Saved guide snapshot expired');
}

function scheduleSnapshotExpiry() {
  if (expiryTimer) clearTimeout(expiryTimer);
  if (!state.savedAt || !state.guides.length) return;
  const remaining = Math.max(0, SNAPSHOT_MAX_AGE_MS - (Date.now() - state.savedAt) + 100);
  expiryTimer = setTimeout(() => {
    enforceSnapshotAge();
    render();
  }, Math.min(remaining, 2_147_000_000));
}

function render(options) {
  const settings = options || {};
  const activeElement = document.activeElement;
  const activeId = activeElement && activeElement.id ? activeElement.id : '';
  const activeCardId = activeElement && activeElement.dataset ? activeElement.dataset.guideCard || '' : '';
  enforceSnapshotAge();
  document.body.dataset.route = state.route.name;
  placePrimaryCall(state.route.name);
  const heading = renderView(viewModel());
  syncMediaSliders(document);
  if (settings.measureSticky) updateStickyHeight();
  document.getElementById('clearSearch').hidden = !state.query;
  if (settings.focusHeading && heading) {
    state.focusNextHeading = false;
    const routeName = state.route.name;
    requestAnimationFrame(() => {
      if (!heading.isConnected || state.route.name !== routeName) return;
      const current = document.activeElement;
      const currentIsAvailable = !current || current === document.body || current === heading || current.id === 'detailRetryButton' || current.getClientRects().length === 0;
      if (!settings.forceFocus && !currentIsAvailable) return;
      heading.focus();
      const container = heading.closest('.detail, .catalog-view') || heading;
      container.scrollIntoView({ block: 'start', behavior: 'auto' });
    });
  }
  if (settings.restoreCard && state.returnFocusId) {
    const id = state.returnFocusId;
    requestAnimationFrame(() => {
      const card = document.querySelector(`[data-guide-card="${CSS.escape(id)}"]`);
      if (card) {
        state.returnFocusId = '';
        card.focus();
      }
    });
  } else if (!settings.focusHeading && (activeId || activeCardId)) {
    requestAnimationFrame(() => {
      const replacement = activeId
        ? document.getElementById(activeId)
        : document.querySelector(`[data-guide-card="${CSS.escape(activeCardId)}"]`);
      if (replacement && document.activeElement === document.body) replacement.focus();
    });
  }
}

function loadData(options) {
  if (activeLoad) return activeLoad;
  const settings = options || {};
  const hasLastGoodData = state.guides.length > 0;
  state.loading = !hasLastGoodData;
  state.refreshing = hasLastGoodData;
  if (!hasLastGoodData) state.failed = false;
  render({ measureSticky: !hasLastGoodData });

  activeLoad = (async () => {
    try {
      const result = await loadSnapshot(settings.fetchOptions);
      state.guides = result.snapshot.guides;
      state.categories = result.snapshot.categories;
      state.types = result.snapshot.types;
      state.source = result.source;
      state.savedAt = result.savedAt || 0;
      state.stale = Boolean(result.stale);
      state.networkError = result.networkError || null;
      state.failed = false;
      state.offline = !navigator.onLine;
    } catch (error) {
      state.networkError = error;
      state.offline = !navigator.onLine;
      if (state.guides.length) {
        state.source = 'memory';
        state.failed = false;
      } else {
        state.failed = true;
      }
    } finally {
      state.loading = false;
      state.refreshing = false;
      activeLoad = null;
      scheduleSnapshotExpiry();
      render({ focusHeading: state.focusNextHeading });
      /* First real render is done (success or failure), so the intro overlay
         can go instead of waiting for every image on the page. */
      if (window.__hideIntro) window.__hideIntro();
    }
  })();
  return activeLoad;
}

function loadWeatherData() {
  if (activeWeatherLoad) return activeWeatherLoad;
  state.weatherLoading = !state.weather;
  renderWeather(viewModel());
  activeWeatherLoad = (async () => {
    const result = await loadWeather({ regionCode: selectedRegionCode() });
    state.weatherRegions = result.regions;
    state.weather = result.weather;
    state.weatherRegionId = result.weather ? result.weather.regionId : 0;
    state.weatherError = !result.weather;
    state.weatherLoading = false;
    activeWeatherLoad = null;
    renderWeather(viewModel());
  })();
  return activeWeatherLoad;
}

function selectedRegionCode() {
  const id = Number(state.weatherRegionId);
  const region = state.weatherRegions.find((entry) => entry.id === id);
  return region ? region.code : '';
}

function setPreference(key, value) {
  state.preferences = updatePreference(state.preferences, key, value);
  applyPreferences(state.preferences);
  render();
}

function languageOptions() {
  const listbox = document.getElementById('languageListbox');
  return listbox ? Array.from(listbox.querySelectorAll('[role="option"]')) : [];
}

function openLanguageMenu(focusLast) {
  const listbox = document.getElementById('languageListbox');
  const trigger = document.getElementById('languageTrigger');
  const options = languageOptions();
  if (!listbox || !options.length) return;
  const selected = options.findIndex((option) => option.getAttribute('aria-selected') === 'true');
  listbox.hidden = false;
  trigger.setAttribute('aria-expanded', 'true');
  const index = focusLast ? options.length - 1 : Math.max(selected, 0);
  options[index].focus();
}

function closeLanguageMenu(restoreFocus) {
  const listbox = document.getElementById('languageListbox');
  const trigger = document.getElementById('languageTrigger');
  if (!listbox || listbox.hidden) return;
  listbox.hidden = true;
  trigger.setAttribute('aria-expanded', 'false');
  if (restoreFocus) trigger.focus();
}

function stepLanguageOption(delta) {
  const options = languageOptions();
  const current = options.indexOf(document.activeElement);
  if (!options.length || current < 0) return;
  options[(current + delta + options.length) % options.length].focus();
}

function selectLanguage(locale) {
  if (!locale || locale === state.preferences.locale) {
    closeLanguageMenu(true);
    return;
  }
  /* render() rebuilds the listbox, so the menu state is settled first and focus
     is handed back to the trigger once the new option exists. */
  closeLanguageMenu(false);
  setPreference('locale', locale);
  closeLanguageMenu(false);
  document.getElementById('languageTrigger').focus();
}

function handleRouteChange() {
  state.route = parseHash(location.hash);
  if (state.route.name === 'home') {
    state.catalogQuery = '';
    state.groupFilter = 'ALL';
  } else if (state.route.name === 'all') {
    state.groupFilter = state.route.category || 'ALL';
  }
  if (state.route.name === 'guide' || state.route.name === 'all' || state.route.name === 'not-found') {
    state.focusNextHeading = true;
    render({ focusHeading: true, measureSticky: true });
    return;
  }
  const restoreCard = state.route.name === 'home' && Boolean(state.returnFocusId);
  render({ restoreCard, focusHeading: state.route.name === 'home' && !restoreCard, forceFocus: state.route.name === 'home' && !restoreCard, measureSticky: true });
}

function bindEvents() {
  document.getElementById('themeToggle').addEventListener('click', () => {
    setPreference('theme', state.preferences.theme === 'dark' ? 'light' : 'dark');
  });

  const languageTrigger = document.getElementById('languageTrigger');
  const languageListbox = document.getElementById('languageListbox');
  languageTrigger.addEventListener('click', () => {
    if (languageListbox.hidden) openLanguageMenu(false);
    else closeLanguageMenu(true);
  });
  languageTrigger.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowDown') {
      event.preventDefault();
      openLanguageMenu(false);
    } else if (event.key === 'ArrowUp') {
      event.preventDefault();
      openLanguageMenu(true);
    }
  });
  languageListbox.addEventListener('click', (event) => {
    const option = event.target.closest('[role="option"]');
    if (option) selectLanguage(option.dataset.locale);
  });
  languageListbox.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowDown') {
      event.preventDefault();
      stepLanguageOption(1);
    } else if (event.key === 'ArrowUp') {
      event.preventDefault();
      stepLanguageOption(-1);
    } else if (event.key === 'Home') {
      event.preventDefault();
      languageOptions()[0].focus();
    } else if (event.key === 'End') {
      event.preventDefault();
      const options = languageOptions();
      if (options.length) options[options.length - 1].focus();
    } else if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      const option = event.target.closest('[role="option"]');
      if (option) selectLanguage(option.dataset.locale);
    } else if (event.key === 'Escape') {
      event.preventDefault();
      closeLanguageMenu(true);
    } else if (event.key === 'Tab') {
      closeLanguageMenu(false);
    }
  });
  /* Pointer users can dismiss by tapping elsewhere; keyboard users get Escape. */
  document.addEventListener('click', (event) => {
    if (languageListbox.hidden) return;
    if (!document.getElementById('languageMenu').contains(event.target)) closeLanguageMenu(false);
  });

  const search = document.getElementById('search');
  search.addEventListener('input', (event) => {
    state.query = event.target.value;
    if (state.route.name !== 'home') location.hash = '#/';
    render();
  });
  /* Type-ahead rows: Enter opens the highlighted guide, arrows move within the
     list, Escape dismisses it. Without a highlighted row Enter falls through to
     the form, which stays inert. */
  search.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
      if (!moveSearchSuggest(event.key === 'ArrowDown' ? 1 : -1)) return;
      event.preventDefault();
      return;
    }
    if (event.key === 'Enter' && commitSearchSuggest()) {
      event.preventDefault();
      return;
    }
    if (event.key === 'Escape') closeSearchSuggest();
  });
  document.getElementById('searchSuggest').addEventListener('click', (event) => {
    const option = event.target.closest('[role="option"]');
    if (!option || !option.dataset.suggestId) return;
    closeSearchSuggest();
    location.hash = routeHref('guide', option.dataset.suggestId);
  });
  /* Tapping outside the form dismisses the list, same as the language menu. */
  document.addEventListener('click', (event) => {
    const list = document.getElementById('searchSuggest');
    if (list.hidden || document.getElementById('searchForm').contains(event.target)) return;
    closeSearchSuggest();
  });
  document.getElementById('searchForm').addEventListener('submit', (event) => event.preventDefault());
  document.addEventListener('input', (event) => {
    if (!event.target || event.target.id !== 'allSearch') return;
    state.catalogQuery = event.target.value;
    render();
  });
  document.addEventListener('click', (event) => {
    const chip = event.target.closest('.filter-chip');
    if (chip) {
      state.groupFilter = chip.dataset.filter || 'ALL';
      render();
      return;
    }
    if (event.target.closest('#catalogReset')) {
      state.catalogQuery = '';
      state.groupFilter = 'ALL';
      render();
      const input = document.getElementById('allSearch');
      if (input) input.focus();
      return;
    }
    if (event.target.closest('#allClear')) {
      state.catalogQuery = '';
      render();
      const input = document.getElementById('allSearch');
      if (input) input.focus();
    }
  });
  document.getElementById('searchChips').addEventListener('click', (event) => {
    const chip = event.target.closest('button[data-search-chip]');
    if (!chip) return;
    const search = document.getElementById('search');
    search.value = chip.dataset.searchChip;
    search.dispatchEvent(new Event('input', { bubbles: true }));
    search.focus();
  });
  /* The card is rebuilt on every render, so the region picker is handled by
     delegation and its choice lives in state. */
  document.addEventListener('change', (event) => {
    const select = event.target.closest('#weatherRegion');
    if (!select) return;
    state.weatherRegionId = Number(select.value);
    state.weather = null;
    state.weatherLoading = true;
    renderWeather(viewModel());
    loadWeatherData();
  });
  document.getElementById('clearSearch').addEventListener('click', () => {
    state.query = '';
    search.value = '';
    render();
    search.focus();
  });
  document.getElementById('retryButton').addEventListener('click', () => loadData());
  document.addEventListener('click', (event) => {
    if (event.target.closest('#detailRetryButton')) {
      if (state.route.name === 'guide' || state.route.name === 'not-found') state.focusNextHeading = true;
      loadData();
    }
  });
  document.getElementById('skipLink').addEventListener('click', (event) => {
    event.preventDefault();
    document.getElementById('main').focus();
    document.getElementById('main').scrollIntoView({ block: 'start', behavior: 'auto' });
  });
  document.addEventListener('click', (event) => {
    const arrow = event.target.closest('[data-slider-prev], [data-slider-next]');
    if (!arrow) return;
    const slider = arrow.closest('[data-slider]');
    const track = slider ? slider.querySelector('[data-slider-track]') : null;
    if (!track) return;
    const direction = arrow.hasAttribute('data-slider-prev') ? -1 : 1;
    const tile = track.querySelector('.media-tile');
    const step = tile ? tile.getBoundingClientRect().width + 16 : Math.max(240, track.clientWidth * 0.8);
    track.scrollBy({ left: direction * step, behavior: 'smooth' });
  });
  document.addEventListener('scroll', () => syncMediaSliders(document), { passive: true, capture: true });
  document.getElementById('detail').addEventListener('load', () => syncMediaSliders(document), true);
  window.addEventListener('resize', () => syncMediaSliders(document));
  window.addEventListener('hashchange', handleRouteChange);
  window.addEventListener('online', () => {
    state.offline = false;
    loadData();
  });
  window.addEventListener('offline', () => {
    state.offline = true;
    render();
  });
  if ('ResizeObserver' in window) {
    const observer = new ResizeObserver(updateStickyHeight);
    const bar = document.getElementById('emergencyBar');
    const header = document.querySelector('.site-header');
    if (bar) observer.observe(bar);
    if (header) observer.observe(header);
  } else {
    window.addEventListener('resize', updateStickyHeight);
  }
}

function placePrimaryCall(routeName) {
  const call = document.querySelector('.call-tile-primary');
  const header = document.querySelector('.header-inner');
  const emergencyInner = document.querySelector('.emergency-inner');
  const headerControls = document.getElementById('headerControls');
  const emergencyBar = document.getElementById('emergencyBar');
  if (!call || !header || !emergencyInner || !headerControls || !emergencyBar) return;
  if (routeName === 'home') {
    if (!header.contains(call)) header.insertBefore(call, headerControls);
    emergencyBar.hidden = true;
  } else if (routeName === 'guide') {
    if (!emergencyInner.contains(call)) emergencyInner.appendChild(call);
    emergencyBar.hidden = true;
  } else {
    if (!emergencyInner.contains(call)) emergencyInner.appendChild(call);
    emergencyBar.hidden = false;
  }
}

function registerServiceWorker() {
  if (!('serviceWorker' in navigator) || !/^https?:$/.test(location.protocol)) return;
  const hadController = Boolean(navigator.serviceWorker.controller);
  let refreshing = false;
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    if (!hadController || refreshing) return;
    refreshing = true;
    location.reload();
  });
  navigator.serviceWorker.register('./sw.js', { scope: './', updateViaCache: 'none' }).catch(() => {});
}

function init() {
  document.documentElement.classList.add('js');
  state.preferences = applyPreferences(loadPreferences());
  bindEvents();
  render({ measureSticky: true });
  /* The pre-paint route hint in src/preferences-early.js has done its job. From
     here on the `hidden` attribute on #emergencyBar is the single source of truth,
     so dropping the attribute stops the early CSS gate from overriding a later
     client-side navigation to #/all or a not-found hash. */
  document.documentElement.removeAttribute('data-initial-route');
  loadData();
  loadWeatherData();
  registerServiceWorker();
}

if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
