const fs = require("fs");
const path = require("path");
const os = require("os");

// Fija el directorio de trabajo a la RAIZ del launcher (un nivel arriba de
// src/, que es donde vive index.js). Sin esto, cuando el proceso se lanza
// sin heredar el cwd de la carpeta del launcher (p.ej. via el protocolo
// eurotrans:// -> start-launcher.vbs, cuyo "oShell.Run" no fija directorio
// de trabajo), dotenv (en ./config) no encuentra el .env del launcher y
// EUROTRANS_API_URL cae en silencio al valor por defecto
// http://127.0.0.1:4000, rompiendo login/telemetria/saldo.
// OJO: chdir(__dirname) (la version anterior de este fix) apuntaba a src/,
// pero el .env vive un nivel arriba (junto a package.json) — eso rompia el
// flujo normal del instalador, que ya fijaba el cwd correcto via
// Start-Process -WorkingDirectory, moviendolo a la carpeta equivocada.
process.chdir(path.join(__dirname, ".."));

const FLAG_DIR  = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "EuroTrans");
const FLAG_FILE = path.join(FLAG_DIR, "connected.json");

// ── Log a archivo ─────────────────────────────────────────────────────────────
// Se configura ANTES que cualquier otra cosa (incluido selfUpdate) para que
// ningún mensaje de diagnóstico se pierda por escribirse antes de este punto:
// cuando el launcher se relanza vía eurotrans:// (start-launcher.vbs) no hay
// consola visible, así que si algo se loguea solo con el console.log original
// (stdout), ese mensaje desaparece sin dejar rastro en launcher.log.
const LOG_FILE = path.join(FLAG_DIR, "launcher.log");
try { fs.mkdirSync(FLAG_DIR, { recursive: true }); } catch (_) {}
const _logStream = (() => { try { return fs.createWriteStream(LOG_FILE, { flags: "a" }); } catch (_) { return null; } })();
function writeLog(line) {
  const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
  const full = `[${ts}] ${line}`;
  if (_logStream) _logStream.write(full + "\n");
}
console.log   = (...a) => { const s = a.join(" "); process.stdout.write(s + "\n"); writeLog(s); };
console.warn  = (...a) => { const s = a.join(" "); process.stderr.write(s + "\n"); writeLog("WARN " + s); };
console.error = (...a) => { const s = a.join(" "); process.stderr.write(s + "\n"); writeLog("ERR  " + s); };

console.log(`[diag] ── arranque del launcher ──`);
console.log(`[diag] node ${process.version} | plataforma ${process.platform} | pid ${process.pid}`);
console.log(`[diag] cwd=${process.cwd()}  __dirname=${__dirname}`);
(() => {
  const envPath = path.join(process.cwd(), ".env");
  console.log(`[diag] .env ${fs.existsSync(envPath) ? "encontrado" : "NO ENCONTRADO"} en ${envPath}`);
})();

// ── Auto-update: descarga src/index.js del servidor y reinicia si hay cambios ──
// NOTA: esto SOLO actualiza este archivo (index.js). Cualquier fix en otro
// archivo del launcher (browserAuth.js, config.js, apiClient.js, telemetry/*)
// no se propaga por aquí — hace falta reinstalar (volver a descargar y
// ejecutar Instalar-EuroTrans-Todo.bat) para recibirlo.
(function selfUpdate() {
  const UPDATE_URL = "https://ets2.lastgamestudio.fun/launcher/src";
  const https = require("https");
  const currentPath = __filename;
  console.log(`[update] comprobando actualizaciones en ${UPDATE_URL}...`);
  const req = https.get(UPDATE_URL, { timeout: 8000 }, (res) => {
    console.log(`[update] respuesta del servidor: HTTP ${res.statusCode}`);
    if (res.statusCode !== 200) {
      console.warn(`[update] status inesperado (${res.statusCode}), se omite la actualización`);
      return;
    }
    const chunks = [];
    res.on("data", c => chunks.push(c));
    res.on("end", () => {
      const next = Buffer.concat(chunks).toString("utf8");
      const current = fs.readFileSync(currentPath, "utf8");
      if (next.length <= 500) {
        console.warn(`[update] respuesta demasiado corta (${next.length} bytes), se omite por seguridad`);
        return;
      }
      if (next === current) {
        console.log(`[update] ya tienes la última versión (${current.length} bytes)`);
        return;
      }
      console.log(`[update] nueva versión detectada (actual ${current.length} bytes → nueva ${next.length} bytes). Actualizando y reiniciando...`);
      fs.writeFileSync(currentPath, next, "utf8");
      const child = require("child_process").spawn(process.execPath, [currentPath], {
        detached: true, stdio: "inherit", env: process.env
      });
      child.unref();
      process.exit(0);
    });
  });
  req.on("timeout", () => {
    console.warn(`[update] timeout (8s) comprobando actualizaciones — se continúa sin actualizar`);
    req.destroy();
  });
  req.on("error", (err) => {
    console.warn(`[update] error comprobando actualizaciones: ${err.message}`);
  });
  req.end();
})();

