import { Request, Response } from 'express';
import { sendError, sendSuccess } from '../utils/response.utils';

interface PostalPlace {
  city: string;
  state: string;
  stateCode: string;
  country: string;
  countryCode: string;
  latitude?: string;
  longitude?: string;
}

interface PostalLookupResult {
  country: string;
  countryCode: string;
  postalCode: string;
  places: PostalPlace[];
  source: 'zippopotam' | 'nominatim';
}

interface CacheEntry {
  expiresAt: number;
  result: PostalLookupResult;
}

const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const CACHE_MAX_ENTRIES = 1000;
const lookupCache = new Map<string, CacheEntry>();

let nominatimQueue: Promise<void> = Promise.resolve();
let lastNominatimRequestAt = 0;

const normalisePostalCode = (value: string) => value.replace(/[\s-]/g, '').toUpperCase();

const normalisePlace = (place: PostalPlace): PostalPlace => {
  // Postal datasets often return a Delhi neighbourhood as the city and
  // "New Delhi" as the state. Checkout/shipping forms need the postal city
  // and state/UT instead: New Delhi, Delhi.
  if (place.countryCode === 'IN' && /^(new\s*)?delhi$/i.test(place.state.trim())) {
    return { ...place, city: 'New Delhi', state: 'Delhi' };
  }
  return place;
};

const uniquePlaces = (places: PostalPlace[]) => {
  const seen = new Set<string>();
  return places.map(normalisePlace).filter((place) => {
    const key = `${place.city}|${place.state}|${place.countryCode}`.toLocaleLowerCase();
    if (!place.city || seen.has(key)) return false;
    seen.add(key);
    return true;
  });
};

