'use strict';

const {
  normalizeLocationInput,
  shouldUseLocationForMessage,
  summarizeLocation,
} = require('./location-awareness');

function _firstHeaderValue(value) {
  if (Array.isArray(value)) value = value[0];
  return String(value || '').split(',')[0].trim();
}

function _normalizeClientIp(value) {
  let ip = String(value || '').trim();
  if (!ip) return '';
  if (ip.startsWith('::ffff:')) ip = ip.slice(7);
  if (ip === '::1') return '127.0.0.1';
  return ip;
}

function _clientIpFromRequest(req) {
  const headers = req?.headers || {};
  return _normalizeClientIp(
    _firstHeaderValue(headers['cf-connecting-ip'])
      || _firstHeaderValue(headers['x-real-ip'])
      || _firstHeaderValue(headers['fly-client-ip'])
      || _firstHeaderValue(headers['x-forwarded-for'])
      || req?.socket?.remoteAddress
      || '',
  );
}

function _isPrivateOrLocalIp(ip) {
  const value = _normalizeClientIp(ip);
  if (!value) return true;
  if (value === '127.0.0.1' || value === 'localhost' || value === '::1') return true;
  if (/^10\./.test(value)) return true;
  if (/^192\.168\./.test(value)) return true;
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(value)) return true;
  if (/^169\.254\./.test(value)) return true;
  if (/^(fc|fd)/i.test(value)) return true;
  return false;
}

function _withQueryParam(url, key, value) {
  if (!value) return url;
  const sep = url.includes('?') ? '&' : '?';
  return `${url}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}

function _ipLocationLookupCandidates(ip) {
  const target = _isPrivateOrLocalIp(ip) ? '' : encodeURIComponent(ip);
  const candidates = [];
  if (process.env.WALLE_IP_LOCATION_URL) {
    candidates.push({ name: 'custom', url: process.env.WALLE_IP_LOCATION_URL });
  }

  const ipinfoToken = process.env.WALLE_IPINFO_TOKEN || process.env.IPINFO_TOKEN || '';
  const ipinfoUrl = target
    ? `https://ipinfo.io/${target}/json`
    : 'https://ipinfo.io/json';
  candidates.push({ name: 'ipinfo', url: _withQueryParam(ipinfoUrl, 'token', ipinfoToken) });
  candidates.push({ name: 'ipwho.is', url: target ? `https://ipwho.is/${target}` : 'https://ipwho.is/' });
  candidates.push({ name: 'ipapi.co', url: target ? `https://ipapi.co/${target}/json/` : 'https://ipapi.co/json/' });
  candidates.push({ name: 'ipwho.org', url: `https://api.ipwho.org/${target || 'me'}` });
  return candidates;
}

function _extractLocationPayload(data) {
  if (!data || typeof data !== 'object') return null;
  if (data.success === false) {
    throw new Error(String(data.message || data.reason || data.error || 'lookup failed'));
  }
  if (data.error || data.reason) {
    throw new Error(String(data.reason || data.error));
  }
  const payload = data.geo && typeof data.geo === 'object'
    ? data.geo
    : data.data && typeof data.data === 'object'
      ? data.data
      : data.location && typeof data.location === 'object'
        ? data.location
        : data;
  if (payload && payload.success === false) {
    throw new Error(String(payload.message || payload.reason || payload.error || 'lookup failed'));
  }
  if (payload && (payload.error || payload.reason)) {
    throw new Error(String(payload.reason || payload.error));
  }
  return payload;
}

function _coordinatesFromPayload(payload) {
  let lat = payload?.latitude ?? payload?.lat;
  let lon = payload?.longitude ?? payload?.lon ?? payload?.lng;
  if ((lat == null || lon == null) && typeof payload?.loc === 'string') {
    const parts = payload.loc.split(',').map((part) => Number(part.trim()));
    if (parts.length === 2 && Number.isFinite(parts[0]) && Number.isFinite(parts[1])) {
      [lat, lon] = parts;
    }
  }
  return { lat, lon };
}