const config = require("./config");
const { browserLogin } = require("./browserAuth");
const { ApiClient } = require("./apiClient");
const telemetrySource = require("./telemetry/funbitSource");
const { startFinePoller, flushFineBuffer, resetNearDestination } = telemetrySource;
const gameLog = require("./gameLogMonitor");
const modGuard = require("./modGuard");

console.log(`[diag] config cargada — apiUrl=${config.apiUrl} telemetryUrl=${config.telemetryUrl}`);

// ── scs-sdk-plugin (RenCloud): detección completa de multas vía bridge HTTP ───
// scs-fine-bridge.exe lee la memoria compartida del plugin y expone los eventos
// de multa en http://127.0.0.1:25556/fines. Si está disponible se usa en lugar
// del proxy de daño, cubriendo también velocidad, semáforo y sentido contrario.

function getLauncherDir() {
  // index.js está en Desktop\EuroTrans\src\ — los binarios bundleados están un nivel arriba
  return path.join(__dirname, "..");
}

// Busca la carpeta de instalación de ETS2 sin asumir la ruta por defecto de
// Steam: consulta el registro de Windows (InstallPath del cliente Steam) y
// lee steamapps\libraryfolders.vdf para cubrir bibliotecas en otras unidades,
// igual que ya hacían los scripts .ps1/.bat de instalación de telemetría.
function findEts2InstallDir() {
  const isEts2Dir = (dir) => fs.existsSync(path.join(dir, "bin", "win_x64", "eurotrucks2.exe"));

  const candidates = [
    "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Euro Truck Simulator 2",
    "C:\\Program Files\\Steam\\steamapps\\common\\Euro Truck Simulator 2",
  ];

  const steamPaths = [];
  try {
    const { execSync } = require("child_process");
    const keys = [
      "HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam",
      "HKLM\\SOFTWARE\\Valve\\Steam",
      "HKCU\\SOFTWARE\\Valve\\Steam",
    ];
    for (const key of keys) {
      for (const value of ["InstallPath", "SteamPath"]) {
        try {
          const out = execSync(`reg query "${key}" /v ${value}`, { encoding: "utf8", timeout: 3000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
          const m = out.match(/REG_SZ\s+(.+)/);
          if (m) steamPaths.push(m[1].trim());
        } catch (_) {}
      }
    }
  } catch (_) {}

  for (const steamPath of [...new Set(steamPaths)]) {
    const norm = steamPath.replace(/\//g, "\\");
    candidates.push(path.join(norm, "steamapps", "common", "Euro Truck Simulator 2"));
    const vdf = path.join(norm, "steamapps", "libraryfolders.vdf");
    if (!fs.existsSync(vdf)) continue;
    try {
      const content = fs.readFileSync(vdf, "utf8");
      for (const m of content.matchAll(/"path"\s*"([^"]+)"/g)) {
        const lib = m[1].replace(/\\\\/g, "\\").replace(/\//g, "\\");
        candidates.push(path.join(lib, "steamapps", "common", "Euro Truck Simulator 2"));
      }
    } catch (_) {}
  }

  return candidates.find(isEts2Dir) || null;
}

function installScsPlugin() {
  const dllSrc = path.join(getLauncherDir(), "scs-telemetry.dll");
  if (!fs.existsSync(dllSrc)) return;

  const ets2Dir = findEts2InstallDir();
  if (!ets2Dir) {
    console.warn("[scs-plugin] no se encontró la instalación de ETS2 (rutas por defecto ni bibliotecas de Steam).");
    return;
  }
  const pluginsDir = path.join(ets2Dir, "bin", "win_x64", "plugins");
  const dllDst     = path.join(pluginsDir, "scs-telemetry.dll");
  try {
    fs.mkdirSync(pluginsDir, { recursive: true });
    if (!fs.existsSync(dllDst)) {
      fs.copyFileSync(dllSrc, dllDst);
      console.log("[scs-plugin] instalado en:", dllDst);
      console.log("[scs-plugin] ⚠ Reinicia ETS2 una vez para activar la detección completa de multas.");
    }
  } catch (err) {
    console.warn("[scs-plugin] no se pudo instalar la DLL:", err.message);
  }
}

function startScsBridge() {
  const candidates = [
    path.join(FLAG_DIR, "scs-fine-bridge.exe"),
    path.join(getLauncherDir(), "scs-fine-bridge.exe"),
  ];
  const exePath = candidates.find(p => fs.existsSync(p));
  if (!exePath) return false;

  // Copiar al FLAG_DIR para tenerlo centralizado
  const flagExe = path.join(FLAG_DIR, "scs-fine-bridge.exe");
  if (exePath !== flagExe) {
    try { fs.copyFileSync(exePath, flagExe); } catch (_) {}
  }

  try {
    const { execSync, spawn } = require("child_process");
    const list = execSync('tasklist /fi "imagename eq scs-fine-bridge.exe" /fo csv /nh', { encoding: "utf8", timeout: 3000, windowsHide: true });
    if (list.toLowerCase().includes("scs-fine-bridge.exe")) {
      console.log("[scs-bridge] ya estaba corriendo");
      return true;
    }
    spawn(fs.existsSync(flagExe) ? flagExe : exePath, [], { detached: true, stdio: "ignore" }).unref();
    console.log("[scs-bridge] iniciado");
    return true;
  } catch (err) {
    console.warn("[scs-bridge] no se pudo iniciar:", err.message);
    return false;
  }
}

let _scsBridgeEnabled = false;
const _scsBridgeBuffer = { speeding: 0, redLight: 0, collision: 0, wrongWay: 0, noLights: 0, other: 0, totalEur: 0 };

function scsClassify(offence) {
  const o = (offence || "").toLowerCase();
  if (/speed/.test(o))                                                              return "speeding";
  if (/red.?light|signal|mandatory.?stop|no.?mandatory/.test(o))                   return "redLight";
  if (/crash|collisi|avoid.?vehicle|avoid.?car|vehicle.?crash|prohib/.test(o))     return "collision";
  if (/wrong.?way|wrong.?dir|avoid.?ped|pedestrian/.test(o))                       return "wrongWay";
  if (/no.?head|headlight|no.?light|without.?light|driving.?light|light.?off/.test(o)) return "noLights";
  return "other";
}

// El polling silencioso (catch vacio) no dejaba rastro cuando scs-fine-bridge.exe
// no respondia o devolvia error: si esto ocurria justo en el segundo en que el
// juego generaba una multa, esa multa se perdia sin ninguna pista en el log
// para diferenciar "el plugin no reporto el evento" de "si lo reporto pero
// nuestro polling no pudo leerlo en ese instante". Se registra el cambio de
// estado (sano -> con error -> sano) para tener esa visibilidad sin llenar
// el log de una linea por segundo mientras dure el corte.
let _scsBridgeHealthy = true;

async function _pollScsBridge() {
  try {
    const res = await fetch("http://127.0.0.1:25556/fines");
    if (!res.ok) {
      if (_scsBridgeHealthy) console.warn(`[scs-bridge] /fines respondió ${res.status} — posible corte temporal, se reintenta cada 1s`);
      _scsBridgeHealthy = false;
      return;
    }
    if (!_scsBridgeHealthy) {
      console.log("[scs-bridge] /fines vuelve a responder con normalidad");
      _scsBridgeHealthy = true;
    }
    const fines = await res.json();
    for (const f of fines) {
      const cat = scsClassify(f.offence);
      _scsBridgeBuffer[cat]++;
      _scsBridgeBuffer.totalEur += Number(f.amount) || 0;
      console.log(`[scs-bridge] multa: offence="${f.offence}" amount=${f.amount} → ${cat}`);
    }
  } catch (err) {
    if (_scsBridgeHealthy) console.warn(`[scs-bridge] error consultando /fines: ${err.message} — posible corte temporal, se reintenta cada 1s`);
    _scsBridgeHealthy = false;
  }
}

function flushScsBridge() {
  const snap = { ..._scsBridgeBuffer };
  Object.keys(_scsBridgeBuffer).forEach(k => (_scsBridgeBuffer[k] = 0));
  return snap;
}

// ── Proxy de multa por colisión: detecta spikes de daño del camión ────────────
// El plugin Funbit v9 (versión actual del repositorio) no expone gameplay events
// como scs_telemetry_gameplay_event_fined. SÍ expone el desgaste del camión
// en tiempo real. Cuando ETS2 aplica una multa por colisión el camión recibe
// daño visible en el siguiente tick (~10s). Un salto brusco en el peor componente
// de desgaste dentro de un tick indica colisión con alta probabilidad.
let _prevTruckWear = null;
const COLLISION_WEAR_THRESHOLD = 0.4; // % de aumento en un tick (0-100 scale)

function detectCollisionByDamage(sample) {
  const wear = sample.truckDamagePercent || 0;

  if (_prevTruckWear === null) {
    _prevTruckWear = wear;
    return 0;
  }

  const delta = wear - _prevTruckWear;
  _prevTruckWear = wear;

  // Spike positivo por encima del umbral y camión no destruido (evitar ruido a 100%)
  if (delta >= COLLISION_WEAR_THRESHOLD && wear < 99) {
    console.log(`[damage-proxy] daño +${delta.toFixed(2)}% en un tick → colisión detectada`);
    return 1;
  }
  return 0;
}

// ─────────────────────────────────────────────────────────────────────────────

let activeContractId = null;
let activeContractDestination = null;
let waitingForNewJob = false;
let noJobTickCount = 0;

// Multas ya extraídas de scs-bridge/game.log/poller pero que todavía no se
// confirmaron guardadas en el servidor (el envío de /telemetry falló). Se
// acumulan aquí hasta un envío exitoso en vez de descartarse: los flush*()
// de cada fuente vacían su buffer interno de inmediato al leerlo, así que si
// no se reintentan aquí, un simple corte de red hace desaparecer la multa
// para siempre aunque sí se haya detectado correctamente.
let carryFines = { speeding: 0, redLight: 0, collision: 0, wrongWay: 0, noLights: 0, other: 0, totalEur: 0 };

// Health-check del puerto HTTP del servidor de telemetria, independiente de
// si el juego esta conectado o no (game.connected) — distingue "el .exe no
// esta escuchando en el puerto" (crash al iniciar, puerto ocupado, etc.) de
// "el .exe esta bien pero ETS2 aun no ha cargado el plugin SCS".
function checkTelemetryServerHealth(context) {
  fetch(config.telemetryUrl)
    .then(res => {
      console.log(`[telemetry-server] health-check (${context}): el puerto responde — HTTP ${res.status} en ${config.telemetryUrl}`);
    })
    .catch(err => {
      console.warn(`[telemetry-server] health-check (${context}): NO responde en ${config.telemetryUrl} — ${err.message}`);
    });
}

function startTelemetryServer() {
  const { execSync, spawn } = require("child_process");
  const exePath = path.join(FLAG_DIR, "ets2-telemetry-server", "server", "Ets2Telemetry.exe");
  if (!fs.existsSync(exePath)) {
    console.warn("[telemetry-server] no encontrado en:", exePath);
    return;
  }
  try {
    const list = execSync('tasklist /fi "imagename eq Ets2Telemetry.exe" /fo csv /nh', { encoding: "utf8", timeout: 3000, windowsHide: true });
    if (list.toLowerCase().includes("ets2telemetry.exe")) {
      console.log("[telemetry-server] ya estaba corriendo");
      setTimeout(() => checkTelemetryServerHealth("proceso ya activo"), 3000);
      return;
    }
  } catch (_) {}
  try {
    spawn(exePath, [], { detached: true, stdio: "ignore", cwd: path.dirname(exePath) }).unref();
    console.log("[telemetry-server] iniciado:", exePath);
    setTimeout(() => checkTelemetryServerHealth("tras iniciar"), 5000);
  } catch (err) {
    console.warn("[telemetry-server] no se pudo iniciar:", err.message);
  }
}

// Elimina archivos de locale que versiones anteriores del launcher instalaron.
function cleanLegacyLocaleFiles() {
  const ets2LocaleDir = path.join(
    process.env.USERPROFILE || os.homedir(),
    "Documents", "Euro Truck Simulator 2", "locale"
  );
  if (!fs.existsSync(ets2LocaleDir)) return;
  const targets = ["local.override.sii", "eurotrans_vtlgst.sui"];
  let removed = 0;
  try {
    for (const lang of fs.readdirSync(ets2LocaleDir)) {
      for (const name of targets) {
        const fp = path.join(ets2LocaleDir, lang, name);
        if (fs.existsSync(fp)) { fs.unlinkSync(fp); removed++; }
      }
    }
  } catch (_) {}
  if (removed > 0) console.log(`[cleanup] eliminados ${removed} archivo(s) de locale obsoletos`);
}

async function ensureActiveContract(api, sample) {
  if (!config.autoAcceptContracts) return;
  const { origin, destination, cargo, plannedDistanceKm, jobIncome } = sample;
  if (!origin || !destination) {
    waitingForNewJob = false;
    // Detectar abandono: contrato activo pero sin trabajo.
    // Umbral alto para no disparar el abandono al entrar en menús o durante
    // transiciones de carga. Solo se confirma si además ETS2 sigue en ejecución
    // (si el juego se cerró, la sesión termina de otra forma).
    if (activeContractId) {
      noJobTickCount++;
      // 30 ticks × 10s = 5 minutos sin datos de trabajo antes de declarar abandono
      if (noJobTickCount >= 30 && isEts2Running()) {
        const cid = activeContractId;
        activeContractId          = null;
        activeContractDestination = null;
        noJobTickCount            = 0;
        // Vaciar todos los buffers de multas pendientes: la penalización por abandono
        // es la única sanción aplicable; las multas de tráfico acumuladas se descartan.
        flushScsBridge();
        flushFineBuffer();
        gameLog.flush();
        try {
          const r = await api.abandonContract();
          if (r.abandoned) console.log(`[contracts] trabajo abandonado — contrato #${cid} — penalización ${r.penalty} VTLGST`);
        } catch (err) {
          console.warn(`[contracts] error al registrar abandono: ${err.message}`);
        }
      }
    } else {
      noJobTickCount = 0;
    }
    return;
  }
  noJobTickCount = 0;
  if (waitingForNewJob) return;
  if (activeContractId && activeContractDestination &&
      activeContractDestination.toLowerCase() === destination.toLowerCase()) return;
  try {
    const result = await api.claimContract(origin, destination, cargo, plannedDistanceKm, jobIncome);
    if (result.success && result.contract) {
      activeContractId          = result.contract.id;
      activeContractDestination = result.contract.destination_city;
      resetNearDestination();
      console.log(`[contracts] reclamado #${result.contract.id}: ${result.contract.origin_city} -> ${result.contract.destination_city} | ${result.contract.reward_lgst} VTL`);
    }
  } catch (err) {
    console.warn(`[contracts] no se pudo reclamar: ${err.message}`);
  }
}

async function telemetryTick(api, sample) {
  const result = await api.sendTelemetry(sample);
  console.log(`[telemetry] enviado OK (contrato=${activeContractId || "ninguno"})`);
  if (result.delivery_completed) {
    console.log(`[delivery] entrega completada del contrato #${activeContractId}`);
    activeContractId = null;
    activeContractDestination = null;
    noJobTickCount = 0;
    waitingForNewJob = true;
    if (telemetrySource.setActiveContract) telemetrySource.setActiveContract(null);
  }
}

function isEts2Running() {
  const { execSync } = require("child_process");
  try {
    const out = execSync('tasklist /fi "imagename eq eurotrucks2.exe" /fo csv /nh', { encoding: "utf8", timeout: 3000, windowsHide: true });
    return out.toLowerCase().includes("eurotrucks2.exe");
  } catch (_) { return false; }
}

// Busca el directorio de saves de ETS2 del perfil más reciente.
function findEts2SaveDir() {
  const docsEts2 = path.join(process.env.USERPROFILE || os.homedir(), "Documents", "Euro Truck Simulator 2");
  const profilesDir = path.join(docsEts2, "profiles");
  if (!fs.existsSync(profilesDir)) return null;
  const profiles = fs.readdirSync(profilesDir)
    .map(p => ({ p, t: fs.statSync(path.join(profilesDir, p)).mtimeMs }))
    .sort((a, b) => b.t - a.t);
  if (!profiles.length) return null;
  return path.join(profilesDir, profiles[0].p, "save");
}

// Lee el game.log de ETS2 para detectar el último save que causó un crash
// por prefab no encontrado, y devuelve el slot name (ej: "autosave_job_5").
function detectCrashedSaveSlot() {
  const docsEts2 = path.join(process.env.USERPROFILE || os.homedir(), "Documents", "Euro Truck Simulator 2");
  // game.log está en la carpeta raíz de documentos de ETS2, no en el perfil
  const logPath = path.join(docsEts2, "game.log");
  if (!fs.existsSync(logPath)) return null;
  try {
    const log = fs.readFileSync(logPath, "utf8");
    // Buscar la última línea "Loading save" y el error de prefab
    const hasPrefabError = log.includes("Failed to find prefab");
    if (!hasPrefabError) return null;
    // Extraer el slot del último "Loading save"
    const matches = [...log.matchAll(/Loading save[^/\n]+\/save\/([^/]+)\/game\.sii/g)];
    if (!matches.length) return null;
    return matches[matches.length - 1][1]; // último slot cargado
  } catch (_) { return null; }
}

// Elimina el save que causó el crash (identificado por game.log) y además
// todos los slots de Quick Job (autosave_job_*) que son los que abren
// directamente la pantalla de selección de trabajo al cargarse.
function repairBrokenSaves() {
  const saveDir = findEts2SaveDir();
  if (!saveDir || !fs.existsSync(saveDir)) return;

  // 1. Identificar el slot crasheado desde game.log y eliminarlo
  const crashedSlot = detectCrashedSaveSlot();
  if (crashedSlot) {
    const crashedPath = path.join(saveDir, crashedSlot);
    if (fs.existsSync(crashedPath)) {
      try {
        const gameSii = path.join(crashedPath, "game.sii");
        const bak     = gameSii + ".bak_crash";
        if (fs.existsSync(gameSii)) {
          fs.renameSync(gameSii, bak);
          console.log(`[save-repair] save crasheado eliminado: ${crashedSlot} (era: ${bak})`);
        }
      } catch (e) { console.warn("[save-repair] no se pudo eliminar save crasheado:", e.message); }
    }
  }

  // 2. Eliminar TODOS los slots de quick-job (autosave_job_*) — siempre abren
  //    la pantalla de selección de trabajo y pueden crashear si hay prefabs rotos.
  try {
    const slots = fs.readdirSync(saveDir);
    for (const slot of slots) {
      if (!slot.startsWith("autosave_job")) continue;
      const gameSii = path.join(saveDir, slot, "game.sii");
      if (!fs.existsSync(gameSii)) continue;
      const bak = gameSii + ".bak_repair";
      if (fs.existsSync(bak)) continue; // ya reparado antes
      try {
        fs.renameSync(gameSii, bak);
        console.log(`[save-repair] quick-job save desactivado: ${slot}`);
      } catch (_) {}
    }
  } catch (_) {}

  console.log("[save-repair] listo — ETS2 abrirá el menú principal.");
  console.log("[save-repair] Selecciona 'Nuevo juego' y elige Hamburgo o Berlín como ciudad base (NO Rijeka).");
}

function launchEts2() {
  const { spawn, exec } = require("child_process");
  repairBrokenSaves();
  const ets2Dir = findEts2InstallDir();
  if (ets2Dir) {
    const exePath = path.join(ets2Dir, "bin", "win_x64", "eurotrucks2.exe");
    console.log(`[ets2] lanzando juego: ${exePath}`);
    spawn(exePath, ["-nointro", "-64bit"], { detached: true, stdio: "ignore" }).unref();
    return;
  }
  // No se encontró ninguna instalación (ni rutas por defecto, ni registro de
  // Steam, ni libraryfolders.vdf) — último recurso: Steam sabe siempre dónde
  // está instalado el juego independientemente de la biblioteca/unidad.
  console.warn("[ets2] no se encontró el ejecutable en ninguna ruta conocida; abriendo via steam://rungameid...");
  exec('start "" "steam://rungameid/227300"', { shell: true, windowsHide: true }, (err) => {
    if (err) console.warn("[ets2] no se pudo abrir Steam:", err.message);
  });
}

async function downloadMod(walletAddress, token) {
  const modDir = path.join(process.env.USERPROFILE || os.homedir(), "Documents", "Euro Truck Simulator 2", "mod");
  const localMod = path.join(modDir, "eurotrans_override.scs");
  const url = `${config.apiUrl}/player/mod.scs?wallet=${encodeURIComponent(walletAddress)}`;
  try {
    const res = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
    if (!res.ok) { console.warn(`[mod] servidor devolvió ${res.status}`); return; }
    const buf = Buffer.from(await res.arrayBuffer());
    fs.mkdirSync(modDir, { recursive: true });
    fs.writeFileSync(localMod, buf);
    console.log(`[mod] instalado (${buf.length} bytes)`);
  } catch (err) {
    console.warn("[mod] no se pudo descargar:", err.message);
  }
}

function loadSavedSession() {
  try {
    if (!fs.existsSync(FLAG_FILE)) return null;
    const saved = JSON.parse(fs.readFileSync(FLAG_FILE, "utf8"));
    if (!saved.token || !saved.wallet) return null;
    if (saved.expiresAt && Date.now() >= new Date(saved.expiresAt).getTime()) return null;
    return saved;
  } catch (_) { return null; }
}

function saveSession(wallet, token, expiresAt) {
  try {
    fs.mkdirSync(FLAG_DIR, { recursive: true });
    fs.writeFileSync(FLAG_FILE, JSON.stringify({ wallet, token, expiresAt, at: new Date().toISOString() }));
  } catch (_) {}
}

async function main() {
  startTelemetryServer();
  installScsPlugin();
  cleanLegacyLocaleFiles();
  console.log(`[launcher] API: ${config.apiUrl}`);

  let session = loadSavedSession();
  if (session) {
    console.log(`[auth] sesion reutilizada para ${session.wallet}`);
  } else {
    session = await browserLogin(config.apiUrl);
    saveSession(session.wallet, session.token, session.expiresAt);
    console.log(`[auth] sesion iniciada para ${session.wallet}`);
  }

  const walletAddress = session.wallet;
  const api = new ApiClient({ address: walletAddress });
  api.token = session.token;

  await api.request("POST", "/player/register", { wallet: walletAddress.toLowerCase() });
  await downloadMod(walletAddress.toLowerCase(), session.token);

  // Restaurar contrato activo si el launcher se reinició con un trabajo en curso
  try {
    const liveData = await api.request("GET", `/player/live?wallet=${encodeURIComponent(walletAddress.toLowerCase())}`);
    const liveContract = liveData && (liveData.active_contract || liveData.contract);
    if (liveContract && liveContract.id) {
      activeContractId          = liveContract.id;
      activeContractDestination = liveContract.destination_city;
      console.log(`[contracts] contrato activo restaurado: #${activeContractId} → ${activeContractDestination}`);
    }
  } catch (_) {}

  // Verificar suscripción activa y deuda pendiente antes de lanzar
  let subActive = false;
  let fineDebt  = 0;
  try {
    const subRes = await api.request("GET", `/player/subscription?wallet=${encodeURIComponent(walletAddress.toLowerCase())}`);
    subActive = !!(subRes && subRes.active);
    fineDebt  = Number(subRes && subRes.fine_debt) || 0;
    if (subActive) {
      console.log(`[subscription] activa — stake OK`);
    }
  } catch (err) {
    console.warn("[subscription] no se pudo verificar:", err.message);
  }

  if (!subActive) {
    console.log("[subscription] ⚠ Sin suscripción activa. Haz stake de 500+ VTLGST en el dashboard → pestaña 🎮 Acceso.");
    console.log("[subscription] Telemetría, heartbeat y contratos desactivados hasta activar stake.");
    try {
      require("child_process").execSync(
        'start "" "https://ets2.lastgamestudio.fun/dashboard/#acceso"',
        { shell: true, windowsHide: true }
      );
    } catch (_) {}
    console.log("[launcher] detenido. Vuelve a ejecutar EuroTrans tras activar la suscripción.");
    return;
  }

  if (fineDebt > 0) {
    // El launcher ya no bloquea el arranque por tener deuda: el neto positivo de
    // cada entrega se descuenta automaticamente de la deuda en el servidor hasta
    // saldarla (ver /telemetry). Bloquear aqui dejaba al jugador sin forma de
    // generar ese neto positivo, encerrandolo si no tenia VTLGST propio para
    // pagar on-chain.
    console.log(`[deuda] ⚠ Tienes una deuda pendiente de ${fineDebt.toFixed(4)} VTLGST.`);
    console.log("[deuda] Se descuenta sola del neto de tus entregas hasta saldarse — puedes seguir jugando con normalidad.");
    console.log("[deuda] Tambien puedes saldarla de golpe con un pago manual desde el dashboard → pestaña 🎮 Acceso.");
  }

  if (isEts2Running()) {
    console.log("[ets2] el juego ya está en ejecución.");
  } else {
    launchEts2();
    // Vigilar crash temprano: si ETS2 cierra en menos de 5 minutos, reparar saves.
    const crashWindow = 5 * 60 * 1000;
    const launchTime  = Date.now();
    let ets2Started   = false;
    const crashWatcher = setInterval(() => {
      const running = isEts2Running();
      if (running) { ets2Started = true; return; }
      if (!ets2Started && Date.now() - launchTime < 30000) return; // esperar arranque inicial
      if (Date.now() - launchTime > crashWindow) { clearInterval(crashWatcher); return; }
      clearInterval(crashWatcher);
      const elapsed = Math.round((Date.now() - launchTime) / 1000);
      console.log(`[ets2] el juego cerró ${elapsed}s después de arrancar — reparando saves y relanzando...`);
      repairBrokenSaves();
      // El cierre prematuro confirma que el fallo fue tecnico (save de quick-job
      // roto), no un abandono del jugador. Si habia un contrato activo, cancelarlo
      // ya mismo sin penalizacion — antes se dejaba que ensureActiveContract lo
      // detectara como "sin trabajo" 5 minutos despues y lo cobrara como abandono
      // completo (penalizacion = recompensa entera) pese a que el jugador nunca
      // llego a conducir.
      if (activeContractId) {
        const cid = activeContractId;
        activeContractId          = null;
        activeContractDestination = null;
        noJobTickCount            = 0;
        waitingForNewJob          = false;
        api.cancelContract(cid)
          .then(() => console.log(`[contracts] contrato #${cid} cancelado sin penalización tras el crash`))
          .catch(err => console.warn(`[contracts] no se pudo cancelar el contrato tras el crash: ${err.message}`));
      }
      setTimeout(() => launchEts2(), 3000);
    }, 5000);
  }

  modGuard.startGuard();

  // Resumen de estado cada 60s — para poder ver de un vistazo, sin tener que
  // cruzar decenas de lineas, en que punto exacto se corta la cadena
  // wallet -> suscripcion -> telemetria -> trabajo en curso.
  setInterval(() => {
    console.log(`[status] wallet=${walletAddress} suscripcion=${subActive ? "activa" : "inactiva"} contrato_activo=${activeContractId || "ninguno"} ets2_corriendo=${isEts2Running()} scs-bridge=${_scsBridgeEnabled ? "on" : "off"}`);
  }, 60000);

  setInterval(() => {
    api.heartbeat()
      .then(() => console.log(`[heartbeat] enviado OK`))
      .catch(err => console.warn(`[heartbeat] error: ${err.message}`));
  }, config.heartbeatIntervalMs);

  // scs-sdk-plugin bridge: detección completa de multas (velocidad, semáforo, colisión…)
  _scsBridgeEnabled = startScsBridge();
  if (_scsBridgeEnabled) {
    setInterval(_pollScsBridge, 1000);
    console.log("[scs-bridge] detección completa de multas activada");
  } else {
    console.log("[scs-bridge] no disponible — usando proxy de daño para colisiones");
  }

  // Poller REST de alta frecuencia (1s) — fallback si Funbit expone fine events
  startFinePoller();

  // Monitor de game.log cada 2s — fallback adicional
  setInterval(() => { gameLog.poll(); }, 2000);

  let _telemetryTickCount = 0;
  setInterval(async () => {
    try {
      const sample = await telemetrySource.read();
      _telemetryTickCount++;
      if (!sample) {
        // Se loguea cada ~1 minuto (no en cada tick de 10s) para no llenar el
        // log, pero deja constancia continua de que seguimos sin muestra.
        if (_telemetryTickCount % 6 === 0) {
          console.log(`[telemetry] sin muestra (game.connected=false o sin partida activa) — ${_telemetryTickCount} intentos desde el arranque`);
        }
        return;
      }
      if (_telemetryTickCount % 6 === 0) {
        console.log(`[telemetry] muestra: speed=${sample.speed} city=${sample.city || "-"} origin=${sample.origin || "-"} destination=${sample.destination || "-"} distancia_recorrida=${sample.distanceDrivenKm}km restante=${sample.remainingKm}km`);
      }

      // Fuente 1: scs-sdk-plugin bridge (cubre todos los tipos de multa)
      const scsFines  = flushScsBridge();
      // Fuente 2: Funbit REST 1s (solo si el plugin expone fine events)
      const pollFines = flushFineBuffer();
      // Fuente 3: game.log
      const logFines  = gameLog.flush();
      // Fuente 4: proxy de daño — siempre activo para colisiones; se suprime si
      // el bridge ya detectó una en este mismo tick para evitar doble conteo.
      const bridgeCollision = scsFines.collision + pollFines.collision + logFines.collision;
      const collisionProxy  = bridgeCollision === 0 ? detectCollisionByDamage(sample) : 0;

      carryFines.speeding  += scsFines.speeding  + pollFines.speeding  + logFines.speeding;
      carryFines.redLight  += scsFines.redLight  + pollFines.redLight  + logFines.redLight;
      carryFines.collision += scsFines.collision + pollFines.collision + logFines.collision + collisionProxy;
      carryFines.wrongWay  += scsFines.wrongWay  + pollFines.wrongWay  + logFines.wrongWay;
      carryFines.noLights  += scsFines.noLights  + pollFines.noLights  + logFines.noLights;
      carryFines.other     += scsFines.other     + pollFines.other     + logFines.other;
      carryFines.totalEur  += scsFines.totalEur  + pollFines.totalEur  + logFines.totalEur;

      sample.fines.speeding  += carryFines.speeding;
      sample.fines.redLight  += carryFines.redLight;
      sample.fines.collision += carryFines.collision;
      sample.fines.wrongWay  += carryFines.wrongWay;
      sample.fines.noLights  += carryFines.noLights;
      sample.fines.other     += carryFines.other;
      sample.fines.totalEur  += carryFines.totalEur;

      await ensureActiveContract(api, sample);
      await telemetryTick(api, sample);
      // Solo se llega aquí si el envío tuvo éxito: ya quedaron guardadas en
      // el servidor, así que se puede vaciar el acumulado de reintento.
      Object.keys(carryFines).forEach(k => (carryFines[k] = 0));
    } catch (err) {
      console.warn(`[telemetry] error: ${err.message} — multas pendientes de este tick se reintentarán en el siguiente`);
    }
  }, config.telemetryIntervalMs);

  console.log("[launcher] corriendo. Ctrl+C para salir.");
}

main().catch(err => {
  console.error("[launcher] error fatal:", err.message);
  process.exit(1);
});
