import type { FeatureMeta, FeatureFilters, CrimeRecordsResponse } 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 { getCrimeSeverityFeatureName } from './crime-severity-filter';
import { getElectionVoteShareFeatureName } from './election-filter';
import { getEthnicityFeatureName } from './ethnicity-filter';
import { getQualificationFeatureName } from './qualification-filter';
import { getTenureFeatureName } from './tenure-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 } };
}

export function apiUrl(endpoint: string, params?: URLSearchParams): string {
  const path = endpoint.startsWith('/') ? endpoint : `/api/${endpoint}`;
  const query = params?.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}`;
}

/** Fetch one page of individual crime records for a hexagon or a postcode. The
 *  list is independent of property filters, so no filter string is sent. */
export async function fetchCrimeRecords(
  selection: { h3: string; resolution: number } | { postcode: string },
  offset: number,
  shareCode?: string
): Promise<CrimeRecordsResponse> {
  const params = new URLSearchParams({ offset: offset.toString() });
  if ('postcode' in selection) {
    params.set('postcode', selection.postcode);
  } else {
    params.set('h3', selection.h3);
    params.set('resolution', selection.resolution.toString());
  }
  if (shareCode) params.set('share', shareCode);
  const res = await fetch(apiUrl('crime-records', params), authHeaders());
  assertOk(res, 'crime-records');
  return res.json();
}

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) ??
      getCrimeSeverityFeatureName(name) ??
      getElectionVoteShareFeatureName(name) ??
      getEthnicityFeatureName(name) ??
      getQualificationFeatureName(name) ??
      getTenureFeatureName(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(';;');
}
