"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.lookupPostalCode = void 0;
const response_utils_1 = require("../utils/response.utils");
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const CACHE_MAX_ENTRIES = 1000;
const lookupCache = new Map();
let nominatimQueue = Promise.resolve();
let lastNominatimRequestAt = 0;
const normalisePostalCode = (value) => value.replace(/[\s-]/g, '').toUpperCase();
const normalisePlace = (place) => {
    // 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) => {
    const seen = new Set();
    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, init = {}, 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, postalCode) => {
    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());
    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',
    };
};
const waitForNominatimSlot = async () => {
    let releaseQueue = () => undefined;
    const previousRequest = nominatimQueue;
    nominatimQueue = new Promise((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, postalCode) => {
    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());
    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',
    };
};
const saveToCache = (key, result) => {
    if (lookupCache.size >= CACHE_MAX_ENTRIES) {
        const oldestKey = lookupCache.keys().next().value;
        if (oldestKey)
            lookupCache.delete(oldestKey);
    }
    lookupCache.set(key, { result, expiresAt: Date.now() + CACHE_TTL_MS });
};
const lookupPostalCode = async (req, res) => {
    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 (0, response_utils_1.sendError)(res, 'A valid two-letter country code is required', null, 400);
    }
    if (!/^[\p{L}\p{N}\s-]{2,16}$/u.test(postalCode)) {
        return (0, response_utils_1.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 (0, response_utils_1.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 (0, response_utils_1.sendSuccess)(res, 'Postal code found', zippopotamResult);
        }
    }
    catch {
        upstreamUnavailable = true;
    }
    try {
        const nominatimResult = await lookupWithNominatim(countryCode, postalCode);
        if (nominatimResult) {
            saveToCache(cacheKey, nominatimResult);
            return (0, response_utils_1.sendSuccess)(res, 'Postal code found', nominatimResult);
        }
    }
    catch {
        upstreamUnavailable = true;
    }
    if (upstreamUnavailable) {
        return (0, response_utils_1.sendError)(res, 'Postal lookup is temporarily unavailable. Enter city and state manually.', null, 503);
    }
    return (0, response_utils_1.sendError)(res, 'Postal code not found. Check the country and code, or enter city and state manually.', null, 404);
};
exports.lookupPostalCode = lookupPostalCode;
//# sourceMappingURL=location.controller.js.map