import type { FeatureMeta, FeatureFilters } from '../types';
import { INITIAL_RETRY_MS, MAX_RETRY_MS } from './consts';
import pb from './pocketbase';
import { getSchoolBackendFeatureName } from './school-filter';
import { getSpecificCrimeFeatureName } from './crime-filter';
import { getElectionVoteShareFeatureName } from './election-filter';
import { getEthnicityFeatureName } from './ethnicity-filter';
import { getPoiDistanceFeatureName } from './poi-distance-filter';

const SCREENSHOT_LANGUAGES = new Set(['en', 'fr', 'de', 'zh', 'hi', 'hu']);

export function logNonAbortError(label: string, error: unknown): void {
  if (error instanceof Error && error.name === 'AbortError') {
    return;
  }

  console.error(`${label}:`, error);
}

export function isAbortError(error: unknown): boolean {
  return error instanceof Error && error.name === 'AbortError';
}

/** Throw if response is not 2xx. Call before `.json()`. */
export function assertOk(res: Response, label: string): void {
  if (!res.ok) {
    throw new Error(`${label}: HTTP ${res.status} ${res.statusText}`);
  }
}

export function authHeaders(init?: RequestInit): RequestInit {
  const headers: Record<string, string> = {};
  if (pb.authStore.isValid && pb.authStore.token) {
    headers['Authorization'] = `Bearer ${pb.authStore.token}`;
  }
  if (!init) return { headers };
  const existing = init.headers as Record<string, string> | undefined;
  return { ...init, headers: { ...existing, ...headers } };
}

// --- Demo location -----------------------------------------------------------
// The visitor's chosen demo centre (their GPS location, with consent) is sent to
// the server on every API request as demoLat/demoLon so the server can build the
// free-zone box around it. Held module-side and injected by apiUrl(), so every
// gated data call carries it without threading it through each hook. Unset when
// the visitor declines (the server then defaults to Canary Wharf).

const DEMO_CENTER_KEY = 'demoCenter';

export type DemoCenter = { lat: number; lon: number };

let demoCenter: DemoCenter | null = null;

/** Endpoints whose access is gated by the demo free zone — only these need the
 *  visitor's demo centre, so we keep the coarse location out of every other URL. */
const DEMO_GATED_ENDPOINTS = new Set([
  'hexagons',
  'postcodes',
  'filter-counts',
  'hexagon-stats',
  'postcode-stats',
  'postcode-properties',
  'hexagon-properties',
  'journey',
  'actual-listings',
  'export',
]);

/** Set (or clear) the demo centre sent on subsequent gated API requests. */
export function setDemoCenter(center: DemoCenter | null): void {
  demoCenter = center;
}

/** Persist the visitor's demo-location choice for this browser session. */
export function persistDemoChoice(value: DemoCenter | 'declined'): void {
  try {
    sessionStorage.setItem(
      DEMO_CENTER_KEY,
      value === 'declined' ? 'declined' : JSON.stringify(value)
    );
  } catch {
    // sessionStorage unavailable (private mode / SSR) — re-prompting is fine.
  }
}

/** Read this session's demo-location choice: coords, 'declined', or null (unset). */
export function readDemoChoice(): DemoCenter | 'declined' | null {
  try {
    const raw = sessionStorage.getItem(DEMO_CENTER_KEY);
    if (!raw) return null;
    if (raw === 'declined') return 'declined';
    const parsed = JSON.parse(raw) as Partial<DemoCenter>;
    if (typeof parsed?.lat === 'number' && typeof parsed?.lon === 'number') {
      return { lat: parsed.lat, lon: parsed.lon };
    }
  } catch {
    // Ignore malformed/unavailable storage.
  }
  return null;
}

export function apiUrl(endpoint: string, params?: URLSearchParams): string {
  const path = endpoint.startsWith('/') ? endpoint : `/api/${endpoint}`;
  const merged = new URLSearchParams(params);
  if (demoCenter && DEMO_GATED_ENDPOINTS.has(endpoint)) {
    // Coarsen to ~1 km (the demo free-zone box is ~25 km wide) so a precise location
    // never reaches the server or its access logs.
    merged.set('demoLat', demoCenter.lat.toFixed(2));
    merged.set('demoLon', demoCenter.lon.toFixed(2));
  }
  const query = merged.toString();
  return query ? `${path}?${query}` : path;
}

