import { capitalizeCity } from './format-address';

// Holt Restaurants (und verwandte Gastro-Typen) aus der Overpass API im Umkreis
// einer Session und normalisiert die Tags. Nutzt mehrere oeffentliche Overpass-
// Mirrors als Fallback, da die Haupt-Instanz (overpass-api.de) bei hoher Last mit
// 504 antworten kann.
export interface OverpassRestaurant {
  osmId: string;
  name: string;
  lat: number;
  lon: number;
  cuisine?: string;
  priceRating?: number;
  isFastFood: boolean;
  phone?: string;
  website?: string;
  openingHoursRaw?: string;
  address?: string;
  postalCode?: string;
  suburb?: string;
  wikidataId?: string;
  wikimediaCommons?: string;
  imageTag?: string;
  dietVegan?: string;
  dietVegetarian?: string;
  dietHalal?: string;
  dietGlutenFree?: string;
}

const OVERPASS_MIRRORS = [
  'https://overpass-api.de/api/interpreter',
  'https://overpass.kumi.systems/api/interpreter',
];

// Erlaubte Gastro-POIs: nur Orte, an denen man essen/trinken kann
// (Frühstück, Mittag, Abendessen, Eis, Kaffee, Biergarten etc.).
// Alles andere (Apotheken, Geldautomaten, Klamotten, DM/Rossmann ...)
// wird strikt ignoriert.
const ALLOWED_AMENITIES = new Set([
  'restaurant',
  'fast_food',
  'cafe',
  'ice_cream',
  'bar',
  'pub',
  'biergarten',
  'food_court',
]);
const ALLOWED_SHOPS = new Set([
  'bakery',
  'confectionery',
  'deli',
  'pastry',
]);

function isAllowedFoodPoi(tags: Record<string, string>): boolean {
  return (
    (!!tags.amenity && ALLOWED_AMENITIES.has(tags.amenity)) ||
    (!!tags.shop && ALLOWED_SHOPS.has(tags.shop))
  );
}

async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...options, signal: controller.signal });
  } finally {
    clearTimeout(id);
  }
}

/**
 * Baut eine lesbare Adresse im Format "Straße Hausnummer, PLZ Stadt"
 * (z.B. "Erbprinzenstraße 31, 76133 Karlsruhe") statt der vorherigen
 * fehlerhaften Aneinanderreihung ohne Komma ("Erbprinzenstraße 31 Karlsruhe").
 * Fehlt ein Teil, wird er einfach weggelassen statt eines Platzhalters.
 */
function formatAddress(
  street?: string,
  housenumber?: string,
  city?: string,
  postalCode?: string,
  suburb?: string,
): string | undefined {
  const streetLine = [street, housenumber].filter(Boolean).join(' ');
  const normalizedCity = city ? capitalizeCity(city.trim()) : undefined;
  const normalizedSuburb = suburb ? capitalizeCity(suburb.trim()) : undefined;
  const cityLine = [postalCode, normalizedCity].filter(Boolean).join(' ');

  // Ohne Straße UND ohne Stadt UND ohne Stadtteil ist die PLZ allein keine
  // brauchbare Adresse.
  if (!streetLine && !normalizedCity && !normalizedSuburb) return undefined;

  const parts = [streetLine, normalizedSuburb, cityLine].filter(Boolean);
  return parts.join(', ') || undefined;
}

function getCenter(el: any): { lat: number; lon: number } | undefined {
  if (el.lat != null && el.lon != null) return { lat: el.lat, lon: el.lon };
  if (el.center?.lat != null && el.center?.lon != null) {
    return { lat: el.center.lat, lon: el.center.lon };
  }
  return undefined;
}

