import { FastifyRequest } from 'fastify';
import { config } from './config';
import { getRedis, REDIS_KEYS } from './redis';

export interface GeoInfo {
  ip: string;
  countryCode?: string;
  countryName?: string;
  city?: string;
  flag?: string;
  source: 'ip-api' | 'ipinfo' | 'local' | 'unknown';
}

const GEO_TTL_SEC = 60 * 60 * 24 * 7;

const COUNTRY_NAMES: Record<string, string> = {
  AD: 'Andorra', AE: 'United Arab Emirates', AF: 'Afghanistan', AG: 'Antigua and Barbuda',
  AI: 'Anguilla', AL: 'Albania', AM: 'Armenia', AO: 'Angola', AR: 'Argentina',
  AT: 'Austria', AU: 'Australia', AZ: 'Azerbaijan', BA: 'Bosnia and Herzegovina',
  BB: 'Barbados', BD: 'Bangladesh', BE: 'Belgium', BG: 'Bulgaria', BH: 'Bahrain',
  BR: 'Brazil', BY: 'Belarus', CA: 'Canada', CH: 'Switzerland', CL: 'Chile',
  CN: 'China', CO: 'Colombia', CY: 'Cyprus', CZ: 'Czechia', DE: 'Germany',
  DK: 'Denmark', EE: 'Estonia', ES: 'Spain', FI: 'Finland', FR: 'France',
  GB: 'United Kingdom', GE: 'Georgia', GR: 'Greece', HR: 'Croatia', HU: 'Hungary',
  IE: 'Ireland', IL: 'Israel', IN: 'India', IS: 'Iceland', IT: 'Italy',
  JP: 'Japan', KR: 'South Korea', KZ: 'Kazakhstan', LT: 'Lithuania', LU: 'Luxembourg',
  LV: 'Latvia', MD: 'Moldova', ME: 'Montenegro', MK: 'North Macedonia', MT: 'Malta',
  MX: 'Mexico', NL: 'Netherlands', NO: 'Norway', PL: 'Poland', PT: 'Portugal',
  RO: 'Romania', RS: 'Serbia', RU: 'Russia', SE: 'Sweden', SI: 'Slovenia',
  SK: 'Slovakia', TR: 'Turkey', UA: 'Ukraine', US: 'United States',
};

export function countryFlag(code?: string): string {
  const cc = normaliseCountryCode(code);
  if (!cc) return '';
  return cc
    .split('')
    .map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
    .join('');
}

export function countryLabel(code?: string | null, city?: string | null, name?: string | null): string {
  const cc = normaliseCountryCode(code ?? undefined);
  if (!cc) return city ? `🌐 Unknown, ${city}` : '🌐 Unknown';
  const parts = [`${countryFlag(cc)} ${name || COUNTRY_NAMES[cc] || cc} (${cc})`];
  if (city) parts.push(city);
  return parts.join(', ');
}

export function formatUserAgent(ua?: string | null): string {
  if (!ua) return 'Unknown device';
  const browser =
    ua.match(/Edg\/[\d.]+/)?.[0].replace('Edg/', 'Edge ') ||
    ua.match(/OPR\/[\d.]+/)?.[0].replace('OPR/', 'Opera ') ||
    ua.match(/Chrome\/[\d.]+/)?.[0].replace('Chrome/', 'Chrome ') ||
    ua.match(/Firefox\/[\d.]+/)?.[0].replace('Firefox/', 'Firefox ') ||
    (ua.includes('Safari/') ? `Safari ${ua.match(/Version\/([\d.]+)/)?.[1] ?? ''}`.trim() : '');
  const os =
    ua.match(/iPhone|iPad/)?.[0] ||
    ua.match(/Android [\d.]+/)?.[0] ||
    ua.match(/Windows NT [\d.]+/)?.[0]?.replace('Windows NT ', 'Windows ') ||
    ua.match(/Mac OS X [\d_]+/)?.[0]?.replace(/_/g, '.') ||
    ua.match(/Linux/)?.[0] ||
    '';
  const device = /Mobile|Android|iPhone|iPad/i.test(ua) ? 'Mobile' : 'Desktop';
  return [browser, os, device].filter(Boolean).join(' · ') || ua.slice(0, 80);
}

export function extractClientIp(request: FastifyRequest): string {
  const headers = request.headers;
  const candidates: string[] = [];
  addHeaderCandidates(candidates, headers['cf-connecting-ip']);
  addHeaderCandidates(candidates, headers['x-real-ip']);
  addHeaderCandidates(candidates, headers['x-forwarded-for']);
  addHeaderCandidates(candidates, request.ip);

  const publicIp = candidates.find((ip) => isPublicIp(ip));
  return publicIp ?? candidates[0] ?? request.ip;
}

