/**
 * Halal Places — Overpass API (OpenStreetMap) integration
 *
 * Free, no API key, real worldwide data from OSM contributors.
 * Uses bounding-box queries (faster than radius) with multiple mirror fallbacks.
 *
 * Server-side caching via JSON file to stay within Overpass rate limits.
 */

import fs from "fs";
import path from "path";
import https from "https";
import { URL } from "url";

// ─── Types ──────────────────────────────────────────────────

export interface HalalPlace {
  id: string;
  name: string;
  address: string;
  lat: number;
  lng: number;
  rating: number;
  type: "mosque" | "restaurant" | "cafe";
  halal_certified: boolean;
  distance?: number;
  source: string;
  phone?: string;
  website?: string;
  opening_hours?: string;
}

interface CacheEntry {
  data: HalalPlace[];
  expiresAt: number;
}

// ─── Constants ──────────────────────────────────────────────

const OVERPASS_MIRRORS = [
  "https://overpass-api.de/api/interpreter",
  "https://lz4.overpass-api.de/api/interpreter",
  "https://z.overpass-api.de/api/interpreter",
];

const NOMINATIM_URL = "https://nominatim.openstreetmap.org/search";
const CACHE_DIR = "/app/data/cache";
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
const MAX_PLACES = 50;
const QUERY_TIMEOUT = 15; // seconds

let cacheReady = false;

function ensureCacheDir() {
  if (cacheReady) return;
  try {
    if (!fs.existsSync(CACHE_DIR)) {
      fs.mkdirSync(CACHE_DIR, { recursive: true });
    }
    cacheReady = true;
  } catch {
    // Non-fatal
  }
}

// ─── Cache helpers ─────────────────────────────────────────

function cacheKey(lat: number, lng: number, radiusM: number): string {
  // Round to ~0.25° grid for broader cache hits
  const gridLat = Math.round(lat * 4) / 4;
  const gridLng = Math.round(lng * 4) / 4;
  const r = Math.round(radiusM / 500) * 500;
  return `places_${gridLat}_${gridLng}_${r}.json`;
}

function cacheRead(key: string): HalalPlace[] | null {
  try {
    ensureCacheDir();
    const filePath = path.join(CACHE_DIR, key);
    if (!fs.existsSync(filePath)) return null;
    const raw = fs.readFileSync(filePath, "utf-8");
    const entry: CacheEntry = JSON.parse(raw);
    if (Date.now() > entry.expiresAt) {
      fs.unlinkSync(filePath);
      return null;
    }
    return entry.data;
  } catch {
    return null;
  }
}

function cacheWrite(key: string, data: HalalPlace[]) {
  try {
    ensureCacheDir();
    const filePath = path.join(CACHE_DIR, key);
    const entry: CacheEntry = { data, expiresAt: Date.now() + CACHE_TTL_MS };
    fs.writeFileSync(filePath, JSON.stringify(entry), "utf-8");
  } catch {
    // Non-fatal
  }
}

// ─── Geocode city → lat/lng via Nominatim ──────────────────

interface GeocodeResult {
  lat: number;
  lng: number;
  displayName: string;
}

async function geocodeCity(city: string, country?: string): Promise<GeocodeResult | null> {
  try {
    const query = country ? `${city}, ${country}` : city;
    const params = new URLSearchParams({
      q: query,
      format: "json",
      limit: "1",
      "accept-language": "en",
    });

    const res = await fetch(`${NOMINATIM_URL}?${params.toString()}`, {
      headers: { "User-Agent": "FalahOS/1.0 (halal-monitor)" },
      signal: AbortSignal.timeout(8000),
    });

    if (!res.ok) return null;
    const data = await res.json();
    if (!Array.isArray(data) || data.length === 0) return null;

    return {
      lat: parseFloat(data[0].lat),
      lng: parseFloat(data[0].lon),
      displayName: data[0].display_name,
    };
  } catch {
    return null;
  }
}

// ─── Bounding box from center + radius ─────────────────────

function boundingBox(lat: number, lng: number, radiusM: number) {
  // 1° lat ≈ 111,320m
  // 1° lng ≈ 111,320 * cos(lat) m
  const dLat = radiusM / 111320;
  const dLng = radiusM / (111320 * Math.cos((lat * Math.PI) / 180));
  return {
    south: lat - dLat,
    north: lat + dLat,
    west: lng - dLng,
    east: lng + dLng,
  };
}

// ─── Build Overpass query (bounding box, not radius) ──────

