'use strict'
// Address validation against the RQA (Répertoire des adresses du Québec).
//
// Why this exists: every Service Location in ERPNext should have its lat/lng
// derived from the official Quebec civic address registry rather than from
// human typing or Mapbox geocode. Free-text geocoding is error-prone for
// rural areas (we just hit a case where the SL coords pointed 9 km away
// from the real address). RQA is the authoritative source — updated every
// 2 weeks, includes a stable `identifiant_unique_adresse` per civic address.
//
// This module exposes ONE public route:
//   POST /address/validate
//     body: { address_line, postal_code?, city? }
//     returns: { exact_match, best, candidates, confidence }
//
// The heavy lifting (Supabase REST → RQA table search) lives in
// ./address-search.js — already used by the customer onboarding wizard.
// We layer a confidence score + canonical formatting on top.

const { json, parseBody, log, cors } = require('./helpers')
const { searchAddresses } = require('./address-search')
const { searchLocal, searchRaw, pool } = require('./address-db')
const { reverseNearestFibre } = require('./legacy-dispatch-sync') // pins de terrain (source de vérité géoloc)

// Normalize for fuzzy comparison: lowercase, strip diacritics, collapse
// whitespace, drop punctuation. Used to score how close a typed address
// is to a RQA result.
function normalizeForCompare (s) {
  return (s || '')
    .toString()
    .toLowerCase()
    .normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9\s]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
}

// 0..1 score: civic number must match exactly, road name fuzzy-includes,
// postal code (first 3 chars) bonus when present, city name bonus.
function scoreMatch (typed, rqaRow) {
  const tNorm = normalizeForCompare(typed.address_line || '')
  const rNum = String(rqaRow.numero_municipal || '')
  const rRoad = normalizeForCompare(rqaRow.odonyme_recompose_normal || '')
  const rCity = normalizeForCompare(rqaRow.nom_municipalite || '')
  const rPC = (rqaRow.code_postal || '').replace(/\s+/g, '').toLowerCase()

  let score = 0
  // Civic number — must appear in the typed string
  if (rNum && tNorm.match(new RegExp('\\b' + rNum + '\\b'))) score += 0.4
  // Road name fuzzy: every word of the RQA road must appear in the typed
  const rWords = rRoad.split(/\s+/).filter(w => w.length > 1 && !['rue','chemin','rang','route','avenue','boulevard'].includes(w))
  if (rWords.length) {
    const matched = rWords.filter(w => tNorm.includes(w)).length
    score += 0.3 * (matched / rWords.length)
  }
  // City — small bonus
  if (typed.city && rCity.includes(normalizeForCompare(typed.city))) score += 0.1
  // Postal code — first 3 chars (FSA) is enough; full postal code = bigger bonus
  if (typed.postal_code) {
    const tPC = typed.postal_code.replace(/\s+/g, '').toLowerCase()
    if (tPC && rPC) {
      if (rPC.startsWith(tPC.slice(0, 3))) score += 0.1
      if (rPC === tPC) score += 0.1
    }
  }
  return Math.min(1, score)
}