function toSupportedLanguage(value: string | undefined): string | null {
  if (!value) return null;
  const lower = value.toLowerCase();
  if (SCREENSHOT_LANGUAGES.has(lower)) return lower;

  const prefix = lower.split('-')[0];
  if (SCREENSHOT_LANGUAGES.has(prefix)) return prefix;

  return null;
}

function browserLanguage(): string | null {
  if (typeof navigator === 'undefined') return null;
  const languages = navigator.languages?.length ? navigator.languages : [navigator.language];
  for (const language of languages) {
    const supported = toSupportedLanguage(language);
    if (supported) return supported;
  }
  return null;
}

export function paramsWithLanguage(params: string, language?: string): string {
  const qs = new URLSearchParams(params.replace(/^\?/, ''));
  if (!qs.has('lang')) {
    const supported = toSupportedLanguage(language) ?? browserLanguage();
    if (supported) qs.set('lang', supported);
  }
  return qs.toString();
}

export async function fetchWithRetry<T>(
  url: string,
  onSuccess: (data: T) => void,
  signal: AbortSignal
): Promise<void> {
  let delay = INITIAL_RETRY_MS;
  while (!signal.aborted) {
    try {
      const res = await fetch(url, authHeaders({ signal }));
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      onSuccess(json);
      return;
    } catch (err) {
      if (signal.aborted) return;
      console.error(`Failed to fetch ${url}, retrying in ${delay}ms:`, err);
      await new Promise((resolve) => setTimeout(resolve, delay));
      delay = Math.min(delay * 2, MAX_RETRY_MS);
    }
  }
}

/** Fire-and-forget request to pre-warm the screenshot cache for OG images. */
export function prewarmScreenshot(params: string, language?: string): void {
  const qs = new URLSearchParams(paramsWithLanguage(params, language));
  qs.set('og', '1');
  fetch(apiUrl('screenshot', qs), authHeaders()).catch(() => {}); // best-effort, don't care if it fails
}

export async function shortenUrl(params: string, language?: string): Promise<string> {
  const res = await fetch(
    apiUrl('shorten'),
    authHeaders({
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ params: paramsWithLanguage(params, language) }),
    })
  );
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  return `${window.location.origin}${data.url}`;
}

export function buildFilterString(
  filters: FeatureFilters,
  features: FeatureMeta[],
  exclude?: string
): string {
  const entries = Object.entries(filters);
  if (entries.length === 0) return '';

  const merged = new Map<string, [number, number] | string[]>();
  for (const [name, value] of entries) {
    if (name === exclude) continue;
    const backendName =
      getSchoolBackendFeatureName(name) ??
      getSpecificCrimeFeatureName(name) ??
      getElectionVoteShareFeatureName(name) ??
      getEthnicityFeatureName(name) ??
      getPoiDistanceFeatureName(name) ??
      name;
    const prev = merged.get(backendName);
    if (
      prev &&
      Array.isArray(prev) &&
      Array.isArray(value) &&
      typeof prev[0] === 'number' &&
      typeof value[0] === 'number'
    ) {
      merged.set(backendName, [
        Math.max(prev[0] as number, value[0] as number),
        Math.min(prev[1] as number, value[1] as number),
      ]);
    } else if (!prev) {
      merged.set(backendName, value);
    }
  }

  return [...merged.entries()]
    .map(([name, value]) => {
      const meta = features.find((f) => f.name === name);
      if (meta?.type === 'enum') {
        return `${name}:${(value as string[]).join('|')}`;
      }
      const [min, max] = value as [number, number];
      const isAtMax = meta?.histogram ? max >= meta.histogram.max : max === meta?.max;
      const maxStr = meta?.absolute && isAtMax ? 'inf' : String(max);
      return `${name}:${min}:${maxStr}`;
    })
    .join(';;');
}
