import { getDb } from "./db.js";
import { correctCity as providerCorrectCity, getActiveProvider, isAiAvailable } from "./ai/index.js";

const SUFFIX_MULTIWORD = [
  " unified government",
  " consolidated government",
  " metropolitan government",
  " metro government",
  " urban county",
  " metro township",
];

const SUFFIX_SINGLE = [
  " CDP",
  " city",
  " town",
  " village",
  " borough",
  " township",
  " municipality",
  " comunidad",
  " urbana",
  " corporation",
  " county",
];

const LSAD_PRIORITY = {
  "25": 1, "35": 2, CG: 2, MG: 2, UG: 2, CN: 2,
  "47": 3, "21": 4, "43": 5, "53": 6, "57": 7, "62": 8,
};

export function lsadPriority(code) {
  return LSAD_PRIORITY[String(code)] ?? 99;
}

export function stripPlaceSuffix(name) {
  let s = String(name || "");
  if (s.endsWith(" (balance)")) s = s.slice(0, -" (balance)".length);
  for (const suf of SUFFIX_MULTIWORD) {
    if (s.endsWith(suf)) { s = s.slice(0, -suf.length); break; }
  }
  for (const suf of SUFFIX_SINGLE) {
    if (s.endsWith(suf)) { s = s.slice(0, -suf.length); break; }
  }
  return s.trim();
}

export function normalizeCityLookupKey(name) {
  return String(name || "")
    .normalize("NFKD")
    .replace(/[̀-ͯ]/g, "")
    .toLowerCase()
    .replace(/\./g, "")
    .replace(/[^a-z0-9\s-]/g, " ")
    .replace(/\s+/g, " ")
    .trim()
    .replace(/^st(?=\s)/, "saint")
    .replace(/^ste(?=\s)/, "sainte")
    .replace(/^mt(?=\s)/, "mount")
    .replace(/^ft(?=\s)/, "fort");
}

function levenshtein(a, b, maxDistance) {
  if (a === b) return 0;
  const al = a.length;
  const bl = b.length;
  if (Math.abs(al - bl) > maxDistance) return maxDistance + 1;
  if (al === 0) return bl;
  if (bl === 0) return al;
  let prev = new Array(bl + 1);
  let curr = new Array(bl + 1);
  for (let j = 0; j <= bl; j++) prev[j] = j;
  for (let i = 1; i <= al; i++) {
    curr[0] = i;
    let rowMin = curr[0];
    for (let j = 1; j <= bl; j++) {
      const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
      curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
      if (curr[j] < rowMin) rowMin = curr[j];
    }
    if (rowMin > maxDistance) return maxDistance + 1;
    [prev, curr] = [curr, prev];
  }
  return prev[bl];
}

const statePlacesCache = new Map();

function loadStatePlaces(stateCode) {
  if (statePlacesCache.has(stateCode)) return statePlacesCache.get(stateCode);
  const rows = getDb().prepare(`
    SELECT state_code AS stateCode, name, full_name AS fullName,
           city_key AS cityKey, full_city_key AS fullCityKey,
           lsad, lsad_priority AS lsadPriority,
           geoid, latitude, longitude
    FROM gazetteer_places
    WHERE state_code = ?
  `).all(stateCode);
  statePlacesCache.set(stateCode, rows);
  return rows;
}

function fuzzySuggestions({ cityKey, candidates, limit = 3, maxDistance = 2 }) {
  const scored = [];
  for (const doc of candidates) {
    const dCity = levenshtein(cityKey, doc.cityKey || "", maxDistance);
    const dFull = doc.fullCityKey && doc.fullCityKey !== doc.cityKey
      ? levenshtein(cityKey, doc.fullCityKey, maxDistance)
      : maxDistance + 1;
    const d = Math.min(dCity, dFull);
    if (d <= maxDistance) scored.push({ doc, distance: d });
  }
  scored.sort((a, b) => a.distance - b.distance || a.doc.lsadPriority - b.doc.lsadPriority);
  const seen = new Set();
  const out = [];
  for (const { doc, distance } of scored) {
    if (seen.has(doc.geoid)) continue;
    seen.add(doc.geoid);
    out.push({
      name: doc.name,
      fullName: doc.fullName,
      stateCode: doc.stateCode,
      geoid: doc.geoid,
      lsad: doc.lsad,
      latitude: doc.latitude,
      longitude: doc.longitude,
      distance,
      displayName: `${doc.name}, ${doc.stateCode}`,
    });
    if (out.length >= limit) break;
  }
  return out;
}