export async function getGeo(ip: string): Promise<GeoInfo> {
  const cleanIp = cleanIpValue(ip);
  if (!cleanIp || isPrivateIp(cleanIp) || config.geoip.provider === 'none') {
    return {
      ip: cleanIp || ip,
      countryName: isPrivateIp(cleanIp) ? 'Local/Unknown' : 'Unknown',
      flag: '',
      source: isPrivateIp(cleanIp) ? 'local' : 'unknown',
    };
  }

  const redis = getRedis();
  const cacheKey = REDIS_KEYS.geoip(cleanIp);
  try {
    const cached = await redis.get(cacheKey);
    if (cached) return JSON.parse(cached) as GeoInfo;
  } catch (err) {
    console.error('[geoip] cache read error:', (err as Error).message);
  }

  const geo = await lookupGeo(cleanIp);
  try {
    await redis.setex(cacheKey, GEO_TTL_SEC, JSON.stringify(geo));
  } catch (err) {
    console.error('[geoip] cache write error:', (err as Error).message);
  }
  return geo;
}

async function lookupGeo(ip: string): Promise<GeoInfo> {
  if (config.geoip.provider === 'ipinfo' && config.geoip.token) {
    const fromIpinfo = await lookupIpinfo(ip);
    if (fromIpinfo) return fromIpinfo;
  }
  const fromIpApi = await lookupIpApi(ip);
  if (fromIpApi) return fromIpApi;
  return { ip, countryName: 'Unknown', flag: '', source: 'unknown' };
}

async function lookupIpApi(ip: string): Promise<GeoInfo | null> {
  try {
    const res = await fetch(
      `http://ip-api.com/json/${encodeURIComponent(ip)}?fields=status,country,countryCode,city,query`,
      { signal: AbortSignal.timeout(config.geoip.timeoutMs) },
    );
    if (!res.ok) return null;
    const data = (await res.json()) as {
      status?: string;
      country?: string;
      countryCode?: string;
      city?: string;
      query?: string;
    };
    if (data.status !== 'success') return null;
    const countryCode = normaliseCountryCode(data.countryCode);
    return {
      ip: data.query || ip,
      countryCode,
      countryName: data.country || (countryCode ? COUNTRY_NAMES[countryCode] : undefined),
      city: data.city || undefined,
      flag: countryFlag(countryCode),
      source: 'ip-api',
    };
  } catch {
    return null;
  }
}

async function lookupIpinfo(ip: string): Promise<GeoInfo | null> {
  try {
    const res = await fetch(
      `https://ipinfo.io/${encodeURIComponent(ip)}/json?token=${encodeURIComponent(config.geoip.token)}`,
      { signal: AbortSignal.timeout(config.geoip.timeoutMs) },
    );
    if (!res.ok) return null;
    const data = (await res.json()) as { ip?: string; country?: string; city?: string };
    const countryCode = normaliseCountryCode(data.country);
    return {
      ip: data.ip || ip,
      countryCode,
      countryName: countryCode ? COUNTRY_NAMES[countryCode] : undefined,
      city: data.city || undefined,
      flag: countryFlag(countryCode),
      source: 'ipinfo',
    };
  } catch {
    return null;
  }
}

function addHeaderCandidates(target: string[], value: string | string[] | undefined): void {
  if (!value) return;
  const raw = Array.isArray(value) ? value.join(',') : value;
  for (const part of raw.split(',')) {
    const ip = cleanIpValue(part);
    if (ip) target.push(ip);
  }
}

function cleanIpValue(value?: string): string {
  if (!value) return '';
  let ip = value.trim();
  if (ip.startsWith('::ffff:')) ip = ip.slice(7);
  if (ip.includes(':') && ip.includes('.') && ip.startsWith('[')) {
    ip = ip.slice(1, ip.indexOf(']'));
  }
  return ip;
}

function isPublicIp(ip: string): boolean {
  return Boolean(ip) && !isPrivateIp(ip);
}

function isPrivateIp(ip: string): boolean {
  if (!ip) return true;
  const clean = cleanIpValue(ip).toLowerCase();
  if (
    clean === 'localhost' ||
    clean === '::1' ||
    clean === '0:0:0:0:0:0:0:1' ||
    clean.startsWith('fc') ||
    clean.startsWith('fd') ||
    clean.startsWith('fe80:')
  ) {
    return true;
  }

  const parts = clean.split('.').map((p) => Number(p));
  if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
    return false;
  }

  const [a, b] = parts;
  return (
    a === 10 ||
    a === 127 ||
    (a === 172 && b >= 16 && b <= 31) ||
    (a === 192 && b === 168) ||
    (a === 169 && b === 254) ||
    (a === 100 && b >= 64 && b <= 127)
  );
}

function normaliseCountryCode(code?: string): string {
  const cc = (code || '').trim().toUpperCase();
  return /^[A-Z]{2}$/.test(cc) ? cc : '';
}