function distanceMeters(lat1: number, lon1: number, lat2: number, lon2: number): number {
  const R = 6371000;
  const toRad = (x: number) => (x * Math.PI) / 180;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

interface AddressPoint {
  lat: number;
  lon: number;
  street: string;
  housenumber?: string;
  city?: string;
}

// Findet den naechsten Adresspunkt (Straße + Hausnummer) zu einer Koordinate.
// Wird genutzt, um Restaurant-Nodes ohne eigene addr:street zu ergänzen -
// das Restaurant liegt meist in einem Gebäude, dessen Adresse separat
// erfasst ist. Nur bis 150m Distanz, sonst riskieren wir eine falsche Straße.
// Bevorzugt Punkte mit passender Stadt (Stadtgrenzen-Problem).
function findNearestAddress(
  lat: number,
  lon: number,
  points: AddressPoint[],
  city?: string,
): { street: string; housenumber?: string } | undefined {
  if (points.length === 0) return undefined;
  const normalizedCity = city?.trim().toLowerCase();
  let candidates = points;
  if (normalizedCity) {
    const matching = points.filter(
      (p) => p.city && p.city.trim().toLowerCase() === normalizedCity,
    );
    if (matching.length > 0) candidates = matching;
  }
  let best: { p: AddressPoint; dist: number } | undefined;
  for (const p of candidates) {
    const dist = distanceMeters(lat, lon, p.lat, p.lon);
    if (!best || dist < best.dist) best = { p, dist };
  }
  if (!best || best.dist > 150) return undefined;
  return { street: best.p.street, housenumber: best.p.housenumber };
}

export async function fetchRestaurantsFromOverpass(
  lat: number, lon: number, radiusMeters: number
): Promise<OverpassRestaurant[]> {
  const query = `
    [out:json][timeout:25];
    (
      nwr["amenity"~"^(restaurant|fast_food|cafe|ice_cream|bar|pub|biergarten|food_court)$"](around:${radiusMeters},${lat},${lon});
      nwr["shop"~"^(bakery|confectionery|deli|pastry)$"](around:${radiusMeters},${lat},${lon});
      node["addr:postcode"](around:${radiusMeters},${lat},${lon});
      way["addr:postcode"](around:${radiusMeters},${lat},${lon});
      relation["addr:postcode"](around:${radiusMeters},${lat},${lon});
    );
    out body center;
  `;

  let lastError: unknown = null;

  for (const mirror of OVERPASS_MIRRORS) {
    try {
      const res = await fetchWithTimeout(mirror, {
        method: 'POST',
        body: `data=${encodeURIComponent(query)}`,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'User-Agent': 'Yumder/0.1 (contact: admin@mineco.de)',
          'Accept': '*/*',
        },
      }, 25000);

      if (!res.ok) {
        lastError = new Error(`Overpass-Fehler (${mirror}): ${res.status}`);
        continue;
      }

      const json = await res.json();
      const elements = (json.elements as any[]) || [];

      // Adresspunkte (Straße + Hausnummer) sammeln, um Restaurant-Nodes ohne
      // eigene addr:street ergänzen zu können.
      const addressPoints: AddressPoint[] = [];
      for (const el of elements) {
        const tags = el.tags || {};
        const street = tags['addr:street'];
        if (!street) continue;
        const center = getCenter(el);
        if (!center) continue;
        addressPoints.push({ lat: center.lat, lon: center.lon, street, housenumber: tags['addr:housenumber'], city: tags['addr:city'] });
      }

      const restaurants = elements
        .filter((el) => {
          const tags = el.tags || {};
          // Nur echte Gastro-POIs behalten; addr:postcode-Objekte mit
          // anderen shop/amenity-Tags (z.B. pharmacy, atm, clothes)
          // dürfen nicht als Restaurant durchschlüpfen.
          return tags.name && isAllowedFoodPoi(tags);
        })
        .map((el) => {
          const tags = el.tags || {};
          const derivedCuisine = tags.cuisine || tags.amenity || tags.shop;
          const center = getCenter(el);
          const type = el.type || 'node';
          const ownStreet = tags['addr:street'];
          const ownHousenumber = tags['addr:housenumber'];
          const nearestAddress = !ownStreet && center
            ? findNearestAddress(center.lat, center.lon, addressPoints, tags['addr:city'])
            : undefined;
          const street = ownStreet || nearestAddress?.street;
          const housenumber = ownHousenumber || nearestAddress?.housenumber;
          return {
            osmId: `${type}/${el.id}`,
            name: tags.name || 'Unbenanntes Restaurant',
            lat: center?.lat ?? el.lat,
            lon: center?.lon ?? el.lon,
            cuisine: derivedCuisine,
            priceRating: mapPriceRange(tags['price:range']),
            isFastFood: tags.amenity === 'fast_food',
            phone: tags.phone || tags['contact:phone'],
            website: tags.website || tags['contact:website'],
            openingHoursRaw: tags.opening_hours,
            address: formatAddress(street, housenumber, tags['addr:city'], tags['addr:postcode'], tags['addr:suburb']),
            postalCode: tags['addr:postcode'],
            suburb: tags['addr:suburb'],
            wikidataId: tags.wikidata,
            wikimediaCommons: tags.wikimedia_commons,
            imageTag: tags.image,
            dietVegan: tags['diet:vegan'],
            dietVegetarian: tags['diet:vegetarian'],
            dietHalal: tags['diet:halal'],
            dietGlutenFree: tags['diet:gluten_free'],
          };
        })
        .filter((r) => r.name !== 'Unbenanntes Restaurant');

      return restaurants;
    } catch (e) {
      lastError = e;
      continue;
    }
  }

  throw lastError instanceof Error ? lastError : new Error('Alle Overpass-Mirrors fehlgeschlagen');
}

function mapPriceRange(range?: string): number | undefined {
  if (!range) return undefined;
  if (range.includes('-')) {
    const [min] = range.split('-').map(Number);
    if (min <= 10) return 1;
    if (min <= 20) return 2;
    if (min <= 35) return 3;
    return 4;
  }
  return undefined;
}
