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 } = 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",
      },
    })
  );

  // redirect raiz para /login
  app.get("/", (req, res) => {
    return res.redirect("/login");
  });

  // middleware que protege rotas que exigem login
  function requireAuth(req, res, next) {
    if (req.session && req.session.user) return next();
    // se for chamada XHR, responda 401 em vez de redirect (útil para API)
    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);
  });

  /////////////////////////////////////////////////////

  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) => 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 = "";

          // If no coords, try ViaCEP -> Google
          if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
            if (rawCep) {
              const cep8 = rawCep.padStart(8, "0");
              const viaCepData = await fetchJson(
                `https://viacep.com.br/ws/${cep8}/json/`
              );
              if (viaCepData && !viaCepData.erro) {
                const logradouro = viaCepData.logradouro || "";
                const bairro = viaCepData.bairro || "";
                const cidade = viaCepData.localidade || "";
                const uf = viaCepData.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 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 fetchJson(
          `https://viacep.com.br/ws/${cep}/json/`
        );
        if (!viaCepData || viaCepData.erro)
          return res.status(404).json({ error: "CEP não encontrado" });
        const logradouro = viaCepData.logradouro || "";
        const bairro = viaCepData.bairro || "";
        const cidade = viaCepData.localidade || "";
        const uf = viaCepData.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 viaCepData = await fetchJson(
        `https://viacep.com.br/ws/${cep}/json/`
      );
      if (!viaCepData || viaCepData.erro)
        return res.status(404).json({ error: "CEP não encontrado" });
      const logradouro = viaCepData.logradouro || "";
      const bairro = viaCepData.bairro || "";
      const cidade = viaCepData.localidade || "";
      const uf = viaCepData.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 /public APENAS quando autenticado
  app.use("/public", requireAuth, express.static(path.join(__dirname, "public")));

  // Middleware para proteger rotas
  app.use((req, res, next) => {
    if (!req.session.user && req.path !== "/login" && !req.path.startsWith("/auth")) {
      return res.redirect("/login");
    }
    next();
  });

  /////////////////////////////////////////////////////

  return app;
}

module.exports = {
  createApp,
};