async function handle (req, res, method, path) {
  // POST /rpc/search_addresses — COMPAT Supabase RPC (forme attendue par l'autocomplete du site web :
  // body { search_term, result_limit } → tableau direct de lignes { id, address_full, numero, rue, ville,
  // code_postal, longitude, latitude, fiber_available, zone_tarifaire, max_speed, similarity_score }).
  // Sert depuis la base LOCALE → on débranche le Supabase cloud (juste basculer VITE_API_BASE côté site).
  if (path === '/rpc/search_addresses') {
    if (method === 'OPTIONS') { cors(res); res.writeHead(204); res.end(); return }
    if (method === 'POST') {
      const body = await parseBody(req)
      const term = (body.search_term || body.q || '').toString().trim()
      const limit = body.result_limit || body.limit || 8
      let rows = []
      try { rows = await searchRaw(term, limit) } catch (e) { log('rpc/search_addresses:', e.message) }
      cors(res)
      return json(res, 200, rows)
    }
  }

  // GET /address/search?q=&limit= — autocomplete PUBLIC (site web) : adresses + disponibilité fibre,
  // depuis la base LOCALE (rqa_addresses + fiber_availability). Remplace l'appel direct au Supabase.
  if (path.startsWith('/address/search')) {
    if (method === 'OPTIONS') { cors(res); res.writeHead(204); res.end(); return }
    if (method === 'GET') {
      const u = new URL(req.url, 'http://localhost')
      const q = (u.searchParams.get('q') || '').trim()
      const limit = u.searchParams.get('limit') || 8
      let results = []
      try { results = await searchLocal(q, limit) } catch (e) { log('address/search error:', e.message) }
      cors(res)
      return json(res, 200, { ok: true, query: q, count: results.length, results })
    }
  }

  // GET /address/reverse?lat=&lon= — géocodage INVERSE : coord → adresse civique la plus proche.
  // PRIORITÉ 1 : pins de TERRAIN validés (table `fibre`, source de vérité géoloc, pin posé sur la maison).
  // PRIORITÉ 2 (repli) : centroïde/rooftop RQA LOCAL (bbox ~±650 m + haversine, index btree lat/lon).
  // Plus de Nominatim. Le champ `source` indique d'où vient la réponse (infra_fibre | rqa).
  if (path.startsWith('/address/reverse')) {
    if (method === 'OPTIONS') { cors(res); res.writeHead(204); res.end(); return }
    if (method === 'GET') {
      cors(res)
      const u = new URL(req.url, 'http://localhost')
      const lat = parseFloat(u.searchParams.get('lat')), lon = parseFloat(u.searchParams.get('lon'))
      if (!isFinite(lat) || !isFinite(lon) || Math.abs(lat) < 0.01) return json(res, 400, { ok: false, error: 'lat/lon requis' })
      // PRIORITÉ 1 — pins de terrain (infra). Best-effort : une panne F ne casse pas le repli RQA.
      try {
        const fib = await reverseNearestFibre(lat, lon)
        if (fib && fib.address) return json(res, 200, { ok: true, found: true, ...fib })
      } catch (e) { log('address/reverse infra:', e.message) }
      const dLat = 0.006, dLon = 0.006 / Math.max(0.2, Math.cos(lat * Math.PI / 180))
      try {
        const q = `SELECT address_full, numero, rue, ville, code_postal, latitude, longitude,
          (6371000*2*asin(sqrt(power(sin(radians($1-latitude)/2),2)+cos(radians($1))*cos(radians(latitude))*power(sin(radians($2-longitude)/2),2)))) AS dist
          FROM rqa_addresses
          WHERE latitude BETWEEN $1-$3 AND $1+$3 AND longitude BETWEEN $2-$4 AND $2+$4
          ORDER BY dist ASC LIMIT 1`
        const { rows } = await pool().query(q, [lat, lon, dLat, dLon])
        const r = rows[0]
        if (!r) return json(res, 200, { ok: true, found: false })
        const line1 = (r.numero && r.rue) ? (r.numero + ' ' + r.rue) : (r.address_full || '')
        const address = [line1, r.ville, r.code_postal].filter(Boolean).join(', ')
        return json(res, 200, { ok: true, found: true, address, distance_m: Math.round(r.dist), latitude: r.latitude, longitude: r.longitude, ville: r.ville, code_postal: r.code_postal, source: 'rqa' })
      } catch (e) { log('address/reverse:', e.message); return json(res, 500, { ok: false, error: 'reverse indisponible' }) }
    }
  }

  // POST /address/validate — score-rank RQA results for a free-text address.
  if (path === '/address/validate' && method === 'POST') {
    const body = await parseBody(req)
    const addressLine = (body.address_line || '').trim()
    if (!addressLine) return json(res, 400, { error: 'address_line required' })

    // Build a search term — RQA's full-text search prefers "civic + road"
    const term = body.postal_code
      ? `${addressLine} ${body.postal_code}`
      : addressLine

    let rqaResults = []
    try {
      rqaResults = await searchAddresses(term, 12)
    } catch (e) {
      log('RQA search failed:', e.message)
      return json(res, 502, { error: 'RQA unavailable: ' + e.message })
    }

    if (!rqaResults.length) {
      return json(res, 200, {
        exact_match: false,
        best: null,
        candidates: [],
        confidence: 0,
        recommendation: 'unmatched',
      })
    }

    const scored = rqaResults
      .map(r => ({
        aq_address_id: r.identifiant_unique_adresse,
        formatted: r.adresse_formatee,
        civic: r.numero_municipal,
        unit: r.numero_unite,
        road: r.odonyme_recompose_normal,
        city: r.nom_municipalite,
        postal_code: r.code_postal,
        latitude: r.latitude ? parseFloat(r.latitude) : null,
        longitude: r.longitude ? parseFloat(r.longitude) : null,
        score: scoreMatch({ address_line: addressLine, postal_code: body.postal_code, city: body.city }, r),
      }))
      .sort((a, b) => b.score - a.score)

    const best = scored[0]
    const exactMatch = best.score >= 0.7
    const recommendation = exactMatch
      ? 'validated'
      : best.score >= 0.4 ? 'review' : 'unmatched'

    return json(res, 200, {
      exact_match: exactMatch,
      best,
      candidates: scored.slice(0, 5),
      confidence: best.score,
      recommendation,
    })
  }

  return json(res, 404, { error: 'Not found' })
}

module.exports = { handle, scoreMatch, normalizeForCompare }