function _locationFromIpData(data, providerName, ttlMs) {
  const payload = _extractLocationPayload(data);
  if (!payload) throw new Error('empty lookup response');
  const { lat, lon } = _coordinatesFromPayload(payload);
  const city = payload.city || payload.locality || payload.town || '';
  const region = payload.region || payload.region_name || payload.regionName || payload.region_code || '';
  const country = payload.country_name || payload.country || payload.country_code || payload.countryCode || '';
  const hasCoords = Number.isFinite(Number(lat)) && Number.isFinite(Number(lon));
  if (!hasCoords && !city) throw new Error('lookup response lacks usable city/coordinates');
  const label = [city, region, country].filter(Boolean).join(', ');
  const accuracyKm = Number(payload.accuracy_radius ?? payload.accuracyRadius);
  const location = normalizeLocationInput({
    lat,
    lon,
    label,
    accuracy_m: Number.isFinite(accuracyKm) ? accuracyKm * 1000 : (payload.accuracy_m || payload.accuracy || undefined),
    source: 'ip_inferred',
    precision: 'coarse',
    permission_state: 'unknown',
    captured_at: new Date().toISOString(),
    ttl_ms: ttlMs || 2 * 60 * 60 * 1000,
  });
  location.ip_provider = providerName;
  return location;
}

async function inferLocationFromIp(ip, opts = {}) {
  if (typeof fetch !== 'function') throw new Error('IP location lookup is unavailable');
  const timeoutMs = Number(opts.timeoutMs || process.env.WALLE_IP_LOCATION_TIMEOUT_MS || 2500);
  const candidates = opts.url
    ? [{ name: 'custom', url: opts.url }]
    : (opts.candidates || _ipLocationLookupCandidates(ip));
  const errors = [];
  for (const candidate of candidates) {
    try {
      const signal = AbortSignal.timeout(Math.max(500, Math.min(10000, timeoutMs)));
      const resp = await fetch(candidate.url, {
        headers: { Accept: 'application/json' },
        signal,
      });
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      const data = await resp.json();
      return _locationFromIpData(data, candidate.name, opts.ttlMs);
    } catch (e) {
      errors.push(`${candidate.name}: ${e && e.message ? e.message : String(e)}`);
    }
  }
  throw new Error(`IP location lookup failed: ${errors.join('; ')}`);
}

function inferLocationFromRequest(req, opts = {}) {
  return inferLocationFromIp(_clientIpFromRequest(req), opts);
}

function inferLocationFromNetwork(opts = {}) {
  return inferLocationFromIp('', opts);
}

function _locationSummaryFromContext(context) {
  const raw = context?.location || context?.currentLocation || null;
  if (!raw) return null;
  try {
    return summarizeLocation(normalizeLocationInput(raw, { source: 'chat_context' }));
  } catch (e) {
    return null;
  }
}

function _locationSummaryFromBrain(brain, opts = {}) {
  try {
    const existing = brain.getCurrentLocation?.();
    return summarizeLocation(existing);
  } catch (e) {
    if (opts.log !== false) console.warn('[location-inference] Ignoring stored current location:', e.message);
    return null;
  }
}

function _capturedAtMs(summary) {
  const value = summary?.captured_at || summary?.capturedAt || '';
  const ts = Date.parse(value);
  return Number.isFinite(ts) ? ts : 0;
}

function _bestFreshLocation(summaries) {
  const fresh = summaries.filter((summary) => summary?.fresh);
  if (fresh.length === 0) return null;
  fresh.sort((a, b) => _capturedAtMs(b) - _capturedAtMs(a));
  return fresh[0];
}

async function resolveLocationForMessage(req, brain, message, opts = {}) {
  if (!shouldUseLocationForMessage(message, opts.topics || [])) return null;
  const contextSummary = _locationSummaryFromContext(opts.context);
  const brainSummary = _locationSummaryFromBrain(brain, opts);
  const existing = _bestFreshLocation([contextSummary, brainSummary]);
  if (existing) return existing;

  if (opts.allowNetworkLocationInference !== true) return null;
  try {
    const location = req
      ? await inferLocationFromRequest(req, opts)
      : await inferLocationFromNetwork(opts);
    brain.setCurrentLocation(location);
    return summarizeLocation(location);
  } catch (e) {
    if (opts.log !== false) console.error('[location-inference] IP location inference failed:', e.message);
    return null;
  }
}

async function ensureLocationForMessage(req, brain, message, opts = {}) {
  return resolveLocationForMessage(req, brain, message, {
    ...opts,
    allowNetworkLocationInference: true,
  });
}

module.exports = {
  ensureLocationForMessage,
  inferLocationFromIp,
  inferLocationFromNetwork,
  inferLocationFromRequest,
  resolveLocationForMessage,
  _internal: {
    _clientIpFromRequest,
    _ipLocationLookupCandidates,
    _locationFromIpData,
    _locationSummaryFromContext,
  },
};