const fetchWithTimeout = async (url: string, init: RequestInit = {}, timeoutMs = 7000) => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await fetch(url, { ...init, signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
};

const lookupWithZippopotam = async (countryCode: string, postalCode: string) => {
  const url = `https://api.zippopotam.us/${countryCode.toLowerCase()}/${encodeURIComponent(postalCode)}`;
  const response = await fetchWithTimeout(url, {
    headers: { Accept: 'application/json' },
  });

  if (response.status === 404) return null;
  if (!response.ok) throw new Error(`Zippopotam returned ${response.status}`);

  const payload = (await response.json()) as {
    country?: string;
    'country abbreviation'?: string;
    places?: Array<{
      'place name'?: string;
      state?: string;
      'state abbreviation'?: string;
      latitude?: string;
      longitude?: string;
    }>;
  };

  const resolvedCountryCode = (payload['country abbreviation'] || countryCode).toUpperCase();
  const places = uniquePlaces(
    (payload.places || []).map((place) => ({
      city: place['place name']?.trim() || '',
      state: place.state?.trim() || '',
      stateCode: place['state abbreviation']?.trim() || '',
      country: payload.country?.trim() || '',
      countryCode: resolvedCountryCode,
      latitude: place.latitude,
      longitude: place.longitude,
    }))
  );

  if (!places.length) return null;

  return {
    country: payload.country?.trim() || places[0].country,
    countryCode: resolvedCountryCode,
    postalCode,
    places,
    source: 'zippopotam' as const,
  };
};

const waitForNominatimSlot = async () => {
  let releaseQueue: () => void = () => undefined;
  const previousRequest = nominatimQueue;
  nominatimQueue = new Promise<void>((resolve) => {
    releaseQueue = resolve;
  });

  await previousRequest;
  const delayMs = Math.max(0, 1000 - (Date.now() - lastNominatimRequestAt));
  if (delayMs > 0) {
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  lastNominatimRequestAt = Date.now();
  releaseQueue();
};

const lookupWithNominatim = async (countryCode: string, postalCode: string) => {
  await waitForNominatimSlot();

  const params = new URLSearchParams({
    format: 'jsonv2',
    addressdetails: '1',
    limit: '5',
    countrycodes: countryCode.toLowerCase(),
    postalcode: postalCode,
  });
  const response = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?${params}`, {
    headers: {
      Accept: 'application/json',
      'Accept-Language': 'en',
      'User-Agent':
        process.env.POSTAL_LOOKUP_USER_AGENT ||
        'IndianBooksWorldwide/1.0 (https://indianbooksworldwide.com)',
    },
  });

  if (!response.ok) throw new Error(`Nominatim returned ${response.status}`);

  const payload = (await response.json()) as Array<{
    lat?: string;
    lon?: string;
    address?: Record<string, string | undefined>;
  }>;
  const requestedPostalCode = normalisePostalCode(postalCode);

  const places = uniquePlaces(
    payload
      .filter((result) => {
        const returnedPostalCodes = (result.address?.postcode || '')
          .split(';')
          .map((value) => normalisePostalCode(value));
        return !returnedPostalCodes.length || returnedPostalCodes.includes(requestedPostalCode);
      })
      .map((result) => {
        const address = result.address || {};
        return {
          city:
            address.city ||
            address.town ||
            address.village ||
            address.municipality ||
            address.hamlet ||
            address.city_district ||
            address.county ||
            '',
          state: address.state || address.region || address.state_district || '',
          stateCode: address['ISO3166-2-lvl4'] || address['ISO3166-2-lvl3'] || '',
          country: address.country || '',
          countryCode: (address.country_code || countryCode).toUpperCase(),
          latitude: result.lat,
          longitude: result.lon,
        };
      })
  );

  if (!places.length) return null;

  return {
    country: places[0].country,
    countryCode: places[0].countryCode || countryCode,
    postalCode,
    places,
    source: 'nominatim' as const,
  };
};

const saveToCache = (key: string, result: PostalLookupResult) => {
  if (lookupCache.size >= CACHE_MAX_ENTRIES) {
    const oldestKey = lookupCache.keys().next().value as string | undefined;
    if (oldestKey) lookupCache.delete(oldestKey);
  }
  lookupCache.set(key, { result, expiresAt: Date.now() + CACHE_TTL_MS });
};

export const lookupPostalCode = async (req: Request, res: Response) => {
  const countryCode = typeof req.query.country === 'string' ? req.query.country.trim().toUpperCase() : '';
  const postalCode = typeof req.query.postalCode === 'string' ? req.query.postalCode.trim() : '';

  if (!/^[A-Z]{2}$/.test(countryCode)) {
    return sendError(res, 'A valid two-letter country code is required', null, 400);
  }
  if (!/^[\p{L}\p{N}\s-]{2,16}$/u.test(postalCode)) {
    return sendError(res, 'Enter a valid postal code or ZIP code', null, 400);
  }

  const cacheKey = `${countryCode}:${normalisePostalCode(postalCode)}`;
  const cached = lookupCache.get(cacheKey);
  if (cached && cached.expiresAt > Date.now()) {
    return sendSuccess(res, 'Postal code found', { ...cached.result, cached: true });
  }
  if (cached) lookupCache.delete(cacheKey);

  let upstreamUnavailable = false;

  try {
    const zippopotamResult = await lookupWithZippopotam(countryCode, postalCode);
    if (zippopotamResult) {
      saveToCache(cacheKey, zippopotamResult);
      return sendSuccess(res, 'Postal code found', zippopotamResult);
    }
  } catch {
    upstreamUnavailable = true;
  }

  try {
    const nominatimResult = await lookupWithNominatim(countryCode, postalCode);
    if (nominatimResult) {
      saveToCache(cacheKey, nominatimResult);
      return sendSuccess(res, 'Postal code found', nominatimResult);
    }
  } catch {
    upstreamUnavailable = true;
  }

  if (upstreamUnavailable) {
    return sendError(res, 'Postal lookup is temporarily unavailable. Enter city and state manually.', null, 503);
  }

  return sendError(res, 'Postal code not found. Check the country and code, or enter city and state manually.', null, 404);
};