function buildOverpassQuery(bbox: { south: number; north: number; west: number; east: number }): string {
  const bb = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`;

  return `
    [out:json][timeout:${QUERY_TIMEOUT}];
    (
      node(${bb})["amenity"="restaurant"]["diet:halal"="yes"];
      way(${bb})["amenity"="restaurant"]["diet:halal"="yes"];
      node(${bb})["amenity"="fast_food"]["diet:halal"="yes"];
      way(${bb})["amenity"="fast_food"]["diet:halal"="yes"];
      node(${bb})["amenity"="place_of_worship"]["religion"="muslim"];
      way(${bb})["amenity"="place_of_worship"]["religion"="muslim"];
      node(${bb})["amenity"="cafe"]["diet:halal"="yes"];
      way(${bb})["amenity"="cafe"]["diet:halal"="yes"];
    );
    out center 50;
  `.trim();
}

// ─── Classify OSM element ─────────────────────────────────

function classifyType(tags: Record<string, string>): "mosque" | "restaurant" | "cafe" | null {
  const amenity = tags.amenity || "";
  if (amenity === "place_of_worship" && tags.religion === "muslim") return "mosque";
  if (amenity === "restaurant" || amenity === "fast_food") return "restaurant";
  if (amenity === "cafe") return "cafe";
  return null;
}

// ─── Parse OSM response ────────────────────────────────────

function parseOsmResponse(data: any): HalalPlace[] {
  if (!data || !Array.isArray(data.elements)) return [];

  const places: HalalPlace[] = [];

  for (const el of data.elements) {
    const tags = el.tags || {};
    const type = classifyType(tags);
    if (!type) continue;

    let lat = el.lat;
    let lng = el.lon;
    if (el.type === "way" && el.center) {
      lat = el.center.lat;
      lng = el.center.lon;
    }
    if (lat === undefined || lng === undefined) continue;

    const name = tags.name || tags["name:en"] || "";
    if (!name) continue;

    const addressParts = [
      tags["addr:housenumber"],
      tags["addr:street"],
      tags["addr:city"],
      tags["addr:postcode"],
    ].filter(Boolean);
    const address = addressParts.length > 0
      ? addressParts.join(", ")
      : [tags["addr:city"], tags["addr:country"]].filter(Boolean).join(", ");

    places.push({
      id: `${type}-${el.type}-${el.id}`,
      name,
      address: address || `${lat.toFixed(4)}, ${lng.toFixed(4)}`,
      lat,
      lng,
      rating: tags.rating ? parseFloat(tags.rating) : 4.0,
      type,
      halal_certified: tags["diet:halal"] === "yes" || tags.cuisine === "halal",
      source: "openstreetmap",
      phone: tags.phone || tags["contact:phone"] || undefined,
      website: tags.website || tags["contact:website"] || undefined,
      opening_hours: tags.opening_hours || undefined,
    });
  }

  return places;
}

// ─── Query Overpass with mirror fallback ───────────────────

async function queryOverpass(query: string): Promise<any> {
  let lastError: Error | null = null;

  for (const mirror of OVERPASS_MIRRORS) {
    try {
      const url = new URL(mirror);
      const body = new URLSearchParams({ data: query }).toString();

      const data = await new Promise<string>((resolve, reject) => {
        const req = https.request(
          {
            hostname: url.hostname,
            path: url.pathname,
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
              "Content-Length": Buffer.byteLength(body),
              Accept: "*/*",
              "User-Agent": "FalahOS-HalalMonitor/1.0",
            },
            timeout: 25000,
          },
          (res) => {
            let chunks: Buffer[] = [];
            res.on("data", (c: Buffer) => chunks.push(c));
            res.on("end", () => {
              const raw = Buffer.concat(chunks).toString("utf-8");
              if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
                resolve(raw);
              } else if (raw.includes("rate_limited")) {
                reject(new Error("RATE_LIMITED"));
              } else {
                reject(new Error(`HTTP ${res.statusCode}: ${raw.slice(0, 100)}`));
              }
            });
          }
        );
        req.on("error", reject);
        req.on("timeout", () => { req.destroy(); reject(new Error("TIMEOUT")); });
        req.write(body);
        req.end();
      });

      return JSON.parse(data);
    } catch (err) {
      if (err instanceof Error) {
        if (err.message === "RATE_LIMITED") continue;
        if (err.message === "TIMEOUT" || err.message.includes("504")) {
          // Retry once on timeout/504 — Overpass can be slow on first query
          continue;
        }
        lastError = err;
      } else {
        lastError = new Error(String(err));
      }
    }
  }

  if (lastError) throw lastError;
  throw new Error("All Overpass mirrors exhausted");
}

// ─── Public API ─────────────────────────────────────────────

export async function fetchNearbyPlaces(
  lat: number,
  lng: number,
  radiusM: number = 5000
): Promise<HalalPlace[]> {
  const key = cacheKey(lat, lng, radiusM);

  // Check cache first
  const cached = cacheRead(key);
  if (cached !== null) return cached;

  try {
    const bbox = boundingBox(lat, lng, radiusM);
    const query = buildOverpassQuery(bbox);
    const data = await queryOverpass(query);
    const places = parseOsmResponse(data).slice(0, MAX_PLACES);

    if (places.length > 0) cacheWrite(key, places);

    return places;
  } catch (error) {
    console.error("Overpass fetch failed:", error);
    return [];
  }
}

export async function fetchCityPlaces(
  city: string,
  country?: string,
  radiusM: number = 5000
): Promise<{ places: HalalPlace[]; location: string | null }> {
  const geo = await geocodeCity(city, country);
  if (!geo) {
    return { places: [], location: null };
  }

  const places = await fetchNearbyPlaces(geo.lat, geo.lng, radiusM);
  return { places, location: geo.displayName };
}