async function aiCorrectCity({ city, stateCode, providerName }) {
  const places = loadStatePlaces(stateCode);
  const candidates = [...new Set(places.map((p) => p.name))].sort();
  const startedAt = Date.now();
  const result = await providerCorrectCity({ city, stateCode, candidates }, providerName);
  return {
    ...result,
    candidateCount: candidates.length,
    elapsedMs: Date.now() - startedAt,
  };
}

const findExactStmt = () => getDb().prepare(`
  SELECT state_code AS stateCode, name, full_name AS fullName,
         city_key AS cityKey, full_city_key AS fullCityKey,
         lsad, lsad_priority AS lsadPriority,
         geoid, latitude, longitude
  FROM gazetteer_places
  WHERE state_code = ? AND (city_key = ? OR full_city_key = ?)
  ORDER BY lsad_priority ASC
  LIMIT 1
`);

const findByNameStmt = () => getDb().prepare(`
  SELECT state_code AS stateCode, name, full_name AS fullName,
         city_key AS cityKey, full_city_key AS fullCityKey,
         lsad, lsad_priority AS lsadPriority,
         geoid, latitude, longitude
  FROM gazetteer_places
  WHERE state_code = ? AND name = ?
  ORDER BY lsad_priority ASC
  LIMIT 1
`);

export async function geocode({ city, state, useAi = false, aiProvider = null } = {}) {
  const startedAt = Date.now();
  const cityRaw = String(city || "").trim();
  const stateCode = String(state || "").trim().toUpperCase();
  const cityKey = normalizeCityLookupKey(cityRaw);

  let hit = null;
  if (cityKey && stateCode) {
    hit = findExactStmt().get(stateCode, cityKey, cityKey) ?? null;
  }
  let matchMethod = hit ? "exact" : null;

  let suggestions = [];
  if (!hit && cityKey && stateCode) {
    const candidates = loadStatePlaces(stateCode);
    suggestions = fuzzySuggestions({ cityKey, candidates });
  }

  let aiCorrection = null;
  if (!hit && useAi && cityRaw && stateCode) {
    if (!isAiAvailable()) {
      aiCorrection = { error: "No AI provider configured" };
    } else {
      try {
        aiCorrection = await aiCorrectCity({ city: cityRaw, stateCode, providerName: aiProvider });
        if (aiCorrection.correctedCityName) {
          const aiHit = findByNameStmt().get(stateCode, aiCorrection.correctedCityName) ?? null;
          if (aiHit) {
            if (aiCorrection.confidence === "high") {
              hit = aiHit;
              matchMethod = "ai";
            } else {
              suggestions = [
                {
                  name: aiHit.name,
                  fullName: aiHit.fullName,
                  stateCode: aiHit.stateCode,
                  geoid: aiHit.geoid,
                  lsad: aiHit.lsad,
                  latitude: aiHit.latitude,
                  longitude: aiHit.longitude,
                  distance: null,
                  displayName: `${aiHit.name}, ${aiHit.stateCode}`,
                  source: "ai",
                  confidence: aiCorrection.confidence,
                },
                ...suggestions,
              ];
            }
          }
        }
      } catch (err) {
        aiCorrection = { error: err.message };
      }
    }
  }

  const found = !!hit;
  const elapsedMs = Date.now() - startedAt;
  const latitude = found ? hit.latitude : null;
  const longitude = found ? hit.longitude : null;
  const displayName = found ? `${hit.name}, ${hit.stateCode}` : "";

  return {
    provider: "us-census-gazetteer",
    found,
    matchMethod,
    elapsedMs,
    city: cityRaw,
    state: stateCode,
    latitude,
    longitude,
    displayName,
    match: hit,
    suggestions,
    aiCorrection,
  };
}
