// require("dotenv").config(); // const express = require("express"); // const multer = require("multer"); // const fs = require("fs"); // const path = require("path"); // const csv = require("csv-parser"); // const fastCsv = require("fast-csv"); // const axios = require("axios"); // const cors = require("cors"); // const session = require("express-session"); // adiciona session // const { geocodeWithGoogle, addressWithGoogle } = require("./service/geocodeService"); // const { fetchJson } = require("./service/fetchService"); // const { BASE_BACKOFF_MS, MAX_RETRIES, REQUEST_DELAY_MS, sleep } = require("./service/retryService"); // const { API_URL, HEADERS } = require("./config/apiConfig"); // const { normalizePartnerSigla } = require("./service/normalizeService"); // const authRoutes = require("./routes/authRoutes.js"); // function createApp() { // const upload = multer({ dest: "uploads/" }); // const app = express(); // // se estiver atrás de um reverse proxy (nginx/traefik) em produção, habilite: // app.set("trust proxy", 1); // app.use(cors()); // app.use(express.json()); // // session TEM que vir antes das rotas que usam req.session // app.use( // session({ // secret: process.env.SESSION_SECRET || "change-me", // resave: false, // saveUninitialized: false, // cookie: { // maxAge: 24 * 60 * 60 * 1000, // secure: process.env.NODE_ENV === "production", // sameSite: "lax", // }, // }) // ); // // -- Desenvolvimento: pular autenticação se configurado // // Para ativar: defina NODE_ENV=development e DEV_SKIP_AUTH=true no .env // if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') { // app.use((req, res, next) => { // // garante que exista uma sessão autenticada para facilitar testes locais // if (req.session && (!req.session.user || !req.session.user.authenticated)) { // req.session.user = { authenticated: true, dev: true }; // } // next(); // }); // } // // middleware que protege rotas que exigem login // function requireAuth(req, res, next) { // if (req.session?.user?.authenticated) { // return next(); // } // if (req.xhr || req.headers.accept?.includes("application/json")) { // return res.status(401).json({ error: "not_authenticated" }); // } // return res.redirect("/login"); // } // // proteger demais rotas (ex.: /upload, /consulta) // app.use((req, res, next) => { // // permissão liberada para rotas de auth já tratadas; proteger o resto // if (req.path.startsWith("/auth") || req.path === "/login") return next(); // return requireAuth(req, res, next); // }); // // redirect raiz // app.get("/", (req, res) => { // // em dev com bypass, sirva a página diretamente (sem redirect) // if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') { // return res.sendFile(path.join(__dirname, 'public', 'index.html')); // } // if (req.session?.user?.authenticated) { // return res.redirect("/public/index.html"); // } // return res.redirect("/login"); // }); // ///////////////////////////////////////////////////// // async function getMinDistance(lat, lon) { // // tenta várias vezes com backoff exponencial; trata 429 usando Retry-After se disponível // let attempt = 0; // while (attempt < MAX_RETRIES) { // try { // // envia também o raio (em metros) - API espera esse parâmetro em várias rotas // const resp = await axios.get(API_URL, { // headers: HEADERS, // params: { // raio: 5000, // latitude: lat, // longitude: lon, // "itens[]": ["caixa"], // consultarPasta: "S", // }, // timeout: 10000, // }); // const data = resp.data; // const registros = data && data.registros ? data.registros : []; // // find registros that have a numeric distancia and keep original object for robust extraction // const candidates = registros // .map((r) => ({ raw: r, distanciaRaw: r && r.distancia })) // .map((o) => ({ // raw: o.raw, // num: // o.distanciaRaw !== undefined && // o.distanciaRaw !== null && // o.distanciaRaw !== "" // ? Number(o.distanciaRaw) // : null, // })) // .filter((x) => x.num !== null && !Number.isNaN(x.num)); // if (candidates.length) { // candidates.sort((a, b) => a.num - b.num); // const best = candidates[0]; // const r = best.raw || {}; // // robust extraction of pasta sigla with fallbacks // let pastaSigla = null; // try { // if (r.pasta) { // if (typeof r.pasta === "string" && r.pasta.trim()) // pastaSigla = r.pasta.trim(); // else if (r.pasta.sigla && String(r.pasta.sigla).trim()) // pastaSigla = String(r.pasta.sigla).trim(); // else if ( // r.pasta.cidade && // r.pasta.cidade.sigla && // String(r.pasta.cidade.sigla).trim() // ) // pastaSigla = String(r.pasta.cidade.sigla).trim(); // } // } catch (e) { // pastaSigla = null; // } // // if closest has no pastaSigla, try find any candidate with non-empty sigla // if (!pastaSigla) { // for (let j = 0; j < candidates.length; j++) { // const rr = candidates[j].raw || {}; // try { // if (rr.pasta) { // if (typeof rr.pasta === "string" && rr.pasta.trim()) { // pastaSigla = rr.pasta.trim(); // break; // } // if (rr.pasta.sigla && String(rr.pasta.sigla).trim()) { // pastaSigla = String(rr.pasta.sigla).trim(); // break; // } // if ( // rr.pasta.cidade && // rr.pasta.cidade.sigla && // String(rr.pasta.cidade.sigla).trim() // ) { // pastaSigla = String(rr.pasta.cidade.sigla).trim(); // break; // } // } // } catch (e) { // // continue // } // } // } // if (!pastaSigla) // console.warn( // `[WARN] Nenhuma pasta.sigla encontrada para coordenadas ${lat},${lon} (closest dist ${best.num})` // ); // pastaSigla = normalizePartnerSigla(pastaSigla); // return { dist: best.num, pastaSigla }; // } // // sem distancias válidas // return null; // } catch (err) { // attempt += 1; // // se for 429, tente respeitar Retry-After quando disponível // if (err.response && err.response.status === 429) { // const ra = // err.response.headers && // (err.response.headers["retry-after"] || // err.response.headers["Retry-After"]); // let waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1); // if (ra) { // const raSec = parseInt(ra, 10); // if (!isNaN(raSec)) waitMs = raSec * 1000; // } // console.warn( // `[WARN] 429 recebido para ${lat},${lon} - aguardando ${waitMs}ms e tentando novamente (attempt ${attempt}/${MAX_RETRIES})` // ); // await sleep(waitMs); // continue; // } // // para outros erros de rede/timeout, aguarda backoff exponencial e tenta de novo // const waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1); // console.warn( // `[WARN] Erro ao consultar API para ${lat},${lon}: ${err.message} - backoff ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})` // ); // await sleep(waitMs); // } // } // // exauriu tentativas // console.error(`[ERROR] Exauriu retries para ${lat},${lon}`); // return null; // } // // upload CSV endpoint // const jobs = {}; // jobId -> { status, total, processed, download, error } // app.post("/upload", upload.single("csvfile"), (req, res) => { // if (!req.file) // return res.status(400).json({ error: "Nenhum arquivo enviado" }); // const filePath = req.file.path; // const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; // jobs[jobId] = { // status: "queued", // total: 0, // processed: 0, // download: null, // error: null, // }; // (async () => { // jobs[jobId].status = "processing"; // try { // const rows = []; // await new Promise((resolve, reject) => { // fs.createReadStream(filePath) // .pipe(csv({ separator: ";" })) // .on("data", (data) => { // // ignora linhas totalmente vazias (todos os campos nulos/undefined/strings vazias) // const values = Object.values(data); // const allEmpty = // values.length === 0 || // values.every( // (v) => v === null || v === undefined || String(v).trim() === "" // ); // if (!allEmpty) rows.push(data); // }) // .on("end", resolve) // .on("error", reject); // }); // jobs[jobId].total = rows.length; // const coordCache = new Map(); // const outRows = []; // for (let i = 0; i < rows.length; i++) { // const row = rows[i]; // // normalize keys to avoid duplicates caused by different headers // const norm = {}; // Object.keys(row).forEach((k) => { // // normalize header: lowercase, remove diacritics and non-alphanumeric // const kn = k // .trim() // .toLowerCase() // .normalize("NFKD") // .replace(/[\u0300-\u036f]/g, "") // .replace(/[^a-z0-9]/g, ""); // norm[kn] = row[k]; // }); // // Input columns (from normalized map) // const rawCep = norm["cep"] // ? String(norm["cep"]).replace(/\D/g, "") // : ""; // const rawNumero = norm["numero"] ? String(norm["numero"]).trim() : ""; // // prefer lat/lon from normalized input if available // const rawLat = norm["latitude"] || norm["lat"] || null; // const rawLon = // norm["longitude"] || norm["lon"] || norm["long"] || null; // // Prefer existing lat/lon if provided from normalized fields // let lat = null, // lon = null; // if (rawLat && rawLon) { // lat = Number(String(rawLat).replace(",", ".")); // lon = Number(String(rawLon).replace(",", ".")); // } // let builtAddress = ""; // const googleGeocodeAddress = await addressWithGoogle(lat, lon); // if (googleGeocodeAddress) { // builtAddress = googleGeocodeAddress; // } else { // console.warn( // `Google Reverse Geocoding não retornou resultado para coords ${lat},${lon} (row ${i + 1})` // ); // } // console.log(`Row ${i + 1}: CEP='${rawCep}' Número='${rawNumero}' Lat='${lat}' Lon='${lon}' BuiltAddress='${builtAddress}'`); // // If no coords, try ViaCEP -> Google // if (!Number.isFinite(lat) || !Number.isFinite(lon)) { // if (rawCep) { // const cep8 = rawCep.padStart(8, "0"); // const cepRestData = await fetch( // 'https://api.cep.rest/', { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ cep: cep8 }) // } // ).then(r => r.json()); // if (cepRestData && !cepRestData.erro) { // const logradouro = cepRestData.data.logradouro || ""; // const bairro = cepRestData.data.bairro || ""; // const cidade = cepRestData.data.localidade || ""; // const uf = cepRestData.data.uf || ""; // if (logradouro) { // builtAddress = // `${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}` // .replace(/, ,/g, ",") // .replace(/^,\s*/, ""); // } else { // // fallback: use neighborhood/city // builtAddress = `${bairro || ""} ${ // cidade ? ", " + cidade : "" // } ${uf ? "- " + uf : ""}`.trim(); // } // // build addressToUse (builtAddress already assembled above) // if (!process.env.GOOGLE_API_KEY) { // console.error( // "[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env" // ); // } else { // const addressToUse = // builtAddress || `${cidade} ${uf} ${cep8}`; // const geo = await geocodeWithGoogle(addressToUse); // if (geo) { // lat = geo.lat; // lon = geo.lon; // } else // console.warn( // `Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${ // i + 1 // })` // ); // } // } else { // console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i + 1})`); // } // } else { // console.log(`Row ${i + 1}: missing/invalid CEP -> '${rawCep}'`); // } // } // // Prepare explicit output row to avoid extra columns // const out = { // CEP: rawCep || "", // Número: rawNumero || "", // Endereço: builtAddress || "", // // write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps // Latitude: Number.isFinite(lat) ? Number(lat).toFixed(6) : "", // Longitude: Number.isFinite(lon) ? Number(lon).toFixed(6) : "", // "Não dedicado": "", // Dedicado: "", // Distancia: "", // "Parceiro/Sothis": "", // }; // if (Number.isFinite(lat) && Number.isFinite(lon)) { // const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`; // if (coordCache.has(coordKey)) { // const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla } // if (cached !== null) { // const d = cached.dist; // const di = Math.round(Number(d)); // out["Não dedicado"] = di <= 500 ? "viável" : "Não viável"; // out["Dedicado"] = di <= 1000 ? "viável" : "Não viável"; // out["Distancia"] = `${di}M`; // out["Parceiro/Sothis"] = // normalizePartnerSigla(cached.pastaSigla) || ""; // } else { // out["Não dedicado"] = "Não viável"; // out["Dedicado"] = "Não viável"; // out["Distancia"] = "5km +"; // out["Parceiro/Sothis"] = ""; // } // } else { // const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null // coordCache.set(coordKey, minResult); // if (minResult !== null) { // const di = Math.round(Number(minResult.dist)); // out["Não dedicado"] = di <= 500 ? "viável" : "Não viável"; // out["Dedicado"] = di <= 1000 ? "viável" : "Não viável"; // out["Distancia"] = `${di}M`; // out["Parceiro/Sothis"] = // normalizePartnerSigla(minResult.pastaSigla) || ""; // } else { // out["Não dedicado"] = "Não viável"; // out["Dedicado"] = "Não viável"; // out["Distancia"] = "5km +"; // out["Parceiro/Sothis"] = ""; // } // await sleep(REQUEST_DELAY_MS); // } // } else { // // no coords available -> keep defaults // } // outRows.push(out); // jobs[jobId].processed = i + 1; // } // // write output csv - use explicit outRows and fixed header order // const outPath = path.join(__dirname, "outputs"); // if (!fs.existsSync(outPath)) fs.mkdirSync(outPath); // const originalName = // req.file && req.file.originalname // ? req.file.originalname // : `upload_${Date.now()}.csv`; // const parsed = path.parse(originalName); // let outBase = `${parsed.name}_output`; // let outFile = path.join(outPath, `${outBase}.csv`); // if (fs.existsSync(outFile)) { // outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`); // } // const headers = [ // "CEP", // "Número", // "Endereço", // "Latitude", // "Longitude", // "Não dedicado", // "Dedicado", // "Distancia", // "Parceiro/Sothis", // ]; // await new Promise((resolve, reject) => { // const ws = fs.createWriteStream(outFile); // ws.write("\uFEFF"); // fastCsv // .write(outRows, { headers: headers, delimiter: ";" }) // .pipe(ws) // .on("finish", resolve) // .on("error", reject); // }); // try { // fs.unlinkSync(filePath); // } catch (e) {} // jobs[jobId].status = "done"; // jobs[jobId].download = `/download/${path.basename(outFile)}`; // } catch (err) { // console.error(err); // jobs[jobId].status = "error"; // jobs[jobId].error = String(err.message || err); // } // })(); // return res.json({ jobId }); // }); // // download model endpoint // app.get("/download-model/:name", (req, res) => { // const name = req.params.name; // const safeName = path.basename(name); // evita ../ // const filePath = path.join(__dirname, "models", safeName); // if (!fs.existsSync(filePath)) return res.status(404).send("Arquivo não encontrado"); // return res.download(filePath, safeName); // }); // // lista os arquivos disponíveis em /models // app.get("/download-models/list", (req, res) => { // const modelDir = path.join(__dirname, "models"); // if (!fs.existsSync(modelDir)) return res.status(404).json({ error: "Pasta de modelos não encontrada" }); // const files = fs.readdirSync(modelDir).filter((f) => fs.statSync(path.join(modelDir, f)).isFile()); // return res.json({ files }); // }); // // download result endpoint // app.get("/download/:name", (req, res) => { // const name = req.params.name; // const p = path.join(__dirname, "outputs", name); // if (!fs.existsSync(p)) // return res.status(404).send("Arquivo não encontrado"); // res.download(p); // }); // // job status endpoint // app.get("/status/:jobId", (req, res) => { // const job = jobs[req.params.jobId]; // if (!job) return res.status(404).json({ error: "job não encontrado" }); // return res.json(job); // }); // // manual query endpoint // // /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid // app.get("/consulta", async (req, res) => { // const { // latitude: rawLat, // longitude: rawLon, // cep: rawCep, // numero: rawNumero, // } = req.query; // // If cep provided, use ViaCEP -> Google geocoding -> Geogrid // if (rawCep) { // const cep = String(rawCep).replace(/\D/g, ""); // const numero = rawNumero ? String(rawNumero).trim() : ""; // try { // const viaCepData = await fetch( // 'https://api.cep.rest/', { method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ cep }) // } // ).then(r => r.json()); // if (!viaCepData || viaCepData.erro) // return res.status(404).json({ error: "CEP não encontrado" }); // const logradouro = viaCepData.data.logradouro || ""; // const bairro = viaCepData.data.bairro || ""; // const cidade = viaCepData.data.localidade || ""; // const uf = viaCepData.data.uf || ""; // const endereco = // `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}` // .replace(/, ,/g, ",") // .replace(/^,\s*/, ""); // if (!process.env.GOOGLE_API_KEY) // return res // .status(500) // .json({ error: "GOOGLE_API_KEY não definida no servidor" }); // const geo = await geocodeWithGoogle( // endereco || `${cidade} ${uf} ${cep}` // ); // if (!geo) // return res // .status(404) // .json({ error: "geocode não encontrado (Google)" }); // const lat = Number(geo.lat); // const lon = Number(geo.lon); // const result = await getMinDistance(lat, lon); // if (result && result.dist !== undefined) { // return res.json({ // endereco, // latitude: lat, // longitude: lon, // distancia: result.dist, // parceiro: result.pastaSigla || "", // }); // } // return res.json({ // endereco, // latitude: lat, // longitude: lon, // distancia: "5km +", // }); // } catch (err) { // console.error(err); // return res.status(500).json({ error: "Erro na consulta" }); // } // } // // Otherwise require latitude+longitude // if (!rawLat || !rawLon) // return res.status(400).json({ // error: "latitude e longitude são obrigatórios (ou forneça cep)", // }); // const latitude = Number(String(rawLat).replace(",", ".")); // const longitude = Number(String(rawLon).replace(",", ".")); // if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { // console.warn( // `Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'` // ); // return res.status(400).json({ error: "latitude ou longitude inválidos" }); // } // try { // console.log(`Consulta manual: lat=${latitude} lon=${longitude}`); // const result = await getMinDistance(latitude, longitude); // console.log(`Resultado consulta manual: ${JSON.stringify(result)}`); // if (result && result.dist !== undefined) { // return res.json({ // distancia: result.dist, // parceiro: result.pastaSigla || "", // }); // } // return res.json({ distancia: "5km +" }); // } catch (err) { // console.error(err); // return res.status(500).json({ error: "Erro na consulta" }); // } // }); // // manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid // app.get("/consulta-cep", async (req, res) => { // const { cep: rawCep, numero: rawNumero } = req.query; // if (!rawCep) return res.status(400).json({ error: "cep é obrigatório" }); // const cep = String(rawCep).replace(/\D/g, ""); // const numero = rawNumero ? String(rawNumero).trim() : ""; // try { // const cepRestData = await fetch( // 'https://api.cep.rest/', { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ cep }) // } // ).then(r => r.json()); // if (!cepRestData || cepRestData.erro) // return res.status(404).json({ error: "CEP não encontrado" }); // const logradouro = cepRestData.data.logradouro || ""; // const bairro = cepRestData.data.bairro || ""; // const cidade = cepRestData.data.localidade || ""; // const uf = cepRestData.data.uf || ""; // const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}` // .replace(/, ,/g, ",") // .replace(/^,\s*/, ""); // if (!process.env.GOOGLE_API_KEY) // return res // .status(500) // .json({ error: "GOOGLE_API_KEY não definida no servidor" }); // const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`); // if (!geo) // return res // .status(404) // .json({ error: "geocode não encontrado (Google)" }); // const lat = Number(geo.lat); // const lon = Number(geo.lon); // const result = await getMinDistance(lat, lon); // if (result && result.dist !== undefined) { // return res.json({ // endereco, // latitude: lat, // longitude: lon, // distancia: result.dist, // parceiro: result.pastaSigla || "", // }); // } // return res.json({ // endereco, // latitude: lat, // longitude: lon, // distancia: "5km +", // }); // } catch (err) { // console.error(err); // return res.status(500).json({ error: "erro na consulta" }); // } // }); // //////////////////////////////////////////////////// // // Servir arquivos estáticos (index.html) // // app.use("/public", express.static(path.join(__dirname, "public"))); // // Usa as rotas de autenticação // app.use("/", authRoutes); // // servir arquivos estáticos da pasta public (rotas protegidas já são tratadas pelo middleware global) // app.use(express.static(path.join(__dirname, "public"))); // // rota protegida que serve o index.html // app.get("/app", requireAuth, (req, res) => { // res.sendFile(path.join(__dirname, "public", "index.html")); // }); // ///////////////////////////////////////////////////// // return app; // } // module.exports = { // createApp, // };