const http = require("node:http");
const crypto = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { URL } = require("node:url");
const { DatabaseSync } = require("node:sqlite");

const PORT = Number(process.env.TMARKET_PORT || process.env.PORT || 3030);
const HOST = process.env.TMARKET_HOST || "0.0.0.0";
const ALLOWED_CORS_ORIGIN = cleanText(process.env.TMARKET_ALLOWED_ORIGIN || "null");
const ROOT_DIR = __dirname;
const CONFIGURED_DB_PATH = cleanText(process.env.TMARKET_DB || "");
const DATA_DIR = process.env.TMARKET_DATA_DIR
  ? path.resolve(process.env.TMARKET_DATA_DIR)
  : (CONFIGURED_DB_PATH ? path.dirname(path.resolve(CONFIGURED_DB_PATH)) : path.join(ROOT_DIR, "data"));
const TASK_MEDIA_DIR = process.env.TMARKET_TASK_MEDIA_DIR
  ? path.resolve(process.env.TMARKET_TASK_MEDIA_DIR)
  : path.join(DATA_DIR, "task-media");
const DB_PATH = CONFIGURED_DB_PATH ? path.resolve(CONFIGURED_DB_PATH) : path.join(DATA_DIR, "tmarket.sqlite");
const BACKUP_DIR = process.env.TMARKET_BACKUP_DIR || path.join(ROOT_DIR, "backups");
const LICENSE_PATH = process.env.TMARKET_LICENSE || path.join(DATA_DIR, "license.json");
const LICENSE_CLOCK_PATH = process.env.TMARKET_LICENSE_CLOCK || path.join(process.env.ProgramData || DATA_DIR, "TMarketPOS", "license-clock.json");
const LICENSE_CLOCK_FALLBACK_PATH = path.join(DATA_DIR, "license-clock.json");
const LOCAL_INTEGRITY_KEY_PATH = process.env.TMARKET_LOCAL_INTEGRITY_KEY || path.join(process.env.ProgramData || DATA_DIR, "TMarketPOS", "local-integrity.key");
const LICENSE_ONLINE_CACHE_PATH = process.env.TMARKET_LICENSE_ONLINE_CACHE || path.join(DATA_DIR, "license-online-cache.json");
const LICENSE_VALIDATION_URL = cleanText(process.env.TMARKET_LICENSE_VALIDATION_URL || "");
const LICENSE_VALIDATION_GRACE_DAYS = Math.max(1, Number(process.env.TMARKET_LICENSE_GRACE_DAYS || 7) || 7);
const SERVER_ID_PATH = path.join(DATA_DIR, "server-id.txt");
const LICENSE_PUBLIC_KEY = process.env.TMARKET_LICENSE_PUBLIC_KEY || `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAM7ak4bs+0/CgXGivEr3Giz23HW0WC0QdhUqfGwV0vSM=
-----END PUBLIC KEY-----`;
const PORTABLE_APP_HTML = path.join(ROOT_DIR, "public", "Tmarket Cortes 9.5.html");
const PORTABLE_LOGO_PATH = path.join(ROOT_DIR, "public", "tmarket-logo.jfif");
const APP_HTML = process.env.TMARKET_HTML || PORTABLE_APP_HTML;
const LOGO_PATH = process.env.TMARKET_LOGO || PORTABLE_LOGO_PATH;
const APP_BUILD = "v5.8.20-impresion-directa";
const SERVER_BOOT_ID = `${Date.now().toString(36)}-${crypto.randomBytes(5).toString("hex")}`;
const AUDIT_MONETARY_AMOUNT_LIMIT = 1000000;
const PIN_HASH_PREFIX = "pbkdf2$";
const LOGIN_MAX_FAILED_ATTEMPTS = 5;
const LOGIN_WARNING_REMAINING_ATTEMPTS = 3;
const LOGIN_LOCK_MINUTES = 15;
const ADMIN_AUTH_ATTEMPT_KEY = "__admin_authorization__";
const SESSION_AUTH_ATTEMPT_PREFIX = "__session_pin__:";

fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(BACKUP_DIR, { recursive: true });
fs.mkdirSync(TASK_MEDIA_DIR, { recursive: true });

function openDatabaseConnection() {
  const connection = new DatabaseSync(DB_PATH);
  connection.exec("PRAGMA journal_mode = WAL;");
  connection.exec("PRAGMA busy_timeout = 5000;");
  connection.exec("PRAGMA foreign_keys = ON;");
  return connection;
}

function getDatabaseSignature() {
  try {
    const stat = fs.statSync(DB_PATH);
    return `${stat.size}:${Math.round(stat.mtimeMs)}`;
  } catch {
    return "";
  }
}

let db = openDatabaseConnection();
let dbSignature = getDatabaseSignature();

function refreshDatabaseConnectionIfNeeded() {
  const currentSignature = getDatabaseSignature();
  if(!currentSignature || currentSignature === dbSignature) return false;
  try {
    db.close();
  } catch {}
  db = openDatabaseConnection();
  dbSignature = currentSignature;
  initSchema();
  migrateLegacyTaskMedia();
  dbSignature = getDatabaseSignature();
  console.warn(`T Market POS reconecto la base SQLite porque el archivo cambio: ${DB_PATH}`);
  return true;
}

const sessions = new Map();
const rolePermissions = {
  admin: ["pos", "inventory", "departmentLabels", "admin", "caja", "customers", "checker", "employee", "discounts", "wholesale1", "wholesale2", "forcePrice", "posAdjustTotalPercent", "posAdjustTotalAmount", "posEditLinePrice", "withdrawals", "exportData", "manageCatalogs", "negativeAdjustments", "inventoryCatalog", "inventoryAdjustments", "purchaseNotes", "purchaseOrders", "purchaseEditCost", "purchaseEditSalePrice", "purchaseEditWholesalePrice", "shoppingList", "promotions", "forceCloseSessions", "cancelSales", "returnSales", "sellNegativeStock"],
  cashier: ["pos", "departmentLabels", "checker", "caja", "discounts", "wholesale1", "withdrawals"],
  employee: ["checker", "employee"]
};

const defaults = {
  departments: ["Electronica", "Oficina", "Empaque"],
  suppliers: [
    { name: "Proveedor Norte", aliases: [], promoter: "Sin asignar", visitDays: "Lunes", visitType: "Preventa", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "Packline", aliases: [], promoter: "Sin asignar", visitDays: "Miercoles", visitType: "Entrega", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "OfiMax", aliases: [], promoter: "Sin asignar", visitDays: "Viernes", visitType: "Preventa", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "Arca Continental", aliases: ["Arca", "Distribuidora Arca", "DAC", "Caj/Bot"], promoter: "", visitDays: "", visitType: "Entrega", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "BYDSA", aliases: ["Sabritas", "Antonio Enriquez", "Leo"], promoter: "", visitDays: "", visitType: "Entrega", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "Bokados", aliases: ["Bokachito", "Topito", "Enredos"], promoter: "", visitDays: "", visitType: "Preventa", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "Dulceria", aliases: ["Panditas", "Gomilocas", "Kranky", "Chocoretas"], promoter: "", visitDays: "", visitType: "Preventa", phone: "", supervisor: "", supervisorPhone: "" },
    { name: "Proveedor general", aliases: ["Num folio", "Razon social", "Cod tienda"], promoter: "", visitDays: "", visitType: "Preventa", phone: "", supervisor: "", supervisorPhone: "" }
  ],
  products: [
    { name: "Monitor 24 pulgadas", sku: "MON-024-HD", supplier: "Proveedor Norte", category: "Electronica", stock: 46, minimum: 12, costPrice: 1850, salePrice: 2450, wholesalePrice: 2250, location: "A-03", photo: "", aliases: [], supplierAliases: {} },
    { name: "Caja corrugada mediana", sku: "EMP-CM-118", supplier: "Packline", category: "Empaque", stock: 9, minimum: 25, costPrice: 12, salePrice: 18, wholesalePrice: 15, location: "C-12", photo: "", aliases: [], supplierAliases: {} },
    { name: "Silla ergonomica negra", sku: "OFC-SIL-701", supplier: "OfiMax", category: "Oficina", stock: 0, minimum: 8, costPrice: 2900, salePrice: 3890, wholesalePrice: 3590, location: "B-01", photo: "", aliases: [], supplierAliases: {} },
    { name: "Teclado mecanico compacto", sku: "TEC-MEC-060", supplier: "Proveedor Norte", category: "Electronica", stock: 31, minimum: 10, costPrice: 720, salePrice: 980, wholesalePrice: 890, location: "A-07", photo: "", aliases: [], supplierAliases: {} }
  ],
  departmentInventory: [
    { department: "Carniceria", sku: "CAR-BIS-001", name: "Bistec de res", stock: 20, unit: "Kg", price: 180, status: "Activo" },
    { department: "Papeleria", sku: "PAP-COP-001", name: "Copia blanco y negro", stock: 500, unit: "Pieza", price: 2, status: "Activo" },
    { department: "Ferreteria", sku: "FER-TOR-001", name: "Tornillo surtido", stock: 50, unit: "Paquete", price: 25, status: "Activo" },
    { department: "Cocina", sku: "COC-COM-001", name: "Comida corrida", stock: 20, unit: "Servicio", price: 85, status: "Activo" }
  ],
  settings: {
    storeName: "T Market",
    branchName: "Sucursal principal",
    defaultExchangeRate: null,
    openingCash: 1500,
    maxCashInDrawer: 8000,
    withdrawalLimit: 3000,
    printerSize: "58mm",
    directPrintEnabled: false,
    directPrintCopies: 1,
    appTheme: "light",
    customerDisplayEnabled: true,
    customerDisplayTitle: "Gracias por su compra",
    customerDisplayPlaylistText: "",
    customerDisplayDefaultDuration: 10,
    taskEvidenceRetention: "1year",
    taskEvidenceDeleteEnabled: false,
    taskEvidenceOriginalPolicy: "deleteAfterOptimize",
    ticketFooter: "Gracias por su compra",
    requireCashier: true,
    autoPrintTicket: true,
    showLogoTicket: false,
    showLogoQuote: true,
    ticketDescriptionMode: "short",
    ticketShowSku: true,
    ticketShowUnitPrice: true,
    ticketShowChange: true,
    ticketShowSavings: true,
    ticketFontFamily: "'Courier New', monospace",
    ticketFontSize: 9.2,
    ticketFontWeight: "900",
    ticketLineHeight: "1.12",
    ticketTextAlign: "left",
    ticketMarginX: 0.5,
    quotePrintFormat: "ticket",
    purchasePrintFormat: "letter",
    quoteTextAlign: "right",
    quoteMarginMm: 14,
    letterLogoWidthMm: 40,
    letterLogoAlign: "left",
    letterLogoOffsetX: 0,
    letterLogoOffsetY: 0,
    customLogo: "",
    ticketHeaderLines: "",
    ticketFooterLines: "",
    ticketTopSpacing: 0,
    ticketBottomSpacing: 0,
    quoteHeaderLines: "",
    quoteFooterLines: "",
    quoteTopSpacing: 0,
    quoteBottomSpacing: 0,
    letterHeaderHtml: "",
    letterFooterHtml: "",
    saleLetterHeaderHtml: "",
    saleLetterFooterHtml: "",
    quoteLetterHeaderHtml: "",
    quoteLetterFooterHtml: "",
    purchaseLetterHeaderHtml: "",
    purchaseLetterFooterHtml: "",
    requireWithdrawalAuth: true,
    cashierCashIncomeEnabled: true,
    cashierCashPaymentEnabled: true,
    cashPaymentAuthMode: "aboveLimit",
    cashPaymentPendingReview: true,
    requireCashIncomeCount: false,
    requireCashPaymentCount: false,
    printCashIncomeTicket: false,
    printCashPaymentTicket: false,
    printInternalWithdrawalTicket: true,
    cashTicketShowCashierSignature: true,
    cashTicketShowReceivedSignature: true,
    cashTicketPrintOriginalCopy: false,
    purchaseNotesUsersCanCreateOrders: true,
    purchaseNotesUsersCanReceiveOrders: true,
    purchaseReceiptsPendingReview: true,
    purchaseInventoryApplyMode: "provisional",
    purchaseCostChangeRequiresAdmin: false,
    scannerMode: "Teclado USB",
    cashDrawer: "Desactivado",
    scaleDevice: "Sin bascula",
    pinpadDevice: "Manual",
    customerCreditEnabled: true,
    loyaltyEnabled: true,
    loyaltyProgramName: "Puntos T Market",
    loyaltyPointValue: 1,
    loyaltyEarnMode: "amount",
    loyaltyAmountThreshold: 100,
    loyaltyPointsPerThreshold: 1,
    loyaltyEarnPercent: 1,
    loyaltyAskPhoneOnSale: true,
    customerPromptTiming: "payment",
    taxesEnabled: false,
    taxPriceMode: "included",
    ivaEnabled: true,
    ivaRate: 16,
    iepsEnabled: false,
    iepsRate: 8,
    deliveryEnabled: false,
    deliveryPartialCreditEnabled: false,
    deliveryPricePerKm: 0,
    deliveryFreeFrom: 0,
    allowNegativeStockSales: false,
    negativeStockSaleMode: "admin",
    negativeStockNoticeAt: 3,
    tempAdminPin: "",
    tempAdminPinExpiresAt: "",
    promotions: []
  },
  cashierUsers: [
    { name: "Administrador", pin: "0000", role: "admin", status: "Activo", permissions: [] },
    { name: "Cajero 1", pin: "739251", role: "cashier", status: "Inactivo", permissions: [] }
  ],
  cashierTasks: [
    { id: 1, title: "Revisar limpieza del area de caja", day: "Diario", shift: "Todos", cashier: "Todos", priority: "Normal", description: "Antes de iniciar turno, limpia mostrador, terminal, lector y zona visible para cliente.", subtasks: [{ id: 1, title: "Limpiar mostrador", done: false }, { id: 2, title: "Revisar terminal y lector", done: false }], photos: [], done: false },
    { id: 2, title: "Confirmar cambio disponible", day: "Diario", shift: "Matutino", cashier: "Todos", priority: "Alta", description: "Verificar fondo de caja y avisar si faltan monedas o billetes de baja denominacion.", subtasks: [{ id: 1, title: "Contar fondo inicial", done: false }, { id: 2, title: "Reportar faltantes", done: false }], photos: [], done: false }
  ]
};

function nowIso() {
  return new Date().toISOString();
}

function localDateInputValue(date = new Date()) {
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}

function cleanDateOnly(value, fallback = localDateInputValue()) {
  const text = cleanText(value);
  if(!text) return fallback;
  const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})/);
  if(iso) return `${iso[1]}-${iso[2]}-${iso[3]}`;
  const mx = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
  if(mx) return `${mx[3]}-${String(mx[2]).padStart(2, "0")}-${String(mx[1]).padStart(2, "0")}`;
  const mxShort = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})/);
  if(mxShort) return `${2000 + Number(mxShort[3])}-${String(mxShort[2]).padStart(2, "0")}-${String(mxShort[1]).padStart(2, "0")}`;
  const parsed = new Date(text);
  if(Number.isFinite(parsed.getTime())) return localDateInputValue(parsed);
  return fallback;
}

function getServerId() {
  try {
    const existing = fs.readFileSync(SERVER_ID_PATH, "utf8").trim();
    if(existing) return existing;
  } catch {}
  const id = `TM-${crypto.randomBytes(8).toString("hex").toUpperCase()}`;
  fs.writeFileSync(SERVER_ID_PATH, id, "utf8");
  return id;
}

function getMachineFingerprint() {
  const raw = [
    os.platform(),
    os.arch(),
    os.hostname(),
    process.env.COMPUTERNAME || "",
    process.env.USERDOMAIN || "",
    process.env.PROCESSOR_IDENTIFIER || ""
  ].map(cleanText).join("|");
  return crypto.createHash("sha256").update(raw).digest("hex");
}

function licenseSignaturePayload(input = {}) {
  const modules = input.modules && typeof input.modules === "object" ? input.modules : {};
  const moduleText = Object.keys(modules).sort().map(key => `${key}:${modules[key] ? 1 : 0}`).join(",");
  const pricing = input.pricing && typeof input.pricing === "object" ? input.pricing : {};
  return [
    cleanText(input.serverId),
    cleanText(input.licenseKey),
    cleanText(input.expiresAt),
    cleanText(input.plan, "mensual"),
    cleanText(input.posPackage || input.package || "pos_full"),
    cleanText(input.billing || "monthly"),
    cleanNumber(pricing.monthly),
    cleanNumber(pricing.annualMonthly),
    moduleText
  ].join("|");
}

function verifyAsymmetricLicenseSignature(input = {}) {
  const signature = cleanText(input.signatureV2 || input.signatureEd25519 || input.publicSignature);
  if(!signature) return false;
  try {
    return crypto.verify(
      null,
      Buffer.from(licenseSignaturePayload(input), "utf8"),
      LICENSE_PUBLIC_KEY,
      Buffer.from(signature, "base64")
    );
  } catch {
    return false;
  }
}

function hasValidLicenseSignature(input = {}) {
  return verifyAsymmetricLicenseSignature(input);
}

function getLocalIntegrityKey() {
  try {
    const existing = cleanText(fs.readFileSync(LOCAL_INTEGRITY_KEY_PATH, "utf8"));
    if(existing) return existing;
  } catch {}
  const generated = crypto.randomBytes(32).toString("base64");
  fs.mkdirSync(path.dirname(LOCAL_INTEGRITY_KEY_PATH), { recursive: true });
  fs.writeFileSync(LOCAL_INTEGRITY_KEY_PATH, generated, { encoding: "utf8", mode: 0o600 });
  return generated;
}

function clockMarkerSignature(input = {}) {
  return crypto.createHmac("sha256", getLocalIntegrityKey())
    .update([
      cleanText(input.machineFingerprint),
      cleanText(input.lastSeenAt),
      cleanText(input.lastSeenDate)
    ].join("|"))
    .digest("hex");
}

function readSignedJsonFromPaths(paths = []) {
  for(const filePath of paths.filter(Boolean)) {
    try {
      const data = jsonParse(fs.readFileSync(filePath, "utf8"), null);
      if(data && typeof data === "object") return data;
    } catch {}
  }
  return null;
}

function writeJsonToFirstAvailablePath(paths = [], payload = {}) {
  let lastError = null;
  for(const filePath of paths.filter(Boolean)) {
    try {
      fs.mkdirSync(path.dirname(filePath), { recursive: true });
      fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
      return filePath;
    } catch(error) {
      lastError = error;
    }
  }
  if(lastError) throw lastError;
  return "";
}

function checkAndUpdateLicenseClock() {
  const paths = [LICENSE_CLOCK_PATH, LICENSE_CLOCK_FALLBACK_PATH].filter((item, index, arr) => item && arr.indexOf(item) === index);
  const marker = readSignedJsonFromPaths(paths);
  const machineFingerprint = getMachineFingerprint();
  const now = new Date();
  const nowMs = now.getTime();
  const nowDate = now.toISOString().slice(0, 10);
  if(marker) {
    const normalized = {
      machineFingerprint: cleanText(marker.machineFingerprint),
      lastSeenAt: cleanText(marker.lastSeenAt),
      lastSeenDate: cleanText(marker.lastSeenDate),
      signature: cleanText(marker.signature)
    };
    const lastSeenMs = Date.parse(normalized.lastSeenAt);
    const validMarker = normalized.machineFingerprint === machineFingerprint
      && normalized.signature === clockMarkerSignature(normalized)
      && Number.isFinite(lastSeenMs);
    if(validMarker && nowMs + 12 * 60 * 60 * 1000 < lastSeenMs) {
      return {
        ok: false,
        status: "clock_rollback",
        message: "La fecha/hora de Windows parece anterior al ultimo uso registrado. Corrige la fecha o actualiza la licencia."
      };
    }
    if(validMarker && lastSeenMs > nowMs) {
      return { ok: true, status: "clock_recent", lastSeenAt: normalized.lastSeenAt };
    }
  }
  const nextMarker = {
    machineFingerprint,
    lastSeenAt: now.toISOString(),
    lastSeenDate: nowDate
  };
  nextMarker.signature = clockMarkerSignature(nextMarker);
  try {
    writeJsonToFirstAvailablePath(paths, nextMarker);
  } catch(error) {
    console.warn("No se pudo actualizar marcador anti-reloj:", error.message || error);
  }
  return { ok: true, status: "clock_ok", lastSeenAt: nextMarker.lastSeenAt };
}

function readOnlineLicenseCache() {
  return jsonParse(fs.existsSync(LICENSE_ONLINE_CACHE_PATH) ? fs.readFileSync(LICENSE_ONLINE_CACHE_PATH, "utf8") : "", {});
}

function writeOnlineLicenseCache(cache) {
  fs.mkdirSync(path.dirname(LICENSE_ONLINE_CACHE_PATH), { recursive: true });
  fs.writeFileSync(LICENSE_ONLINE_CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
}

function getOnlineLicenseStatusForLicense(license = {}) {
  if(!LICENSE_VALIDATION_URL) {
    return { enabled: false, ok: true, status: "disabled", message: "Validacion online no configurada." };
  }
  const cache = readOnlineLicenseCache();
  const licenseKey = cleanText(license.licenseKey);
  const row = cache[licenseKey] || {};
  const lastOkMs = Date.parse(row.lastOkAt || "");
  const graceMs = LICENSE_VALIDATION_GRACE_DAYS * 86400000;
  if(row.status === "revoked" || row.status === "expired" || row.status === "invalid") {
    return { enabled: true, ok: false, status: row.status, message: row.message || "La validacion online rechazo la licencia.", lastCheckedAt: row.lastCheckedAt || "" };
  }
  if(Number.isFinite(lastOkMs) && Date.now() - lastOkMs <= graceMs) {
    return { enabled: true, ok: true, status: "validated", message: "Licencia validada online.", lastOkAt: row.lastOkAt, lastCheckedAt: row.lastCheckedAt || row.lastOkAt };
  }
  if(row.firstPendingAt) {
    const firstPendingMs = Date.parse(row.firstPendingAt);
    if(Number.isFinite(firstPendingMs) && Date.now() - firstPendingMs > graceMs) {
      return { enabled: true, ok: false, status: "online_grace_expired", message: `No se pudo validar online dentro de ${LICENSE_VALIDATION_GRACE_DAYS} dias.`, lastCheckedAt: row.lastCheckedAt || "" };
    }
  }
  return { enabled: true, ok: true, status: "pending", message: `Validacion online pendiente. Tolerancia: ${LICENSE_VALIDATION_GRACE_DAYS} dias.`, lastCheckedAt: row.lastCheckedAt || "" };
}

async function validateLicenseOnlineNow(reason = "scheduled") {
  if(!LICENSE_VALIDATION_URL) return null;
  const license = normalizeLicenseInput({ license: readLicenseFile() || {} });
  if(!license.licenseKey) return null;
  const licenseKey = license.licenseKey;
  const cache = readOnlineLicenseCache();
  const previous = cache[licenseKey] || {};
  const payload = {
    serverId: getServerId(),
    licenseKey,
    licenseType: license.licenseType,
    posPackage: license.posPackage,
    appPackage: license.appPackage,
    expiresAt: license.expiresAt,
    appBuild: APP_BUILD,
    reason
  };
  try {
    const response = await fetch(LICENSE_VALIDATION_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload)
    });
    const data = await response.json().catch(() => ({}));
    const accepted = response.ok && data && data.active !== false && !["revoked", "expired", "invalid"].includes(cleanText(data.status).toLowerCase());
    cache[licenseKey] = {
      ...previous,
      status: accepted ? "validated" : cleanText(data.status, response.ok ? "invalid" : "error"),
      message: cleanText(data.message || data.error || (accepted ? "Licencia validada online." : "Licencia no validada online.")),
      lastCheckedAt: nowIso(),
      lastOkAt: accepted ? nowIso() : previous.lastOkAt || "",
      firstPendingAt: accepted ? "" : (previous.firstPendingAt || nowIso())
    };
    writeOnlineLicenseCache(cache);
    return cache[licenseKey];
  } catch(error) {
    cache[licenseKey] = {
      ...previous,
      status: "offline",
      message: error.message || "No se pudo contactar el servidor de licencias.",
      lastCheckedAt: nowIso(),
      firstPendingAt: previous.firstPendingAt || nowIso()
    };
    writeOnlineLicenseCache(cache);
    return cache[licenseKey];
  }
}

function scheduleOnlineLicenseValidation() {
  if(!LICENSE_VALIDATION_URL) return;
  setTimeout(() => validateLicenseOnlineNow("startup").catch(error => console.warn("Validacion online fallida:", error.message || error)), 1500);
  setInterval(() => validateLicenseOnlineNow("interval").catch(error => console.warn("Validacion online fallida:", error.message || error)), 6 * 60 * 60 * 1000).unref?.();
}

function getDefaultLicenseModules(posPackage = "pos_full", appPackage = "none") {
  const cleanPackage = cleanText(posPackage, "pos_full");
  const modules = {
    posSales: true,
    inventory: true,
    cashManagement: cleanPackage !== "pos_lite",
    admin: cleanPackage !== "pos_lite",
    reports: cleanPackage !== "pos_lite",
    purchases: cleanPackage !== "pos_lite",
    promotions: cleanPackage !== "pos_lite",
    internalLabels: cleanPackage === "pos_full",
    appAndroid: appPackage !== "none",
    appInventory: appPackage !== "none",
    appSales: appPackage === "app_full" || appPackage === "app_vip",
    appAdmin: appPackage === "app_vip"
  };
  return modules;
}

function getLicensePricing(posPackage = "pos_full", billing = "monthly", appPackage = "none", appBilling = "none") {
  const posPrices = {
    pos_full: { monthly: 600, annualMonthly: 450, label: "POS completa" },
    pos_semi: { monthly: 400, annualMonthly: 300, label: "POS semi completa" },
    pos_lite: { monthly: 200, annualMonthly: 150, label: "POS lite" }
  };
  const appPrices = {
    none: { monthly: 0, annualMonthly: 0, label: "Sin app" },
    app_basic: { monthly: 100, annualMonthly: 50, label: "App inventario" },
    app_full: { monthly: 100, annualMonthly: 50, label: "App ventas" },
    app_vip: { monthly: 0, annualMonthly: 0, label: "App VIP incluida" }
  };
  const pos = posPrices[posPackage] || posPrices.pos_full;
  const app = appPrices[appPackage] || appPrices.none;
  const isAnnual = cleanText(billing, "monthly") === "annual";
  const appIsAnnual = cleanText(appBilling, isAnnual ? "annual" : "monthly") === "annual";
  return {
    monthly: isAnnual ? pos.annualMonthly : pos.monthly,
    annualMonthly: pos.annualMonthly,
    appMonthly: appPackage === "none" ? 0 : (appIsAnnual ? app.annualMonthly : app.monthly),
    appAnnualMonthly: app.annualMonthly,
    totalMonthly: (isAnnual ? pos.annualMonthly : pos.monthly) + (appPackage === "none" ? 0 : (appIsAnnual ? app.annualMonthly : app.monthly)),
    currency: "MXN",
    posLabel: pos.label,
    appLabel: app.label
  };
}

function normalizeLicenseInput(input = {}) {
  let source = input.license || input;
  const text = cleanText(input.licenseText || input.text || input.code);
  if(text) {
    try {
      source = JSON.parse(text);
    } catch {
      throw httpError(400, "La licencia debe pegarse como JSON valido.");
    }
  }
  return {
    serverId: cleanText(source.serverId),
    licenseKey: cleanText(source.licenseKey || source.key),
    plan: cleanText(source.plan, "mensual"),
    posPackage: cleanText(source.posPackage || source.package, "pos_full"),
    billing: cleanText(source.billing, "monthly"),
    appPackage: cleanText(source.appPackage, "none"),
    appBilling: cleanText(source.appBilling, source.billing || "monthly"),
    licenseType: cleanText(source.licenseType || source.type || (String(source.licenseKey || source.key || "").startsWith("TRIAL-") ? "trial" : "paid")),
    modules: source.modules && typeof source.modules === "object" ? source.modules : getDefaultLicenseModules(source.posPackage || source.package, source.appPackage),
    pricing: source.pricing && typeof source.pricing === "object" ? source.pricing : getLicensePricing(source.posPackage || source.package || "pos_full", source.billing || "monthly", source.appPackage || "none", source.appBilling || source.billing || "monthly"),
    expiresAt: cleanText(source.expiresAt || source.expires),
    issuedAt: cleanText(source.issuedAt, nowIso()),
    signature: cleanText(source.signature),
    signatureV2: cleanText(source.signatureV2 || source.signatureEd25519 || source.publicSignature),
    signatureAlgorithm: cleanText(source.signatureAlgorithm || (source.signatureV2 ? "Ed25519" : "HMAC-SHA256"))
  };
}

function readLicenseFile() {
  try {
    return jsonParse(fs.readFileSync(LICENSE_PATH, "utf8"), null);
  } catch {
    return null;
  }
}

function writeLicenseFile(license) {
  fs.writeFileSync(LICENSE_PATH, JSON.stringify(license, null, 2), "utf8");
}

function getLicenseStatus() {
  const serverId = getServerId();
  const license = readLicenseFile();
  if(!license) {
    return {
      ok: true,
      serverId,
      active: false,
      status: "activation_required",
      message: "Este servidor requiere una licencia firmada para comenzar.",
      licenseKey: "",
      licenseType: "none",
      modules: {},
      expiresAt: "",
      daysRemaining: 0,
      signatureMode: "none",
      onlineValidation: { enabled: Boolean(LICENSE_VALIDATION_URL), ok: true, status: "pending", message: "Licencia pendiente de activacion." }
    };
  }
  const normalized = normalizeLicenseInput({ license });
  const expiresAt = normalized.expiresAt;
  const expiresTime = expiresAt ? new Date(`${expiresAt}T23:59:59`).getTime() : NaN;
  const nowTime = Date.now();
  const validServer = normalized.serverId === serverId;
  const validSignature = hasValidLicenseSignature(normalized);
  const hasValidDate = Number.isFinite(expiresTime);
  const clockStatus = checkAndUpdateLicenseClock();
  const onlineStatus = getOnlineLicenseStatusForLicense(normalized);
  const active = validServer && validSignature && hasValidDate && expiresTime >= nowTime && clockStatus.ok && onlineStatus.ok;
  const daysRemaining = hasValidDate ? Math.ceil((expiresTime - nowTime) / 86400000) : 0;
  const licenseType = cleanText(normalized.licenseType, normalized.licenseKey?.startsWith("TRIAL-") ? "trial" : "paid");
  let status = active ? (licenseType === "trial" ? "trial" : "active") : "active";
  let message = licenseType === "trial" ? "Licencia de prueba activa." : "Licencia activa.";
  if(!validServer) {
    status = "invalid";
    message = "La licencia pertenece a otro servidor.";
  } else if(!validSignature) {
    status = "invalid";
    message = "La firma de la licencia no es valida.";
  } else if(!hasValidDate) {
    status = "invalid";
    message = "La licencia no tiene fecha de vencimiento valida.";
  } else if(!clockStatus.ok) {
    status = clockStatus.status || "clock_error";
    message = clockStatus.message || "La fecha/hora del equipo no es confiable.";
  } else if(!onlineStatus.ok) {
    status = onlineStatus.status || "online_invalid";
    message = onlineStatus.message || "La licencia no paso la validacion online.";
  } else if(!active) {
    status = "expired";
    message = "Licencia vencida. No se permiten nuevas ventas.";
  }
  return {
    ok: true,
    serverId,
    active,
    status,
    message,
    licenseKey: normalized.licenseKey,
    licenseType,
    plan: normalized.plan,
    posPackage: normalized.posPackage,
    billing: normalized.billing,
    appPackage: normalized.appPackage,
    appBilling: normalized.appBilling,
    modules: normalized.modules,
    pricing: normalized.pricing,
    expiresAt,
    daysRemaining: Math.max(0, daysRemaining),
    signatureMode: normalized.signatureV2 ? "public-key" : "invalid",
    onlineValidation: onlineStatus,
    clock: clockStatus
  };
}

function saveLicense(input = {}, sessionContext = null) {
  const license = normalizeLicenseInput(input);
  const serverId = getServerId();
  if(license.serverId !== serverId) throw httpError(400, `Esta licencia es para otro servidor. ID de este servidor: ${serverId}`);
  if(!license.licenseKey) throw httpError(400, "Clave de licencia requerida.");
  if(!license.expiresAt || Number.isNaN(new Date(`${license.expiresAt}T23:59:59`).getTime())) throw httpError(400, "Fecha de vencimiento invalida.");
  if(!hasValidLicenseSignature(license)) throw httpError(400, "Firma de licencia invalida.");
  writeLicenseFile(license);
  logAuditEvent({
    type: "Licencia actualizada",
    severity: "Alta",
    reference: license.licenseKey,
    details: `Licencia ${license.plan} guardada con vencimiento ${license.expiresAt}.`,
    payload: { serverId: license.serverId, plan: license.plan, posPackage: license.posPackage, appPackage: license.appPackage, expiresAt: license.expiresAt }
  }, sessionContext);
  return getLicenseStatus();
}

function licenseAllowsModule(moduleName) {
  const status = getLicenseStatus();
  return Boolean(status.active && status.modules && status.modules[moduleName]);
}

function requireActiveLicenseForModule(moduleName, label = "esta funcion") {
  const status = getLicenseStatus();
  if(status.active && (!moduleName || status.modules?.[moduleName])) return status;
  if(status.active && moduleName) throw httpError(402, `Tu licencia activa no incluye ${label}.`);
  throw httpError(402, `${status.message} Siempre puedes entrar a administracion y generar respaldos.`);
}

function getNetworkIPv4Addresses() {
  const addresses = [];
  const interfaces = os.networkInterfaces();
  Object.entries(interfaces).forEach(([name, rows]) => {
    (rows || []).forEach(row => {
      if(row.family !== "IPv4" || row.internal) return;
      if(row.address.startsWith("169.254.")) return;
      if(row.address.startsWith("192.168.56.")) return;
      const adapterName = String(name || "").toLowerCase();
      if(adapterName.includes("virtual") || adapterName.includes("vmware") || adapterName.includes("vbox") || adapterName.includes("hyper-v")) return;
      addresses.push({ name, address: row.address, url: `http://${row.address}:${PORT}` });
    });
  });
  return addresses;
}

function getConnectionInfo() {
  const addresses = getNetworkIPv4Addresses();
  return {
    ok: true,
    app: "TMarketPOS",
    name: "T Market POS Server",
    build: APP_BUILD,
    port: PORT,
    localUrl: `http://localhost:${PORT}`,
    urls: addresses,
    primaryUrl: addresses[0]?.url || `http://localhost:${PORT}`,
    serverTime: nowIso()
  };
}

function readInstallManifest() {
  const manifestPath = path.join(ROOT_DIR, "install-manifest.json");
  if(!fs.existsSync(manifestPath)) return null;
  return jsonParse(fs.readFileSync(manifestPath, "utf8"), null);
}

function getLocalLibraryStatus() {
  const vendorDir = path.join(path.dirname(APP_HTML), "vendor");
  const expected = [
    "tesseract.min.js",
    "worker.min.js",
    "xlsx.full.min.js",
    "jspdf.umd.min.js",
    "JsBarcode.all.min.js",
    "lucide.min.js"
  ];
  return {
    vendorDir,
    items: expected.map(file => {
      const fullPath = path.join(vendorDir, file);
      const exists = fs.existsSync(fullPath);
      return {
        file,
        exists,
        sizeBytes: exists ? fs.statSync(fullPath).size : 0
      };
    })
  };
}


function safeDbCount(tableName, whereClause = "", params = []) {
  const allowed = new Set([
    "products", "sales", "customers", "cash_movements", "cut_reports", "purchase_notes",
    "audit_events", "inventory_adjustments", "department_labels", "cashier_users", "cashier_tasks"
  ]);
  if(!allowed.has(tableName)) return 0;
  try {
    const row = db.prepare(`SELECT COUNT(*) AS total FROM ${tableName}${whereClause ? ` ${whereClause}` : ""}`).get(...params);
    return cleanInteger(row?.total || 0);
  } catch {
    return 0;
  }
}

function getPerformanceSnapshot() {
  const productCount = safeDbCount("products");
  const saleCount = safeDbCount("sales");
  const customerCount = safeDbCount("customers");
  const purchaseCount = safeDbCount("purchase_notes");
  const auditCount = safeDbCount("audit_events");
  const cashMovementCount = safeDbCount("cash_movements");
  const activeProducts = safeDbCount("products", "WHERE COALESCE(product_status, 'Activo') NOT IN ('Inactivo', 'inactive', 'pausado', 'paused', 'Pausado')");
  const pendingAudit = safeDbCount("audit_events", "WHERE COALESCE(status, '') NOT IN ('reviewed', 'completed', 'resuelto', 'resolved')");
  const dbExists = fs.existsSync(DB_PATH);
  const dbStat = dbExists ? fs.statSync(DB_PATH) : null;
  const warnings = [];
  if(productCount >= 3000) warnings.push("Catalogo grande: conviene cargar inventario por busqueda/paginacion en vez de enviar todo en /state.");
  if(saleCount >= 10000) warnings.push("Historial de ventas grande: conviene paginar reportes y evitar cargar todo al iniciar.");
  if(customerCount >= 500) warnings.push("Clientes numerosos: conviene paginar/buscar clientes bajo demanda.");
  if(auditCount >= 1000) warnings.push("Auditoria grande: conviene mantener solo ultimos eventos en estado y consultar historial por filtro.");
  if(dbStat && dbStat.size > 250 * 1024 * 1024) warnings.push("Base de datos pesada: conviene hacer respaldo, VACUUM/compactacion y revisar indices.");
  return {
    generatedAt: nowIso(),
    counts: {
      products: productCount,
      activeProducts,
      sales: saleCount,
      customers: customerCount,
      purchasesReceipts: purchaseCount,
      cashMovements: cashMovementCount,
      auditEvents: auditCount,
      pendingAuditEvents: pendingAudit
    },
    databaseSizeBytes: dbStat ? dbStat.size : 0,
    warnings,
    previousRecommendation: "Dividir /api/state en estados parciales o endpoints por modulo, usar modo light por defecto y paginar ventas, auditoria, clientes y compras cuando crezcan los datos."
  };
}

function getRecentAuditSnapshot(limit = 80) {
  try {
    return db.prepare("SELECT id, type, severity, title, message, status, source, related_type, related_id, created_at, reviewed_at, reviewed_by FROM audit_events ORDER BY id DESC LIMIT ?")
      .all(Math.max(1, Math.min(200, cleanInteger(limit || 80))))
      .map(row => ({
        id: row.id,
        type: row.type,
        severity: row.severity,
        title: row.title,
        message: row.message,
        status: row.status,
        source: row.source,
        relatedType: row.related_type,
        relatedId: row.related_id,
        createdAt: row.created_at,
        reviewedAt: row.reviewed_at,
        reviewedBy: row.reviewed_by
      }));
  } catch {
    return [];
  }
}

function getSupportPackage(sessionContext = null) {
  const diagnostics = getServerDiagnostics(sessionContext);
  const performance = getPerformanceSnapshot();
  const settings = getSettings();
  const safeSettings = {
    printerSize: settings.printerSize,
    appTheme: settings.appTheme,
    quotePrintFormat: settings.quotePrintFormat,
    purchasePrintFormat: settings.purchasePrintFormat,
    purchaseInventoryApplyMode: settings.purchaseInventoryApplyMode,
    purchaseReceiptsPendingReview: settings.purchaseReceiptsPendingReview,
    purchaseNotesUsersCanCreateOrders: settings.purchaseNotesUsersCanCreateOrders,
    purchaseNotesUsersCanReceiveOrders: settings.purchaseNotesUsersCanReceiveOrders,
    cashierCashIncomeEnabled: settings.cashierCashIncomeEnabled,
    cashierCashPaymentEnabled: settings.cashierCashPaymentEnabled,
    cashPaymentAuthMode: settings.cashPaymentAuthMode,
    cashPaymentPendingReview: settings.cashPaymentPendingReview,
    withdrawalLimit: settings.withdrawalLimit,
    allowNegativeStockSales: settings.allowNegativeStockSales,
    negativeStockSaleMode: settings.negativeStockSaleMode
  };
  return {
    ok: true,
    packageType: "tmarket-support-package",
    generatedAt: nowIso(),
    appBuild: APP_BUILD,
    serverId: getServerId(),
    diagnostics,
    performance,
    safeSettings,
    recentAuditEvents: getRecentAuditSnapshot(80),
    recentBackups: listDatabaseBackups().slice(0, 20),
    notes: [
      "Este paquete no incluye la base SQLite completa ni el catalogo completo.",
      "Sirve para revisar version, rutas, respaldos, licencia, rendimiento y ultimos eventos de auditoria.",
      "Si el problema es de datos, genera tambien un respaldo manual antes de pedir soporte."
    ]
  };
}

function getServerDiagnostics(sessionContext = null) {
  const backups = listDatabaseBackups();
  const dailyBackups = backups.filter(item => item.category === "daily" || /daily/i.test(item.path || item.fileName || ""));
  const preUpdateBackups = backups.filter(item => /pre-update|installer-backup/i.test(item.path || item.fileName || item.reason || ""));
  const dbExists = fs.existsSync(DB_PATH);
  const dbStat = dbExists ? fs.statSync(DB_PATH) : null;
  return {
    ok: true,
    generatedAt: new Date().toLocaleString("es-MX"),
    connection: getConnectionInfo(),
    appBuild: APP_BUILD,
    performance: getPerformanceSnapshot(),
    rootDir: ROOT_DIR,
    appHtml: APP_HTML,
    database: {
      path: DB_PATH,
      exists: dbExists,
      sizeBytes: dbStat ? dbStat.size : 0,
      modifiedAt: dbStat ? dbStat.mtime.toLocaleString("es-MX") : ""
    },
    backups: {
      root: BACKUP_DIR,
      total: backups.length,
      latest: backups[0] || null,
      latestDaily: dailyBackups[0] || null,
      latestPreUpdate: preUpdateBackups[0] || null
    },
    installManifest: readInstallManifest(),
    localLibraries: getLocalLibraryStatus(),
    license: getLicenseStatus(),
    session: {
      user: sessionContext?.user?.name || "",
      role: sessionContext?.user?.role || "",
      cashSessionId: sessionContext?.cashSession?.id || "",
      cashSessionStatus: sessionContext?.cashSession?.status || ""
    }
  };
}


function jsonString(value) {
  return JSON.stringify(value ?? null);
}

function jsonParse(value, fallback) {
  if(value === null || value === undefined || value === "") return fallback;
  try {
    return JSON.parse(typeof value === "string" ? value.replace(/^\uFEFF/, "") : value);
  } catch {
    return fallback;
  }
}

function cleanText(value, fallback = "") {
  const text = String(value ?? "").trim();
  return text || fallback;
}

function cleanNumber(value, fallback = 0) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function isHashedPin(value) {
  return cleanText(value).startsWith(PIN_HASH_PREFIX);
}

function hashPin(pin) {
  const cleanPin = cleanText(pin);
  const iterations = 120000;
  const salt = crypto.randomBytes(16).toString("hex");
  const digest = crypto.pbkdf2Sync(cleanPin, salt, iterations, 32, "sha256").toString("hex");
  return `${PIN_HASH_PREFIX}${iterations}$${salt}$${digest}`;
}

function verifyPin(pin, storedPin) {
  const cleanPin = cleanText(pin);
  const stored = cleanText(storedPin);
  if(!cleanPin || !stored) return false;
  if(!isHashedPin(stored)) return stored === cleanPin;
  const parts = stored.split("$");
  if(parts.length !== 4 || parts[0] !== "pbkdf2") return false;
  const iterations = cleanInteger(parts[1]);
  const salt = parts[2];
  const expectedHex = parts[3];
  if(!iterations || !salt || !expectedHex) return false;
  try {
    const expected = Buffer.from(expectedHex, "hex");
    const actual = crypto.pbkdf2Sync(cleanPin, salt, iterations, expected.length, "sha256");
    return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
  } catch {
    return false;
  }
}

function migrateCashierPinIfNeeded(row, pin) {
  if(!row || isHashedPin(row.pin) || !verifyPin(pin, row.pin)) return;
  db.prepare("UPDATE cashier_users SET pin = ? WHERE name = ?").run(hashPin(pin), row.name);
}

function cleanMoney(value, fallback = 0) {
  const number = cleanNumber(value, fallback);
  return Math.round((number + Number.EPSILON) * 100) / 100;
}

function cleanPriceDecimal(value, fallback = 0) {
  return cleanNumber(value, fallback);
}

function cleanInteger(value, fallback = 0) {
  return Math.round(cleanNumber(value, fallback));
}

function makeSku(name, index = 0) {
  const base = cleanText(name, "PROD")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toUpperCase()
    .replace(/[^A-Z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 18) || "PROD";
  return `${base}-${String(Date.now()).slice(-5)}${index ? `-${index}` : ""}`;
}

function cleanDepartmentSku(value) {
  return cleanText(value)
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toUpperCase()
    .replace(/[^A-Z0-9-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 32);
}

function makeDepartmentLabelCode() {
  const stamp = Date.now().toString(36).toUpperCase().slice(-7);
  const rand = crypto.randomBytes(2).toString("hex").toUpperCase();
  return `DL-${stamp}-${rand}`;
}

function initSchema() {
  db.exec(`
    CREATE TABLE IF NOT EXISTS departments (
      name TEXT PRIMARY KEY,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS suppliers (
      name TEXT PRIMARY KEY,
      aliases_json TEXT NOT NULL DEFAULT '[]',
      promoter TEXT NOT NULL DEFAULT '',
      visit_days TEXT NOT NULL DEFAULT '',
      delivery_days TEXT NOT NULL DEFAULT '',
      visit_type TEXT NOT NULL DEFAULT 'Preventa',
      phone TEXT NOT NULL DEFAULT '',
      supervisor TEXT NOT NULL DEFAULT '',
      supervisor_phone TEXT NOT NULL DEFAULT '',
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS products (
      sku TEXT PRIMARY KEY,
      name TEXT NOT NULL,
      supplier TEXT NOT NULL DEFAULT '',
      category TEXT NOT NULL DEFAULT '',
      stock INTEGER NOT NULL DEFAULT 0,
      minimum_stock INTEGER NOT NULL DEFAULT 0,
      cost_price REAL NOT NULL DEFAULT 0,
      sale_price REAL NOT NULL DEFAULT 0,
      wholesale_price REAL NOT NULL DEFAULT 0,
      wholesale_price_2 REAL NOT NULL DEFAULT 0,
      wholesale_qty_1 INTEGER NOT NULL DEFAULT 0,
      wholesale_qty_2 INTEGER NOT NULL DEFAULT 0,
      package_pieces INTEGER NOT NULL DEFAULT 1,
      bundle_json TEXT NOT NULL DEFAULT '[]',
      location TEXT NOT NULL DEFAULT '',
      photo TEXT NOT NULL DEFAULT '',
      aliases_json TEXT NOT NULL DEFAULT '[]',
      supplier_aliases_json TEXT NOT NULL DEFAULT '{}',
      supplier_options_json TEXT NOT NULL DEFAULT '[]',
      product_type TEXT NOT NULL DEFAULT 'sale',
      sale_mode TEXT NOT NULL DEFAULT 'unit',
      product_status TEXT NOT NULL DEFAULT 'Activo',
      tax_enabled INTEGER NOT NULL DEFAULT 0,
      tax_country TEXT NOT NULL DEFAULT 'MX',
      tax_name TEXT NOT NULL DEFAULT 'IVA',
      tax_rate REAL NOT NULL DEFAULT 16,
      inactivity_keep_active_at TEXT NOT NULL DEFAULT '',
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS sales (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      folio TEXT NOT NULL UNIQUE,
      session_id TEXT NOT NULL DEFAULT '',
      date TEXT NOT NULL,
      total REAL NOT NULL,
      paid REAL NOT NULL,
      change_amount REAL NOT NULL,
      cashier TEXT NOT NULL DEFAULT '',
      payments_json TEXT NOT NULL DEFAULT '{}',
      ticket TEXT NOT NULL DEFAULT '',
      status TEXT NOT NULL DEFAULT 'Completada',
      cancel_reason TEXT NOT NULL DEFAULT '',
      cancelled_at TEXT NOT NULL DEFAULT '',
      cancelled_by TEXT NOT NULL DEFAULT '',
      cancelled_session_id TEXT NOT NULL DEFAULT '',
      customer_id INTEGER NOT NULL DEFAULT 0,
      customer_name TEXT NOT NULL DEFAULT '',
      loyalty_phone TEXT NOT NULL DEFAULT '',
      loyalty_points REAL NOT NULL DEFAULT 0,
      client_request_id TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS sale_items (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      sale_id INTEGER NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
      sku TEXT NOT NULL,
      name TEXT NOT NULL,
      quantity INTEGER NOT NULL,
      price REAL NOT NULL,
      cost_price REAL NOT NULL DEFAULT 0,
      price_type TEXT NOT NULL DEFAULT 'sale',
      bundle_json TEXT NOT NULL DEFAULT '[]',
      line_total REAL NOT NULL
    );

    CREATE TABLE IF NOT EXISTS quotations (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      folio TEXT NOT NULL UNIQUE,
      date TEXT NOT NULL,
      expires_at TEXT NOT NULL DEFAULT '',
      cashier TEXT NOT NULL DEFAULT '',
      total REAL NOT NULL DEFAULT 0,
      format TEXT NOT NULL DEFAULT 'ticket',
      status TEXT NOT NULL DEFAULT 'Activa',
      customer_id INTEGER NOT NULL DEFAULT 0,
      customer_name TEXT NOT NULL DEFAULT '',
      cart_json TEXT NOT NULL DEFAULT '[]',
      quote_text TEXT NOT NULL DEFAULT '',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS cash_movements (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      session_id TEXT NOT NULL DEFAULT '',
      type TEXT NOT NULL CHECK(type IN ('income', 'payment')),
      amount REAL NOT NULL,
      method TEXT NOT NULL,
      concept TEXT NOT NULL,
      reference TEXT NOT NULL DEFAULT '',
      cashier TEXT NOT NULL DEFAULT '',
      date TEXT NOT NULL,
      client_request_id TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS cash_sessions (
      id TEXT PRIMARY KEY,
      user_name TEXT NOT NULL,
      opened_at TEXT NOT NULL,
      closed_at TEXT NOT NULL DEFAULT '',
      status TEXT NOT NULL DEFAULT 'Abierta',
      device TEXT NOT NULL DEFAULT '',
      device_id TEXT NOT NULL DEFAULT '',
      token_hint TEXT NOT NULL DEFAULT '',
      close_reason TEXT NOT NULL DEFAULT '',
      closed_by TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS customers (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      address TEXT NOT NULL DEFAULT '',
      phone TEXT NOT NULL DEFAULT '',
      addresses_json TEXT NOT NULL DEFAULT '[]',
      has_credit INTEGER NOT NULL DEFAULT 0,
      credit_limit REAL NOT NULL DEFAULT 0,
      balance REAL NOT NULL DEFAULT 0,
      loyalty_points REAL NOT NULL DEFAULT 0,
      price_level TEXT NOT NULL DEFAULT 'sale',
      status TEXT NOT NULL DEFAULT 'Activo',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS customer_movements (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      customer_id INTEGER NOT NULL DEFAULT 0,
      date TEXT NOT NULL,
      type TEXT NOT NULL,
      amount REAL NOT NULL DEFAULT 0,
      points REAL NOT NULL DEFAULT 0,
      method TEXT NOT NULL DEFAULT '',
      reference TEXT NOT NULL DEFAULT '',
      notes TEXT NOT NULL DEFAULT '',
      cashier TEXT NOT NULL DEFAULT '',
      client_request_id TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS inventory_adjustments (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      date TEXT NOT NULL,
      sku TEXT NOT NULL,
      product_name TEXT NOT NULL,
      supplier TEXT NOT NULL DEFAULT '',
      type TEXT NOT NULL,
      quantity INTEGER NOT NULL,
      delta INTEGER NOT NULL,
      before_stock INTEGER NOT NULL,
      after_stock INTEGER NOT NULL,
      reference TEXT NOT NULL DEFAULT '',
      reason TEXT NOT NULL DEFAULT '',
      client_request_id TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS purchase_notes (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      mode TEXT NOT NULL,
      source_order_id INTEGER NOT NULL DEFAULT 0,
      supplier TEXT NOT NULL DEFAULT '',
      folio TEXT NOT NULL DEFAULT '',
      date TEXT NOT NULL,
      line_count INTEGER NOT NULL DEFAULT 0,
      total REAL NOT NULL DEFAULT 0,
      notes TEXT NOT NULL DEFAULT '',
      image TEXT NOT NULL DEFAULT '',
      ocr_text TEXT NOT NULL DEFAULT '',
      lines_json TEXT NOT NULL DEFAULT '[]',
      reviewed_by_admin_at TEXT NOT NULL DEFAULT '',
      reviewed_by_admin_user TEXT NOT NULL DEFAULT '',
      client_request_id TEXT NOT NULL DEFAULT '',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS shopping_list (
      sku TEXT PRIMARY KEY,
      quantity INTEGER NOT NULL DEFAULT 1,
      supplier TEXT NOT NULL DEFAULT '',
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS shopping_supplier_notes (
      supplier TEXT PRIMARY KEY,
      note TEXT NOT NULL DEFAULT '',
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS department_inventory (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      department TEXT NOT NULL,
      sku TEXT NOT NULL UNIQUE,
      name TEXT NOT NULL,
      stock REAL NOT NULL DEFAULT 0,
      unit TEXT NOT NULL DEFAULT 'Pieza',
      price REAL NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'Activo',
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS department_labels (
      code TEXT PRIMARY KEY,
      department TEXT NOT NULL,
      sku TEXT NOT NULL,
      product_name TEXT NOT NULL,
      quantity REAL NOT NULL,
      unit TEXT NOT NULL DEFAULT 'Pieza',
      unit_price REAL NOT NULL DEFAULT 0,
      total REAL NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'Pendiente',
      created_at TEXT NOT NULL,
      charged_at TEXT NOT NULL DEFAULT '',
      cashier TEXT NOT NULL DEFAULT '',
      notes TEXT NOT NULL DEFAULT '',
      lines_json TEXT NOT NULL DEFAULT '[]'
    );

    CREATE TABLE IF NOT EXISTS cashier_users (
      name TEXT PRIMARY KEY,
      pin TEXT NOT NULL DEFAULT '',
      role TEXT NOT NULL DEFAULT 'cashier',
      status TEXT NOT NULL DEFAULT 'Activo',
      permissions_json TEXT NOT NULL DEFAULT '[]'
    );

    CREATE TABLE IF NOT EXISTS login_attempts (
      login_name TEXT PRIMARY KEY,
      failed_attempts INTEGER NOT NULL DEFAULT 0,
      locked_until TEXT NOT NULL DEFAULT '',
      last_failed_at TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS cashier_tasks (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      day TEXT NOT NULL DEFAULT 'Diario',
      shift TEXT NOT NULL DEFAULT 'Todos',
      cashier TEXT NOT NULL DEFAULT 'Todos',
      priority TEXT NOT NULL DEFAULT 'Normal',
      category TEXT NOT NULL DEFAULT 'General',
      description TEXT NOT NULL DEFAULT '',
      subtasks_json TEXT NOT NULL DEFAULT '[]',
      photos_json TEXT NOT NULL DEFAULT '[]',
      done INTEGER NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'Activa',
      created_at TEXT NOT NULL DEFAULT '',
      completed_at TEXT NOT NULL DEFAULT ''
    );

    CREATE TABLE IF NOT EXISTS settings (
      key TEXT PRIMARY KEY,
      value_json TEXT NOT NULL
    );

    CREATE TABLE IF NOT EXISTS task_attachments (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      task_id INTEGER NOT NULL,
      file_path TEXT NOT NULL,
      thumb_path TEXT NOT NULL DEFAULT '',
      original_path TEXT NOT NULL DEFAULT '',
      mime_type TEXT NOT NULL DEFAULT 'image/webp',
      size_bytes INTEGER NOT NULL DEFAULT 0,
      thumb_size_bytes INTEGER NOT NULL DEFAULT 0,
      original_size_bytes INTEGER NOT NULL DEFAULT 0,
      original_name TEXT NOT NULL DEFAULT '',
      storage_tier TEXT NOT NULL DEFAULT 'recent',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      archived_at TEXT NOT NULL DEFAULT '',
      FOREIGN KEY(task_id) REFERENCES cashier_tasks(id) ON DELETE CASCADE
    );

    CREATE TABLE IF NOT EXISTS internal_withdrawals (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      session_id TEXT NOT NULL,
      time_text TEXT NOT NULL DEFAULT '',
      concept TEXT NOT NULL DEFAULT 'Retiro',
      amount_mxn REAL NOT NULL,
      original_amount REAL NOT NULL DEFAULT 0,
      currency TEXT NOT NULL DEFAULT 'MXN',
      detail_html TEXT NOT NULL DEFAULT '',
      cashier TEXT NOT NULL DEFAULT '',
      client_request_id TEXT NOT NULL DEFAULT '',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS settings_snapshots (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
      user_name TEXT NOT NULL DEFAULT '',
      reason TEXT NOT NULL DEFAULT '',
      settings_json TEXT NOT NULL DEFAULT '{}'
    );

    CREATE TABLE IF NOT EXISTS print_profiles (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL UNIQUE,
      type TEXT NOT NULL DEFAULT 'general',
      terminal_id TEXT NOT NULL DEFAULT '',
      settings_json TEXT NOT NULL DEFAULT '{}',
      is_default INTEGER NOT NULL DEFAULT 0,
      updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS cut_reports (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      session_id TEXT NOT NULL DEFAULT '',
      client_request_id TEXT NOT NULL DEFAULT '',
      date TEXT NOT NULL,
      cashier TEXT NOT NULL DEFAULT '',
      shift TEXT NOT NULL DEFAULT '',
      difference REAL NOT NULL DEFAULT 0,
      notes TEXT NOT NULL DEFAULT '',
      payload_json TEXT NOT NULL DEFAULT '{}',
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS utility_expenses (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      date TEXT NOT NULL,
      type TEXT NOT NULL DEFAULT 'daily',
      concept TEXT NOT NULL DEFAULT '',
      amount REAL NOT NULL DEFAULT 0,
      active INTEGER NOT NULL DEFAULT 1,
      created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS audit_events (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      date TEXT NOT NULL,
      type TEXT NOT NULL,
      severity TEXT NOT NULL DEFAULT 'Media',
      user_name TEXT NOT NULL DEFAULT '',
      session_id TEXT NOT NULL DEFAULT '',
      reference TEXT NOT NULL DEFAULT '',
      amount REAL NOT NULL DEFAULT 0,
      details TEXT NOT NULL DEFAULT '',
      payload_json TEXT NOT NULL DEFAULT '{}'
    );
  `);
  const saleColumns = db.prepare("PRAGMA table_info(sales)").all().map(row => row.name);
  if(!saleColumns.includes("session_id")) db.exec("ALTER TABLE sales ADD COLUMN session_id TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("status")) db.exec("ALTER TABLE sales ADD COLUMN status TEXT NOT NULL DEFAULT 'Completada'");
  if(!saleColumns.includes("cancel_reason")) db.exec("ALTER TABLE sales ADD COLUMN cancel_reason TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("cancelled_at")) db.exec("ALTER TABLE sales ADD COLUMN cancelled_at TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("cancelled_by")) db.exec("ALTER TABLE sales ADD COLUMN cancelled_by TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("cancelled_session_id")) db.exec("ALTER TABLE sales ADD COLUMN cancelled_session_id TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("customer_id")) db.exec("ALTER TABLE sales ADD COLUMN customer_id INTEGER NOT NULL DEFAULT 0");
  if(!saleColumns.includes("customer_name")) db.exec("ALTER TABLE sales ADD COLUMN customer_name TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("loyalty_phone")) db.exec("ALTER TABLE sales ADD COLUMN loyalty_phone TEXT NOT NULL DEFAULT ''");
  if(!saleColumns.includes("loyalty_points")) db.exec("ALTER TABLE sales ADD COLUMN loyalty_points REAL NOT NULL DEFAULT 0");
  const customerColumns = db.prepare("PRAGMA table_info(customers)").all().map(row => row.name);
  if(!customerColumns.includes("addresses_json")) db.exec("ALTER TABLE customers ADD COLUMN addresses_json TEXT NOT NULL DEFAULT '[]'");
  if(!saleColumns.includes("client_request_id")) db.exec("ALTER TABLE sales ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  const saleItemColumns = db.prepare("PRAGMA table_info(sale_items)").all().map(row => row.name);
  if(!saleItemColumns.includes("cost_price")) db.exec("ALTER TABLE sale_items ADD COLUMN cost_price REAL NOT NULL DEFAULT 0");
  if(!saleItemColumns.includes("bundle_json")) db.exec("ALTER TABLE sale_items ADD COLUMN bundle_json TEXT NOT NULL DEFAULT '[]'");
  db.exec("CREATE TABLE IF NOT EXISTS quotations (id INTEGER PRIMARY KEY AUTOINCREMENT, folio TEXT NOT NULL UNIQUE, date TEXT NOT NULL, expires_at TEXT NOT NULL DEFAULT '', cashier TEXT NOT NULL DEFAULT '', total REAL NOT NULL DEFAULT 0, format TEXT NOT NULL DEFAULT 'ticket', status TEXT NOT NULL DEFAULT 'Activa', customer_id INTEGER NOT NULL DEFAULT 0, customer_name TEXT NOT NULL DEFAULT '', cart_json TEXT NOT NULL DEFAULT '[]', quote_text TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)");
  const quotationColumns = db.prepare("PRAGMA table_info(quotations)").all().map(row => row.name);
  if(!quotationColumns.includes("customer_id")) db.exec("ALTER TABLE quotations ADD COLUMN customer_id INTEGER NOT NULL DEFAULT 0");
  if(!quotationColumns.includes("customer_name")) db.exec("ALTER TABLE quotations ADD COLUMN customer_name TEXT NOT NULL DEFAULT ''");
  const productColumns = db.prepare("PRAGMA table_info(products)").all().map(row => row.name);
  if(!productColumns.includes("product_type")) db.exec("ALTER TABLE products ADD COLUMN product_type TEXT NOT NULL DEFAULT 'sale'");
  if(!productColumns.includes("wholesale_price_2")) db.exec("ALTER TABLE products ADD COLUMN wholesale_price_2 REAL NOT NULL DEFAULT 0");
  if(!productColumns.includes("wholesale_qty_1")) db.exec("ALTER TABLE products ADD COLUMN wholesale_qty_1 INTEGER NOT NULL DEFAULT 0");
  if(!productColumns.includes("wholesale_qty_2")) db.exec("ALTER TABLE products ADD COLUMN wholesale_qty_2 INTEGER NOT NULL DEFAULT 0");
  if(!productColumns.includes("package_pieces")) db.exec("ALTER TABLE products ADD COLUMN package_pieces INTEGER NOT NULL DEFAULT 1");
  if(!productColumns.includes("bundle_json")) db.exec("ALTER TABLE products ADD COLUMN bundle_json TEXT NOT NULL DEFAULT '[]'");
  if(!productColumns.includes("sale_mode")) db.exec("ALTER TABLE products ADD COLUMN sale_mode TEXT NOT NULL DEFAULT 'unit'");
  if(!productColumns.includes("product_status")) db.exec("ALTER TABLE products ADD COLUMN product_status TEXT NOT NULL DEFAULT 'Activo'");
  if(!productColumns.includes("supplier_options_json")) db.exec("ALTER TABLE products ADD COLUMN supplier_options_json TEXT NOT NULL DEFAULT '[]'");
  if(!productColumns.includes("tax_enabled")) db.exec("ALTER TABLE products ADD COLUMN tax_enabled INTEGER NOT NULL DEFAULT 0");
  if(!productColumns.includes("tax_country")) db.exec("ALTER TABLE products ADD COLUMN tax_country TEXT NOT NULL DEFAULT 'MX'");
  if(!productColumns.includes("tax_name")) db.exec("ALTER TABLE products ADD COLUMN tax_name TEXT NOT NULL DEFAULT 'IVA'");
  if(!productColumns.includes("tax_rate")) db.exec("ALTER TABLE products ADD COLUMN tax_rate REAL NOT NULL DEFAULT 16");
  if(!productColumns.includes("inactivity_keep_active_at")) db.exec("ALTER TABLE products ADD COLUMN inactivity_keep_active_at TEXT NOT NULL DEFAULT ''");
  const cashMovementColumns = db.prepare("PRAGMA table_info(cash_movements)").all().map(row => row.name);
  if(!cashMovementColumns.includes("session_id")) db.exec("ALTER TABLE cash_movements ADD COLUMN session_id TEXT NOT NULL DEFAULT ''");
  if(!cashMovementColumns.includes("client_request_id")) db.exec("ALTER TABLE cash_movements ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  if(!cashMovementColumns.includes("review_status")) db.exec("ALTER TABLE cash_movements ADD COLUMN review_status TEXT NOT NULL DEFAULT ''");
  if(!cashMovementColumns.includes("admin_authorized_by")) db.exec("ALTER TABLE cash_movements ADD COLUMN admin_authorized_by TEXT NOT NULL DEFAULT ''");
  const cashSessionColumns = db.prepare("PRAGMA table_info(cash_sessions)").all().map(row => row.name);
  if(!cashSessionColumns.includes("device_id")) db.exec("ALTER TABLE cash_sessions ADD COLUMN device_id TEXT NOT NULL DEFAULT ''");
  if(!cashSessionColumns.includes("close_reason")) db.exec("ALTER TABLE cash_sessions ADD COLUMN close_reason TEXT NOT NULL DEFAULT ''");
  if(!cashSessionColumns.includes("closed_by")) db.exec("ALTER TABLE cash_sessions ADD COLUMN closed_by TEXT NOT NULL DEFAULT ''");
  const purchaseNoteColumns = db.prepare("PRAGMA table_info(purchase_notes)").all().map(row => row.name);
  if(!purchaseNoteColumns.includes("source_order_id")) db.exec("ALTER TABLE purchase_notes ADD COLUMN source_order_id INTEGER NOT NULL DEFAULT 0");
  if(!purchaseNoteColumns.includes("client_request_id")) db.exec("ALTER TABLE purchase_notes ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("reviewed_by_admin_at")) db.exec("ALTER TABLE purchase_notes ADD COLUMN reviewed_by_admin_at TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("reviewed_by_admin_user")) db.exec("ALTER TABLE purchase_notes ADD COLUMN reviewed_by_admin_user TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("status")) db.exec("ALTER TABLE purchase_notes ADD COLUMN status TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("source")) db.exec("ALTER TABLE purchase_notes ADD COLUMN source TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("received_at")) db.exec("ALTER TABLE purchase_notes ADD COLUMN received_at TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("received_by")) db.exec("ALTER TABLE purchase_notes ADD COLUMN received_by TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("review_status")) db.exec("ALTER TABLE purchase_notes ADD COLUMN review_status TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("inventory_status")) db.exec("ALTER TABLE purchase_notes ADD COLUMN inventory_status TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("receipt_folio")) db.exec("ALTER TABLE purchase_notes ADD COLUMN receipt_folio TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("receipt_notes")) db.exec("ALTER TABLE purchase_notes ADD COLUMN receipt_notes TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("receipt_total")) db.exec("ALTER TABLE purchase_notes ADD COLUMN receipt_total REAL NOT NULL DEFAULT 0");
  if(!purchaseNoteColumns.includes("receipt_image")) db.exec("ALTER TABLE purchase_notes ADD COLUMN receipt_image TEXT NOT NULL DEFAULT ''");
  if(!purchaseNoteColumns.includes("receipt_lines_json")) db.exec("ALTER TABLE purchase_notes ADD COLUMN receipt_lines_json TEXT NOT NULL DEFAULT '[]'");
  if(!purchaseNoteColumns.includes("ordered_lines_json")) db.exec("ALTER TABLE purchase_notes ADD COLUMN ordered_lines_json TEXT NOT NULL DEFAULT '[]'");
  if(!purchaseNoteColumns.includes("created_at")) db.exec("ALTER TABLE purchase_notes ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
  db.exec(`
    UPDATE purchase_notes
    SET status = CASE
        WHEN status <> '' THEN status
        WHEN mode = 'pedido_borrador' THEN 'draft'
        WHEN mode IN ('pedido','order') THEN 'ordered'
        WHEN mode = 'pedido_revision' THEN 'receiving'
        WHEN mode = 'pedido_recibido' THEN 'received'
        WHEN mode IN ('manual','image') THEN 'received'
        ELSE 'draft'
      END,
      source = CASE
        WHEN source <> '' THEN source
        WHEN mode IN ('manual','image') AND source_order_id > 0 THEN 'order_receipt'
        WHEN mode = 'manual' THEN 'direct_receipt'
        WHEN mode = 'image' THEN 'receipt_image'
        ELSE 'order'
      END,
      review_status = CASE WHEN review_status <> '' THEN review_status WHEN reviewed_by_admin_at <> '' THEN 'reviewed' ELSE review_status END,
      inventory_status = CASE WHEN inventory_status <> '' THEN inventory_status WHEN mode IN ('manual','pedido_recibido') THEN 'applied' ELSE inventory_status END,
      ordered_lines_json = CASE WHEN ordered_lines_json <> '[]' THEN ordered_lines_json WHEN mode IN ('pedido_borrador','pedido','order','pedido_revision','pedido_recibido') THEN lines_json ELSE ordered_lines_json END,
      receipt_lines_json = CASE WHEN receipt_lines_json <> '[]' THEN receipt_lines_json WHEN mode IN ('manual','image') THEN lines_json ELSE receipt_lines_json END
    WHERE status = '' OR source = '' OR ordered_lines_json = '[]' OR receipt_lines_json = '[]'
  `);
  db.exec(`
    UPDATE purchase_notes
    SET status = CASE
        WHEN mode IN ('pedido','order') THEN 'ordered'
        WHEN mode = 'pedido_revision' THEN 'receiving'
        ELSE status
      END
    WHERE (mode IN ('pedido','order') AND status IN ('', 'draft'))
       OR (mode = 'pedido_revision' AND status IN ('', 'draft', 'ordered'))
  `);
  const customerMovementColumns = db.prepare("PRAGMA table_info(customer_movements)").all().map(row => row.name);
  if(!customerMovementColumns.includes("client_request_id")) db.exec("ALTER TABLE customer_movements ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  const inventoryAdjustmentColumns = db.prepare("PRAGMA table_info(inventory_adjustments)").all().map(row => row.name);
  if(!inventoryAdjustmentColumns.includes("client_request_id")) db.exec("ALTER TABLE inventory_adjustments ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  const cutReportColumns = db.prepare("PRAGMA table_info(cut_reports)").all().map(row => row.name);
  if(!cutReportColumns.includes("session_id")) db.exec("ALTER TABLE cut_reports ADD COLUMN session_id TEXT NOT NULL DEFAULT ''");
  if(!cutReportColumns.includes("client_request_id")) db.exec("ALTER TABLE cut_reports ADD COLUMN client_request_id TEXT NOT NULL DEFAULT ''");
  if(!cutReportColumns.includes("created_at")) {
    db.exec("ALTER TABLE cut_reports ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
    db.prepare("UPDATE cut_reports SET created_at = ? WHERE created_at = '' OR created_at IS NULL").run(nowIso());
  }
  const departmentLabelColumns = db.prepare("PRAGMA table_info(department_labels)").all().map(row => row.name);
  if(!departmentLabelColumns.includes("lines_json")) db.exec("ALTER TABLE department_labels ADD COLUMN lines_json TEXT NOT NULL DEFAULT '[]'");
  const supplierColumns = db.prepare("PRAGMA table_info(suppliers)").all().map(row => row.name);
  if(!supplierColumns.includes("delivery_days")) db.exec("ALTER TABLE suppliers ADD COLUMN delivery_days TEXT NOT NULL DEFAULT ''");
  if(!supplierColumns.includes("visit_type")) db.exec("ALTER TABLE suppliers ADD COLUMN visit_type TEXT NOT NULL DEFAULT 'Preventa'");
  const shoppingColumns = db.prepare("PRAGMA table_info(shopping_list)").all().map(row => row.name);
  if(!shoppingColumns.includes("supplier")) db.exec("ALTER TABLE shopping_list ADD COLUMN supplier TEXT NOT NULL DEFAULT ''");
  const taskColumns = db.prepare("PRAGMA table_info(cashier_tasks)").all().map(row => row.name);
  if(!taskColumns.includes("reset_period")) db.exec("ALTER TABLE cashier_tasks ADD COLUMN reset_period TEXT NOT NULL DEFAULT 'weekly'");
  if(!taskColumns.includes("created_at")) db.exec("ALTER TABLE cashier_tasks ADD COLUMN created_at TEXT NOT NULL DEFAULT ''");
  if(!taskColumns.includes("completed_at")) db.exec("ALTER TABLE cashier_tasks ADD COLUMN completed_at TEXT NOT NULL DEFAULT ''");
  if(!taskColumns.includes("status")) db.exec("ALTER TABLE cashier_tasks ADD COLUMN status TEXT NOT NULL DEFAULT 'Activa'");
  if(!taskColumns.includes("category")) db.exec("ALTER TABLE cashier_tasks ADD COLUMN category TEXT NOT NULL DEFAULT 'General'");
  const taskAttachmentColumns = db.prepare("PRAGMA table_info(task_attachments)").all().map(row => row.name);
  if(!taskAttachmentColumns.includes("original_path")) db.exec("ALTER TABLE task_attachments ADD COLUMN original_path TEXT NOT NULL DEFAULT ''");
  if(!taskAttachmentColumns.includes("original_size_bytes")) db.exec("ALTER TABLE task_attachments ADD COLUMN original_size_bytes INTEGER NOT NULL DEFAULT 0");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_sales_client_request_id ON sales(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_cash_movements_client_request_id ON cash_movements(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_customer_movements_client_request_id ON customer_movements(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_adjustments_client_request_id ON inventory_adjustments(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_notes_client_request_id ON purchase_notes(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_id_desc ON audit_events(id DESC)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_date ON audit_events(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_reference ON audit_events(reference)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_type ON audit_events(type)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_severity ON audit_events(severity)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_sales_date ON sales(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_sales_session_id ON sales(session_id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_sales_status ON sales(status)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_sale_items_sale_id ON sale_items(sale_id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_sale_items_sku ON sale_items(sku)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_cut_reports_date ON cut_reports(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_cut_reports_session ON cut_reports(session_id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_cash_movements_date ON cash_movements(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_cash_movements_session ON cash_movements(session_id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_internal_withdrawals_session ON internal_withdrawals(session_id)");
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_internal_withdrawals_request ON internal_withdrawals(client_request_id) WHERE client_request_id <> ''");
  db.exec("CREATE INDEX IF NOT EXISTS idx_inventory_adjustments_date ON inventory_adjustments(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_inventory_adjustments_sku ON inventory_adjustments(sku)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_date ON purchase_notes(date)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_status ON purchase_notes(status)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_source ON purchase_notes(source)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_supplier ON purchase_notes(supplier)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_received_at ON purchase_notes(received_at)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_review_status ON purchase_notes(review_status)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_purchase_notes_inventory_status ON purchase_notes(inventory_status)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_settings_snapshots_created ON settings_snapshots(created_at)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_task_attachments_task ON task_attachments(task_id, id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_task_attachments_created ON task_attachments(created_at)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_print_profiles_type ON print_profiles(type)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_print_profiles_terminal ON print_profiles(terminal_id)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_products_status ON products(product_status)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_products_supplier ON products(supplier)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone)");
  db.exec("CREATE INDEX IF NOT EXISTS idx_utility_expenses_date ON utility_expenses(date)");
  try {
    db.prepare(`
      SELECT
        json_extract(payload_json, '$.sku') AS sku,
        MAX(COALESCE(json_extract(payload_json, '$.reviewedAt'), date)) AS reviewed_at
      FROM audit_events
      WHERE json_extract(payload_json, '$.action') = 'inactiveProductReview'
        AND COALESCE(json_extract(payload_json, '$.reviewed'), 0) = 1
      GROUP BY sku
    `).all().forEach(row => {
      const sku = cleanText(row.sku).toUpperCase();
      const reviewedAt = cleanText(row.reviewed_at);
      if(!sku || !reviewedAt) return;
      db.prepare(`
        UPDATE products
        SET inactivity_keep_active_at = CASE
          WHEN inactivity_keep_active_at = '' OR inactivity_keep_active_at < ? THEN ?
          ELSE inactivity_keep_active_at
        END
        WHERE sku = ?
      `).run(reviewedAt, reviewedAt, sku);
    });
  } catch(error) {
    console.warn("[T Market] No se pudo migrar auditoria de productos inactivos:", error.message);
  }
}

function scalar(sql, params = []) {
  return db.prepare(sql).get(...params);
}

function seedDatabase() {
  const productCount = scalar("SELECT COUNT(*) AS count FROM products").count;
  const insertSettingIfMissing = db.prepare("INSERT OR IGNORE INTO settings(key, value_json) VALUES (?, ?)");
  Object.entries(defaults.settings).forEach(([key, value]) => insertSettingIfMissing.run(key, jsonString(value)));
  const insertDepartmentInventoryDefault = db.prepare(`
    INSERT OR IGNORE INTO department_inventory(department, sku, name, stock, unit, price, status, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `);
  defaults.departmentInventory.forEach(item => insertDepartmentInventoryDefault.run(
    item.department,
    cleanDepartmentSku(item.sku),
    item.name,
    cleanNumber(item.stock),
    item.unit || "Pieza",
    cleanNumber(item.price),
    item.status || "Activo",
    nowIso()
  ));
  if(productCount > 0) return;

  const insertDepartment = db.prepare("INSERT OR IGNORE INTO departments(name) VALUES (?)");
  const insertSupplier = db.prepare(`
    INSERT OR REPLACE INTO suppliers(name, aliases_json, promoter, visit_days, visit_type, phone, supervisor, supervisor_phone, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  const insertProduct = db.prepare(`
    INSERT OR REPLACE INTO products(sku, name, supplier, category, stock, minimum_stock, cost_price, sale_price, wholesale_price, wholesale_price_2, wholesale_qty_1, wholesale_qty_2, package_pieces, bundle_json, location, photo, aliases_json, supplier_aliases_json, supplier_options_json, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  const insertSetting = db.prepare("INSERT OR REPLACE INTO settings(key, value_json) VALUES (?, ?)");
  const insertCashier = db.prepare("INSERT OR REPLACE INTO cashier_users(name, pin, role, status, permissions_json) VALUES (?, ?, ?, ?, ?)");
  const insertTask = db.prepare("INSERT OR REPLACE INTO cashier_tasks(id, title, day, shift, cashier, priority, description, subtasks_json, photos_json, done, status, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");

  db.exec("BEGIN IMMEDIATE");
  try {
    defaults.departments.forEach(department => insertDepartment.run(department));
    defaults.suppliers.forEach(supplier => insertSupplier.run(
      supplier.name,
      jsonString(supplier.aliases || []),
      supplier.promoter || "",
      supplier.visitDays || "",
      supplier.visitType || "Preventa",
      supplier.phone || "",
      supplier.supervisor || "",
      supplier.supervisorPhone || "",
      nowIso()
    ));
    defaults.products.forEach(product => insertProduct.run(
      product.sku,
      product.name,
      product.supplier,
      product.category,
      cleanInteger(product.stock),
      cleanInteger(product.minimum),
      cleanNumber(product.costPrice),
      cleanNumber(product.salePrice),
      cleanNumber(product.wholesalePrice || product.salePrice),
      cleanNumber(product.wholesalePrice2),
      cleanInteger(product.wholesaleQty1),
      cleanInteger(product.wholesaleQty2),
      Math.max(1, cleanInteger(product.packagePieces, 1)),
      jsonString(product.bundleItems || []),
      product.location || "",
      product.photo || "",
      jsonString(product.aliases || []),
      jsonString(product.supplierAliases || {}),
      jsonString(product.supplierOptions || []),
      nowIso()
    ));
    Object.entries(defaults.settings).forEach(([key, value]) => insertSetting.run(key, jsonString(value)));
    defaults.cashierUsers.forEach(user => insertCashier.run(user.name, hashPin(user.pin), user.role, user.status, jsonString(user.permissions || [])));
    defaults.cashierTasks.forEach(task => insertTask.run(task.id, task.title, task.day, task.shift, task.cashier, task.priority, task.description, jsonString(task.subtasks || []), jsonString(task.photos || []), task.done ? 1 : 0, task.status || "Activa", task.createdAt || new Date().toLocaleDateString("es-MX"), task.completedAt || ""));
    db.exec("COMMIT");
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function hardenDefaultDemoCashier() {
  try {
    const row = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = 'cajero 1'").get();
    if(!row || !verifyPin("1234", row.pin)) return;
    const replacementPin = String(crypto.randomInt(100000, 999999));
    db.prepare("UPDATE cashier_users SET pin = ?, status = 'Inactivo' WHERE name = ?")
      .run(hashPin(replacementPin), row.name);
    logAuditEvent({
      type: "Cajero demo desactivado",
      severity: "Media",
      reference: row.name,
      details: "Se desactivo automaticamente el usuario demo Cajero 1 porque conservaba el PIN debil 1234.",
      payload: { user: row.name, reason: "default-demo-pin" }
    });
  } catch(error) {
    console.warn("[T Market] No se pudo endurecer el cajero demo:", error.message);
  }
}

function rowToProduct(row) {
  return {
    name: row.name,
    sku: row.sku,
    supplier: row.supplier,
    category: row.category,
    stock: row.stock,
    minimum: row.minimum_stock,
    costPrice: row.cost_price,
    salePrice: row.sale_price,
    wholesalePrice: row.wholesale_price,
    wholesalePrice2: row.wholesale_price_2 || 0,
    wholesaleQty1: row.wholesale_qty_1 || 0,
    wholesaleQty2: row.wholesale_qty_2 || 0,
    packagePieces: row.package_pieces || 1,
    bundleItems: jsonParse(row.bundle_json, []),
    location: row.location,
    photo: row.photo ? `/product-photo/${encodeURIComponent(row.sku)}?v=${encodeURIComponent(row.updated_at || "")}` : "",
    aliases: jsonParse(row.aliases_json, []),
    supplierAliases: jsonParse(row.supplier_aliases_json, {}),
    supplierOptions: jsonParse(row.supplier_options_json, []),
    productType: row.product_type || "sale",
    saleMode: row.sale_mode || "unit",
    status: row.product_status || "Activo",
    inactivityKeepActiveAt: row.inactivity_keep_active_at || "",
    taxEnabled: Boolean(row.tax_enabled),
    taxCountry: row.tax_country || "MX",
    taxName: row.tax_name || "IVA",
    taxRate: row.tax_rate ?? 16,
    updatedAt: row.updated_at || ""
  };
}

function rowToUtilityExpense(row) {
  return {
    id: row.id,
    date: row.date,
    type: row.type || "daily",
    concept: row.concept || "",
    amount: row.amount,
    active: Boolean(row.active),
    createdAt: row.created_at || ""
  };
}

function rowToQuotation(row) {
  return {
    id: row.id,
    folio: row.folio,
    date: row.date,
    expiresAt: row.expires_at || "",
    cashier: row.cashier || "",
    total: row.total,
    format: row.format || "ticket",
    status: row.status || "Activa",
    customerId: row.customer_id || 0,
    customerName: row.customer_name || "",
    cart: jsonParse(row.cart_json, []),
    quoteText: row.quote_text || "",
    createdAt: row.created_at || ""
  };
}

function rowToAuditEvent(row) {
  return {
    id: row.id,
    date: row.date,
    type: row.type,
    severity: row.severity,
    user: row.user_name,
    sessionId: row.session_id,
    reference: row.reference,
    amount: row.amount,
    details: row.details,
    payload: jsonParse(row.payload_json, {})
  };
}

function rowToCustomer(row) {
  return {
    id: row.id,
    name: row.name || "",
    address: row.address || "",
    phone: row.phone || "",
    addresses: jsonParse(row.addresses_json, []),
    hasCredit: Boolean(row.has_credit),
    creditLimit: row.credit_limit || 0,
    balance: row.balance || 0,
    loyaltyPoints: row.loyalty_points || 0,
    priceLevel: row.price_level || "sale",
    status: row.status || "Activo",
    createdAt: row.created_at || "",
    updatedAt: row.updated_at || ""
  };
}

function rowToCustomerMovement(row) {
  return {
    id: row.id,
    customerId: row.customer_id || 0,
    date: row.date || "",
    type: row.type || "",
    amount: row.amount || 0,
    points: row.points || 0,
    method: row.method || "",
    reference: row.reference || "",
    notes: row.notes || "",
    cashier: row.cashier || "",
    clientRequestId: row.client_request_id || ""
  };
}

function rowToDepartmentInventory(row) {
  return {
    id: row.id,
    department: row.department,
    sku: row.sku,
    name: row.name,
    stock: row.stock,
    unit: row.unit,
    price: row.price,
    status: row.status
  };
}

function rowToDepartmentLabel(row) {
  return {
    code: row.code,
    department: row.department,
    sku: row.sku,
    productName: row.product_name,
    quantity: row.quantity,
    unit: row.unit,
    unitPrice: row.unit_price,
    total: row.total,
    status: row.status,
    createdAt: row.created_at,
    chargedAt: row.charged_at,
    cashier: row.cashier,
    notes: row.notes,
    lines: jsonParse(row.lines_json, [])
  };
}

function rowToSupplier(row) {
  return {
    name: row.name,
    aliases: jsonParse(row.aliases_json, []),
    promoter: row.promoter,
    visitDays: row.visit_days,
    deliveryDays: row.delivery_days || "",
    visitType: row.visit_type || "Preventa",
    phone: row.phone,
    supervisor: row.supervisor,
    supervisorPhone: row.supervisor_phone
  };
}

function rowToCashMovement(row) {
  return {
    id: row.id,
    sessionId: row.session_id || "",
    type: row.type,
    amount: row.amount,
    method: row.method,
    concept: row.concept,
    reference: row.reference,
    cashier: row.cashier,
    date: row.date,
    clientRequestId: row.client_request_id || "",
    reviewStatus: row.review_status || "",
    adminAuthorizedBy: row.admin_authorized_by || ""
  };
}

function rowToInventoryAdjustment(row) {
  return {
    id: row.id,
    date: row.date,
    sku: row.sku,
    productName: row.product_name,
    supplier: row.supplier,
    type: row.type,
    quantity: row.quantity,
    delta: row.delta,
    beforeStock: row.before_stock,
    afterStock: row.after_stock,
    reference: row.reference,
    reason: row.reason,
    clientRequestId: row.client_request_id || ""
  };
}

function rowToSaleReport(row) {
  const paymentsData = jsonParse(row.payments_json, {});
  const paymentLabels = [
    cleanNumber(paymentsData.cash) > 0 ? "Efectivo" : "",
    cleanNumber(paymentsData.card) > 0 ? "Tarjeta" : "",
    cleanNumber(paymentsData.transfer) > 0 ? "Transferencia" : "",
    cleanNumber(paymentsData.usd) > 0 ? "Dolares" : ""
  ].filter(Boolean).join(", ");
  return {
    id: row.id,
    sessionId: row.session_id || "",
    date: row.date,
    folio: row.folio,
    total: row.total,
    paid: row.paid,
    change: row.change_amount,
    cashier: row.cashier,
    items: row.items || 0,
    payments: paymentLabels,
    paymentDetails: paymentsData,
    deliveryOrder: paymentsData.deliveryOrder || null,
    ticket: row.ticket,
    status: row.status || "Completada",
    cancelReason: row.cancel_reason || "",
    cancelledAt: row.cancelled_at || "",
    cancelledBy: row.cancelled_by || "",
    cancelSessionId: row.cancelled_session_id || "",
    customerId: row.customer_id || 0,
    customerName: row.customer_name || "",
    loyaltyPhone: row.loyalty_phone || "",
    loyaltyPoints: row.loyalty_points || 0,
    lines: []
  };
}

function getPurchaseDocumentStatus(row) {
  const stored = cleanText(row?.status);
  if(stored) return stored;
  const mode = cleanText(row?.mode);
  if(mode === "pedido_borrador") return "draft";
  if(mode === "pedido" || mode === "order") return "ordered";
  if(mode === "pedido_revision") return "receiving";
  if(mode === "pedido_recibido" || mode === "manual" || mode === "image") return "received";
  return "draft";
}

function getPurchaseDocumentSource(row) {
  const stored = cleanText(row?.source);
  if(stored) return stored;
  const mode = cleanText(row?.mode);
  if((mode === "manual" || mode === "image") && cleanInteger(row?.source_order_id) > 0) return "order_receipt";
  if(mode === "manual") return "direct_receipt";
  if(mode === "image") return "receipt_image";
  return "order";
}

function getReceivedByFromSession(sessionContext) {
  return cleanText(sessionContext?.user?.name || sessionContext?.user?.username || sessionContext?.cashier || "");
}

function rowToPurchaseNote(row) {
  return {
    id: row.id,
    mode: row.mode,
    status: getPurchaseDocumentStatus(row),
    source: getPurchaseDocumentSource(row),
    sourceOrderId: row.source_order_id || 0,
    supplier: row.supplier,
    folio: row.folio,
    date: row.date,
    lines: row.line_count,
    total: row.total,
    notes: row.notes,
    image: row.image,
    ocrText: row.ocr_text,
    lineItems: jsonParse(row.lines_json, []),
    orderedLineItems: jsonParse(row.ordered_lines_json, []),
    receiptLineItems: jsonParse(row.receipt_lines_json, []),
    receivedAt: row.received_at || "",
    receivedBy: row.received_by || "",
    reviewStatus: row.review_status || (row.reviewed_by_admin_at ? "reviewed" : ""),
    inventoryStatus: row.inventory_status || "",
    receiptFolio: row.receipt_folio || "",
    receiptNotes: row.receipt_notes || "",
    receiptTotal: row.receipt_total || 0,
    receiptImage: row.receipt_image || "",
    createdAt: row.created_at || "",
    reviewedByAdminAt: row.reviewed_by_admin_at || "",
    reviewedByAdminUser: row.reviewed_by_admin_user || "",
    clientRequestId: row.client_request_id || ""
  };
}

function taskMediaRelativePath(absolutePath) {
  return path.relative(DATA_DIR, absolutePath).replace(/\\/g, "/");
}

function taskMediaAbsolutePath(relativePath) {
  const resolved = path.resolve(DATA_DIR, cleanText(relativePath));
  if(resolved !== TASK_MEDIA_DIR && !resolved.startsWith(`${path.resolve(TASK_MEDIA_DIR)}${path.sep}`)) {
    throw httpError(400, "Ruta de evidencia de tarea invalida.");
  }
  return resolved;
}

function parseTaskPhotoDataUrl(value) {
  const match = String(value || "").match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([\s\S]+)$/);
  if(!match) return null;
  try {
    return { mimeType: match[1].toLowerCase(), buffer: Buffer.from(match[2], "base64") };
  } catch {
    return null;
  }
}

function taskMediaExtension(mimeType) {
  if(mimeType === "image/png") return ".png";
  if(mimeType === "image/gif") return ".gif";
  if(mimeType === "image/webp") return ".webp";
  return ".jpg";
}

function saveTaskMediaBuffer(taskId, dataUrl, variant = "evidence", originalName = "") {
  const parsed = parseTaskPhotoDataUrl(dataUrl);
  if(!parsed || !parsed.buffer.length) return null;
  const now = new Date();
  const folder = path.join(TASK_MEDIA_DIR, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0"), `tarea-${taskId}`);
  fs.mkdirSync(folder, { recursive: true });
  const fileName = `${variant}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${taskMediaExtension(parsed.mimeType)}`;
  const target = path.join(folder, fileName);
  fs.writeFileSync(target, parsed.buffer);
  const stat = fs.statSync(target);
  if(stat.size !== parsed.buffer.length) {
    try { fs.unlinkSync(target); } catch {}
    throw httpError(500, "No se pudo verificar la evidencia guardada.");
  }
  return { absolutePath: target, relativePath: taskMediaRelativePath(target), mimeType: parsed.mimeType, sizeBytes: stat.size, originalName: cleanText(originalName) };
}

function listTaskAttachments(taskId, includeData = false) {
  return db.prepare("SELECT * FROM task_attachments WHERE task_id = ? ORDER BY id").all(cleanInteger(taskId)).map(row => {
    const attachment = {
      id: row.id,
      taskId: row.task_id,
      mimeType: row.mime_type,
      sizeBytes: row.size_bytes,
      thumbSizeBytes: row.thumb_size_bytes,
      originalSizeBytes: row.original_size_bytes || 0,
      originalName: row.original_name || "",
      storageTier: row.storage_tier || "recent",
      createdAt: row.created_at || ""
    };
    if(includeData) {
      const fullPath = taskMediaAbsolutePath(row.file_path);
      const thumbPath = row.thumb_path ? taskMediaAbsolutePath(row.thumb_path) : fullPath;
      if(fs.existsSync(fullPath)) attachment.dataUrl = `data:${row.mime_type};base64,${fs.readFileSync(fullPath).toString("base64")}`;
      if(fs.existsSync(thumbPath)) attachment.thumbDataUrl = `data:${row.mime_type};base64,${fs.readFileSync(thumbPath).toString("base64")}`;
    }
    return attachment;
  });
}

function saveTaskAttachment(taskId, photo) {
  const dataUrl = typeof photo === "string" ? photo : String(photo?.dataUrl || photo?.src || photo?.url || "");
  const thumbDataUrl = typeof photo === "object" ? String(photo?.thumbDataUrl || photo?.thumbnail || "") : "";
  const evidence = saveTaskMediaBuffer(taskId, dataUrl, "evidence", photo?.name || photo?.originalName || "");
  if(!evidence) return null;
  let thumb = null;
  let original = null;
  try {
    thumb = thumbDataUrl ? saveTaskMediaBuffer(taskId, thumbDataUrl, "thumb", photo?.name || "") : null;
    original = typeof photo === "object" && photo?.originalDataUrl
      ? saveTaskMediaBuffer(taskId, String(photo.originalDataUrl), "original", photo?.name || "")
      : null;
    const result = db.prepare(`
      INSERT INTO task_attachments(task_id, file_path, thumb_path, original_path, mime_type, size_bytes, thumb_size_bytes, original_size_bytes, original_name, storage_tier, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'recent', ?)
    `).run(taskId, evidence.relativePath, thumb?.relativePath || evidence.relativePath, original?.relativePath || "", evidence.mimeType, evidence.sizeBytes, thumb?.sizeBytes || evidence.sizeBytes, original?.sizeBytes || 0, evidence.originalName, nowIso());
    return Number(result.lastInsertRowid);
  } catch(error) {
    try { fs.unlinkSync(evidence.absolutePath); } catch {}
    if(thumb?.absolutePath && thumb.absolutePath !== evidence.absolutePath) try { fs.unlinkSync(thumb.absolutePath); } catch {}
    if(original?.absolutePath) try { fs.unlinkSync(original.absolutePath); } catch {}
    throw error;
  }
}

function syncTaskAttachments(taskId, photos = []) {
  if(!Array.isArray(photos)) return;
  photos.forEach(photo => {
    if(photo && typeof photo === "object" && cleanInteger(photo.id)) return;
    saveTaskAttachment(taskId, photo);
  });
}

function removeTaskAttachmentFiles(attachmentId) {
  const row = db.prepare("SELECT * FROM task_attachments WHERE id = ?").get(cleanInteger(attachmentId));
  if(!row) return;
  for(const relativePath of new Set([row.file_path, row.thumb_path, row.original_path].filter(Boolean))) {
    try {
      const absolutePath = taskMediaAbsolutePath(relativePath);
      if(fs.existsSync(absolutePath)) fs.unlinkSync(absolutePath);
    } catch {}
  }
  db.prepare("DELETE FROM task_attachments WHERE id = ?").run(row.id);
}

function migrateLegacyTaskMedia() {
  const rows = db.prepare("SELECT id, photos_json FROM cashier_tasks WHERE photos_json <> '[]' AND photos_json <> '' ORDER BY id").all();
  let index = 0;
  const next = () => {
    if(index >= rows.length) {
      if(rows.length) console.log(`[T Market] Migracion de evidencias terminada: ${rows.length} tarea(s).`);
      return;
    }
    const row = rows[index++];
    const photos = jsonParse(row.photos_json, []);
    const inserted = [];
    try {
      photos.forEach(photo => {
        const id = saveTaskAttachment(row.id, photo);
        if(id) inserted.push(id);
      });
      if(inserted.length === photos.length) {
        db.prepare("UPDATE cashier_tasks SET photos_json = '[]' WHERE id = ?").run(row.id);
      } else {
        inserted.forEach(removeTaskAttachmentFiles);
      }
    } catch(error) {
      inserted.forEach(removeTaskAttachmentFiles);
      console.warn(`[T Market] No se pudo migrar evidencia de tarea ${row.id}:`, error.message);
    }
    setTimeout(next, 40);
  };
  next();
}

function runTaskMediaRetention() {
  const settings = getSettings();
  const retentionDays = { "3months": 90, "6months": 180, "1year": 365, always: Infinity }[settings.taskEvidenceRetention] ?? 365;
  const deleteEnabled = Boolean(settings.taskEvidenceDeleteEnabled) && Number.isFinite(retentionDays);
  const originalPolicy = ["deleteAfterOptimize", "archive15", "always"].includes(settings.taskEvidenceOriginalPolicy)
    ? settings.taskEvidenceOriginalPolicy
    : "deleteAfterOptimize";
  const now = Date.now();
  let deleted = 0;
  let originalsRemoved = 0;
  db.prepare("SELECT * FROM task_attachments ORDER BY id").all().forEach(row => {
    const created = Date.parse(row.created_at || "") || now;
    const ageDays = Math.max(0, Math.floor((now - created) / 86400000));
    if(deleteEnabled && ageDays > retentionDays) {
      removeTaskAttachmentFiles(row.id);
      deleted += 1;
      return;
    }
    const tier = ageDays <= 15 ? "recent" : ageDays <= 30 ? "warm" : ageDays <= 180 ? "compressed" : "archived";
    if(tier !== row.storage_tier) db.prepare("UPDATE task_attachments SET storage_tier = ?, archived_at = ? WHERE id = ?").run(tier, tier === "archived" ? nowIso() : "", row.id);
    const removeOriginal = row.original_path && (originalPolicy === "deleteAfterOptimize" || (originalPolicy === "archive15" && ageDays > 15));
    if(removeOriginal) {
      try {
        const originalPath = taskMediaAbsolutePath(row.original_path);
        if(fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
      } catch {}
      db.prepare("UPDATE task_attachments SET original_path = '', original_size_bytes = 0 WHERE id = ?").run(row.id);
      originalsRemoved += 1;
    }
  });
  if(deleted || originalsRemoved) {
    logAuditEvent({
      type: "Mantenimiento de evidencias",
      severity: "Baja",
      reference: new Date().toLocaleDateString("es-MX"),
      details: `Evidencias eliminadas: ${deleted}. Originales optimizados retirados: ${originalsRemoved}.`,
      payload: { deleted, originalsRemoved, retention: settings.taskEvidenceRetention, originalPolicy }
    });
  }
  return { deleted, originalsRemoved };
}

function scheduleTaskMediaMaintenance() {
  setTimeout(() => migrateLegacyTaskMedia(), 5000);
  setTimeout(() => runTaskMediaRetention(), 60000);
  const timer = setInterval(() => runTaskMediaRetention(), 6 * 60 * 60 * 1000);
  if(typeof timer.unref === "function") timer.unref();
}

function rowToTask(row, includeDetail = false) {
  const legacyPhotos = jsonParse(row.photos_json, []);
  const attachments = includeDetail ? listTaskAttachments(row.id, true) : [];
  const task = {
    id: row.id,
    title: row.title,
    day: row.day,
    shift: row.shift,
    cashier: row.cashier,
    priority: row.priority,
    category: row.category || "General",
    description: includeDetail ? row.description : "",
    subtasks: jsonParse(row.subtasks_json, []),
    photos: includeDetail ? [...attachments, ...legacyPhotos.map((dataUrl, index) => ({ id: `legacy-${index + 1}`, dataUrl, thumbDataUrl: dataUrl, legacy: true }))] : [],
    photoCount: cleanInteger(row.attachment_count) + legacyPhotos.length,
    done: Boolean(row.done),
    status: row.status || "Activa",
    resetPeriod: row.reset_period || "weekly",
    createdAt: row.created_at,
    completedAt: row.completed_at
  };
  return task;
}

function rowToCutReport(row) {
  const payload = jsonParse(row.payload_json, {});
  return {
    id: row.id,
    sessionId: row.session_id || "",
    clientRequestId: row.client_request_id || payload.clientRequestId || payload.client_request_id || "",
    date: row.date,
    cashier: row.cashier,
    shift: row.shift,
    difference: row.difference,
    notes: row.notes,
    createdAt: row.created_at || payload.createdAt || "",
    payload
  };
}

function rowToCashSession(row) {
  return {
    id: row.id,
    userName: row.user_name,
    openedAt: row.opened_at,
    closedAt: row.closed_at,
    status: row.status,
    device: row.device || "",
    deviceId: row.device_id || "",
    closeReason: row.close_reason || "",
    closedBy: row.closed_by || ""
  };
}

function getUserPermissions(user) {
  const direct = Array.isArray(user.permissions) ? user.permissions : [];
  return direct.length ? direct : [...(rolePermissions[user.role] || [])];
}

function publicUser(row) {
  const user = {
    name: row.name,
    role: row.role,
    status: row.status,
    permissions: jsonParse(row.permissions_json, [])
  };
  user.permissions = getUserPermissions(user);
  return user;
}

function isInitialAdminPinPending() {
  const row = db.prepare("SELECT pin FROM cashier_users WHERE LOWER(name) = 'administrador' AND role = 'admin' AND status = 'Activo' LIMIT 1").get();
  return Boolean(row && verifyPin("0000", row.pin));
}

function validateSecurePin(pin) {
  const cleanPin = cleanText(pin);
  if(!/^\d{4,8}$/.test(cleanPin)) throw httpError(400, "El nuevo PIN debe tener de 4 a 8 numeros.");
  const weakPins = new Set(["0000", "1111", "2222", "3333", "4444", "5555", "6666", "7777", "8888", "9999", "1234", "4321", "1212", "2468", "9876"]);
  if(weakPins.has(cleanPin)) throw httpError(400, "Ese PIN es demasiado facil. Usa uno diferente.");
  return cleanPin;
}

function setupInitialAdmin(input = {}) {
  if(!isInitialAdminPinPending()) throw httpError(409, "El administrador inicial ya fue configurado.");
  const newPin = validateSecurePin(input.newPin || input.pin);
  const adminName = cleanText(input.name || "Administrador") || "Administrador";
  if(adminName.length > 40) throw httpError(400, "El nombre del administrador es demasiado largo.");
  db.prepare("UPDATE cashier_users SET name = ?, pin = ? WHERE LOWER(name) = 'administrador' AND role = 'admin'").run(adminName, hashPin(newPin));
  logAuditEvent({
    type: "Administrador configurado",
    severity: "Alta",
    reference: adminName,
    details: "Se cambio el PIN inicial inseguro del administrador.",
    payload: { name: adminName }
  });
  return { ok: true, message: "Administrador configurado. Ya puedes iniciar sesion con tu nuevo PIN." };
}

function getSessionUser(req) {
  return getSessionContext(req).user;
}

function getSessionContext(req) {
  const header = req.headers.authorization || "";
  const bearer = header.startsWith("Bearer ") ? header.slice(7).trim() : "";
  const token = bearer || req.headers["x-tmarket-token"];
  if(!token || !sessions.has(token)) throw httpError(401, "Inicia sesion para continuar.");
  return sessions.get(token);
}

function makeCashSessionId() {
  return `CS-${Date.now().toString(36).toUpperCase()}-${crypto.randomBytes(3).toString("hex").toUpperCase()}`;
}


function isMobileAppDevice(input = {}) {
  const device = cleanText(input.device || input.terminal || input.deviceName || input.source || input.app || "").toLowerCase();
  return /android|tmarket_android|app movil|app móvil|mobile app|movil|móvil/.test(device);
}

function buildMobileAuthSession(user, input = {}) {
  const openedAt = new Date().toLocaleString("es-MX");
  return {
    id: "",
    userName: user.name,
    openedAt,
    closedAt: "",
    status: "App movil - sin caja propia",
    device: cleanText(input.device || input.terminal || "Android"),
    deviceId: normalizeDeviceId(input),
    closeReason: "",
    closedBy: ""
  };
}

function normalizeDeviceId(input = {}) {
  return cleanText(input.deviceId || input.device_id || input.clientDeviceId || input.client_device_id || input.terminalId || input.terminal_id || "");
}

function getDeviceLabel(input = {}) {
  return cleanText(input.device || input.terminal || input.deviceName || input.source || "PC");
}

function findActivePcCashSessionForUserDevice(userName, deviceId = "", excludeSessionId = "") {
  const cleanDeviceId = normalizeDeviceId({ deviceId });
  if(cleanDeviceId) {
    return db.prepare(`
      SELECT *
      FROM cash_sessions
      WHERE LOWER(user_name) = LOWER(?)
        AND status = 'Abierta'
        AND id <> ?
        AND device_id = ?
        AND COALESCE(device, '') NOT LIKE 'Android%'
        AND LOWER(COALESCE(device, '')) NOT LIKE '%android%'
        AND LOWER(COALESCE(device, '')) NOT LIKE '%movil%'
        AND LOWER(COALESCE(device, '')) NOT LIKE '%móvil%'
        AND LOWER(COALESCE(device, '')) NOT LIKE '%mobile%'
      ORDER BY rowid DESC
      LIMIT 1
    `).get(userName, excludeSessionId, cleanDeviceId);
  }
  return db.prepare(`
    SELECT *
    FROM cash_sessions
    WHERE LOWER(user_name) = LOWER(?)
      AND status = 'Abierta'
      AND id <> ?
      AND COALESCE(device, '') NOT LIKE 'Android%'
      AND LOWER(COALESCE(device, '')) NOT LIKE '%android%'
      AND LOWER(COALESCE(device, '')) NOT LIKE '%movil%'
      AND LOWER(COALESCE(device, '')) NOT LIKE '%móvil%'
      AND LOWER(COALESCE(device, '')) NOT LIKE '%mobile%'
    ORDER BY rowid DESC
    LIMIT 1
  `).get(userName, excludeSessionId);
}

function findActivePcCashSessionForUser(userName, excludeSessionId = "") {
  return findActivePcCashSessionForUserDevice(userName, "", excludeSessionId);
}

function findActiveMobileAuthSession(userName, deviceId = "") {
  const cleanUserName = cleanText(userName).toLowerCase();
  const cleanDeviceId = normalizeDeviceId({ deviceId });
  if(!cleanUserName) return null;
  for(const [token, sessionContext] of sessions.entries()) {
    if(!sessionContext?.mobileApp) continue;
    if(cleanText(sessionContext.user?.name).toLowerCase() !== cleanUserName) continue;
    const sessionDeviceId = normalizeDeviceId(sessionContext.cashSession || {});
    if(cleanDeviceId && sessionDeviceId && sessionDeviceId !== cleanDeviceId) continue;
    return { token, sessionContext };
  }
  return null;
}

function closeMobileAuthSession(token, options = {}) {
  if(!token || !sessions.has(token)) return null;
  const sessionContext = sessions.get(token);
  sessions.delete(token);
  const closedAt = new Date().toLocaleString("es-MX");
  if(sessionContext?.cashSession) {
    sessionContext.cashSession.closedAt = closedAt;
    sessionContext.cashSession.status = cleanText(options.status || "App movil cerrada");
    sessionContext.cashSession.closeReason = cleanText(options.reason || "Sesion movil cerrada");
    sessionContext.cashSession.closedBy = cleanText(options.closedBy || sessionContext.user?.name || "");
  }
  return sessionContext;
}

function createCashSession(user, token, input = {}) {
  const openedAt = new Date().toLocaleString("es-MX");
  const cashSession = {
    id: makeCashSessionId(),
    userName: user.name,
    openedAt,
    closedAt: "",
    status: "Abierta",
    device: getDeviceLabel(input),
    deviceId: normalizeDeviceId(input),
    closeReason: "",
    closedBy: ""
  };
  db.prepare(`
    INSERT INTO cash_sessions(id, user_name, opened_at, closed_at, status, device, device_id, token_hint, close_reason, closed_by)
    VALUES (?, ?, ?, '', 'Abierta', ?, ?, ?, '', '')
  `).run(cashSession.id, cashSession.userName, cashSession.openedAt, cashSession.device, cashSession.deviceId, token.slice(0, 8));
  return cashSession;
}

function getCashSessionActivitySummary(sessionId) {
  const cleanSessionId = cleanText(sessionId);
  if(!cleanSessionId) {
    return { salesCount: 0, salesTotal: 0, movementsCount: 0, movementsTotal: 0, cutCount: 0, hasActivity: false, hasCut: false };
  }
  const sales = db.prepare("SELECT COUNT(*) AS count, COALESCE(SUM(total), 0) AS total FROM sales WHERE session_id = ? AND status <> 'Cancelada'").get(cleanSessionId) || {};
  const movements = db.prepare("SELECT COUNT(*) AS count, COALESCE(SUM(amount), 0) AS total FROM cash_movements WHERE session_id = ?").get(cleanSessionId) || {};
  const cuts = db.prepare("SELECT COUNT(*) AS count FROM cut_reports WHERE session_id = ?").get(cleanSessionId) || {};
  const salesCount = Number(sales.count) || 0;
  const movementsCount = Number(movements.count) || 0;
  const cutCount = Number(cuts.count) || 0;
  return {
    salesCount,
    salesTotal: cleanNumber(sales.total),
    movementsCount,
    movementsTotal: cleanNumber(movements.total),
    cutCount,
    hasActivity: salesCount > 0 || movementsCount > 0,
    hasCut: cutCount > 0
  };
}

function getCashSessionActivitySummaryFromRow(row) {
  return getCashSessionActivitySummary(row?.id || "");
}

function canForceCloseSessions(user, adminPin = "") {
  if(userHasPermission(user, "forceCloseSessions")) return true;
  const adminUser = validateAdminPin(adminPin);
  return Boolean(adminUser && userHasPermission(adminUser, "forceCloseSessions"));
}

function closeCashSession(session, options = {}) {
  if(!session?.cashSession?.id) return;
  const closedAt = new Date().toLocaleString("es-MX");
  const status = cleanText(options.status || "Cerrada");
  const reason = cleanText(options.reason || "");
  const closedBy = cleanText(options.closedBy || session.user?.name || "");
  db.prepare("UPDATE cash_sessions SET closed_at = ?, status = ?, close_reason = ?, closed_by = ? WHERE id = ? AND status = 'Abierta'")
    .run(closedAt, status, reason, closedBy, session.cashSession.id);
  session.cashSession.closedAt = closedAt;
  session.cashSession.status = status;
  session.cashSession.closeReason = reason;
  session.cashSession.closedBy = closedBy;
}

function closeCashSessionById(sessionId, options = {}) {
  const cleanSessionId = cleanText(sessionId);
  if(!cleanSessionId) throw httpError(400, "Sesion requerida.");
  const row = db.prepare("SELECT * FROM cash_sessions WHERE id = ?").get(cleanSessionId);
  if(!row) throw httpError(404, "Sesion no encontrada.");
  if(row.status !== "Abierta") return rowToCashSession(row);
  const closedAt = new Date().toLocaleString("es-MX");
  const status = cleanText(options.status || "Cerrada sin corte");
  const reason = cleanText(options.reason || "Cierre forzado desde administracion");
  const closedBy = cleanText(options.closedBy || "Administrador");
  db.prepare("UPDATE cash_sessions SET closed_at = ?, status = ?, close_reason = ?, closed_by = ? WHERE id = ? AND status = 'Abierta'")
    .run(closedAt, status, reason, closedBy, cleanSessionId);
  for(const [token, value] of sessions.entries()) {
    if(value?.cashSession?.id === cleanSessionId) sessions.delete(token);
  }
  const updated = db.prepare("SELECT * FROM cash_sessions WHERE id = ?").get(cleanSessionId);
  logAuditEvent({
    type: "Cierre forzado de sesion",
    severity: "Alta",
    user: closedBy,
    sessionId: cleanSessionId,
    reference: cleanSessionId,
    details: reason,
    payload: { status, reason, closedBy, activity: getCashSessionActivitySummary(cleanSessionId) }
  });
  return rowToCashSession(updated);
}

function listCashSessionsForMonitor() {
  return db.prepare("SELECT * FROM cash_sessions ORDER BY rowid DESC LIMIT 300").all().map(row => ({
    ...rowToCashSession(row),
    activity: getCashSessionActivitySummary(row.id)
  }));
}

function getAuditEventPayload(input = {}) {
  return input.payload && typeof input.payload === "object" ? input.payload : {};
}

function getAuditEventDedupeSku(input = {}) {
  const payload = getAuditEventPayload(input);
  return cleanText(payload.sku || payload.productSku || payload.product_sku || input.reference).split("|")[0].trim().toUpperCase();
}

function getAuditEventDedupeKey(input = {}) {
  const payload = getAuditEventPayload(input);
  const sku = getAuditEventDedupeSku(input);
  const type = cleanText(input.type, "Movimiento sensible").toLowerCase();
  const action = cleanText(payload.action || payload.type).toLowerCase();
  const delta = payload.delta !== undefined ? String(payload.delta) : "";
  const ref = cleanText(input.reference).toLowerCase();
  if(sku && (type.includes("ajuste") || type.includes("inventario") || type.includes("producto"))) {
    return `product:${sku}:${action}:${delta}:${type.includes("ajuste sensible") ? "ajuste" : type}`;
  }
  return `${type}|${ref}|${cleanText(input.details).slice(0, 80).toLowerCase()}`;
}

function isRedundantProductModificationServerAudit(input = {}) {
  const text = `${input.type || ""} ${input.details || ""}`.toLowerCase();
  const sku = getAuditEventDedupeSku(input);
  if(!sku) return false;
  const looksLikeProductModified = text.includes("producto modificado") || text.includes("producto actualizado") || text.includes("producto que se modificó") || text.includes("producto que se modifico");
  if(!looksLikeProductModified) return false;
  const recent = db.prepare(`
    SELECT type, reference, details, payload_json
    FROM audit_events
    WHERE reference = ?
    ORDER BY id DESC
    LIMIT 12
  `).all(sku);
  return recent.some(row => {
    const rowText = `${row.type || ""} ${row.details || ""}`.toLowerCase();
    return rowText.includes("ajuste sensible") || rowText.includes("ajuste de inventario") || rowText.includes("movimiento de inventario");
  });
}

function logAuditEvent(input = {}, sessionContext = null) {
  if(isRedundantProductModificationServerAudit(input)) return null;
  const type = cleanText(input.type, "Movimiento sensible");
  const userName = cleanText(sessionContext?.user?.name || input.user || input.userName, "Sistema");
  const sessionId = cleanText(sessionContext?.cashSession?.id || input.sessionId);
  const reference = cleanText(input.reference);
  const details = cleanText(input.details);
  const payload = input.payload || {};
  const dedupeKey = getAuditEventDedupeKey({ ...input, reference, details, payload });
  const recentRows = db.prepare(`
    SELECT id, type, reference, details, payload_json
    FROM audit_events
    WHERE reference = ?
    ORDER BY id DESC
    LIMIT 15
  `).all(reference);
  const duplicate = recentRows.find(row => getAuditEventDedupeKey({
    type: row.type,
    reference: row.reference,
    details: row.details,
    payload: jsonParse(row.payload_json, {})
  }) === dedupeKey);
  if(duplicate) return duplicate.id;
  db.prepare(`
    INSERT INTO audit_events(date, type, severity, user_name, session_id, reference, amount, details, payload_json)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    cleanText(input.date, new Date().toLocaleString("es-MX")),
    type,
    cleanText(input.severity, "Media"),
    userName,
    sessionId,
    reference,
    cleanNumber(input.amount),
    details,
    jsonString(payload)
  );
  return scalar("SELECT last_insert_rowid() AS id").id;
}

function updateAuditEventReviewStatus(input = {}, sessionContext = null) {
  requirePermission(sessionContext?.user, "admin");
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "Alerta requerida.");
  const row = db.prepare("SELECT * FROM audit_events WHERE id = ?").get(id);
  if(!row) throw httpError(404, "Alerta no encontrada.");
  const payload = jsonParse(row.payload_json, {});
  const reviewed = Boolean(input.reviewed);
  payload.reviewed = reviewed;
  payload.reviewedAt = reviewed ? nowIso() : "";
  payload.reviewedBy = reviewed ? cleanText(sessionContext?.user?.name, "Sistema") : "";
  payload.reviewNote = reviewed ? cleanText(input.note) : "";
  if(cleanText(payload.action) === "cashMovementReview") {
    payload.reviewStatus = reviewed ? "reviewed" : "pending_review";
    payload.adminAuthorizedBy = reviewed ? cleanText(sessionContext?.user?.name, "Administrador") : "";
  }
  db.prepare("UPDATE audit_events SET payload_json = ? WHERE id = ?").run(jsonString(payload), id);
  if(cleanText(payload.action) === "cashMovementReview" && cleanInteger(payload.movementId)) {
    db.prepare(`
      UPDATE cash_movements
      SET review_status = ?, admin_authorized_by = ?
      WHERE id = ?
    `).run(
      reviewed ? "reviewed" : "pending_review",
      reviewed ? cleanText(sessionContext?.user?.name, "Administrador") : "",
      cleanInteger(payload.movementId)
    );
  }
  if(["purchaseReceiptReview", "purchaseReceiptDifferences"].includes(cleanText(payload.action)) && cleanInteger(payload.purchaseNoteId)) {
    if(reviewed) {
      markPurchaseNoteAdminReview({ id: cleanInteger(payload.purchaseNoteId) }, sessionContext);
    } else {
      db.prepare("UPDATE purchase_notes SET review_status = ? WHERE id = ?").run("pending_review", cleanInteger(payload.purchaseNoteId));
    }
  }
  if(reviewed && cleanText(payload.action) === "inactiveProductReview") {
    const sku = cleanText(payload.sku).toUpperCase();
    if(sku) {
      db.prepare(`
        UPDATE products
        SET inactivity_keep_active_at = ?, updated_at = updated_at
        WHERE sku = ?
      `).run(payload.reviewedAt, sku);
    }
  }
  return rowToAuditEvent(db.prepare("SELECT * FROM audit_events WHERE id = ?").get(id));
}


function buildAuditStatusCondition(status, where) {
  if(status === "pending") {
    where.push("(COALESCE(json_extract(payload_json, '$.reviewed'), 0) = 0)");
  } else if(status === "reviewed") {
    where.push("(COALESCE(json_extract(payload_json, '$.reviewed'), 0) = 1)");
  }
}

function getAuditExactDateLikePatterns(value) {
  const text = cleanText(value);
  if(!text) return [];
  const patterns = new Set([`${text}%`, `%${text}%`]);
  const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if(iso) {
    const year = iso[1];
    const month = Number(iso[2]);
    const day = Number(iso[3]);
    [
      `${day}/${month}/${year}`,
      `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}/${year}`,
      `${day}/${String(month).padStart(2, "0")}/${year}`,
      `${String(day).padStart(2, "0")}/${month}/${year}`
    ].forEach(dateText => {
      patterns.add(`${dateText}%`);
      patterns.add(`%${dateText}%`);
    });
  }
  return [...patterns];
}

function buildAuditQueryWhere(params = {}) {
  const where = [];
  const values = [];
  const exact = cleanText(params.exact || params.date);
  const from = cleanText(params.from);
  const to = cleanText(params.to);
  const q = cleanText(params.q || params.search).toLowerCase();
  const status = cleanText(params.status, "pending");

  if(exact) {
    const exactPatterns = getAuditExactDateLikePatterns(exact);
    where.push(`(${exactPatterns.map(() => "date LIKE ?").join(" OR ")})`);
    values.push(...exactPatterns);
  } else {
    if(from) {
      where.push("date >= ?");
      values.push(from);
    }
    if(to) {
      where.push("date <= ?");
      values.push(`${to} 23:59:59`);
    }
  }
  buildAuditStatusCondition(status, where);
  where.push(`NOT (
    COALESCE(json_extract(payload_json, '$.reviewed'), 0) = 0
    AND json_extract(payload_json, '$.action') = 'inactiveProductReview'
    AND UPPER(COALESCE(json_extract(payload_json, '$.sku'), reference, '')) IN (
      SELECT UPPER(sku)
      FROM products
      WHERE LOWER(COALESCE(product_status, '')) IN ('pausado', 'paused')
         OR LOWER(COALESCE(category, '')) = 'productos en pausa'
    )
  )`);
  if(q) {
    where.push("(LOWER(type) LIKE ? OR LOWER(reference) LIKE ? OR LOWER(details) LIKE ? OR LOWER(user_name) LIKE ?)");
    const like = `%${q}%`;
    values.push(like, like, like, like);
  }
  return {
    sql: where.length ? `WHERE ${where.join(" AND ")}` : "",
    values
  };
}

function isAuditAmountMonetary(row = {}) {
  const type = cleanText(row.type).toLowerCase();
  const payload = jsonParse(row.payload_json || row.payloadJson || row.payload || "{}", {});
  const action = cleanText(payload.action || payload.type || payload.source).toLowerCase();
  if(!cleanNumber(row.amount)) return false;
  if(type.includes("respaldo")) return false;
  if(type.includes("ajuste sensible de inventario")) return false;
  if(type.includes("inventario negativo")) return false;
  if(type.includes("cambios de precio")) return false;
  if(action === "addstock" || action === "negativestockcorrected") return false;
  return true;
}

function summarizeAuditAmounts(rows = []) {
  return rows.reduce((summary, row) => {
    const amount = cleanNumber(row.amount);
    if(!amount) return summary;
    if(!isAuditAmountMonetary(row)) {
      summary.nonMonetary += 1;
      summary.nonMonetaryAmount += amount;
      return summary;
    }
    if(Math.abs(amount) > AUDIT_MONETARY_AMOUNT_LIMIT) {
      summary.outliers += 1;
      summary.outlierAmount += amount;
      return summary;
    }
    summary.amount += amount;
    return summary;
  }, { amount: 0, outliers: 0, outlierAmount: 0, nonMonetary: 0, nonMonetaryAmount: 0 });
}

function queryAuditEvents(params = {}) {
  const limit = Math.max(20, Math.min(300, cleanInteger(params.limit, 80)));
  const offset = Math.max(0, cleanInteger(params.offset, 0));
  const query = buildAuditQueryWhere(params);
  const sortKey = cleanText(params.sort, "date").toLowerCase();
  const sortDirection = cleanText(params.direction, "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
  const sortExpressions = {
    date: "id",
    type: "type COLLATE NOCASE",
    severity: "CASE severity WHEN 'Critica' THEN 3 WHEN 'Alta' THEN 2 WHEN 'Media' THEN 1 ELSE 0 END",
    status: "COALESCE(json_extract(payload_json, '$.reviewed'), 0)",
    user: "user_name COLLATE NOCASE",
    reference: "reference COLLATE NOCASE",
    amount: "amount",
    details: "details COLLATE NOCASE"
  };
  const orderBy = sortExpressions[sortKey] || sortExpressions.date;
  const rows = db.prepare(`
    SELECT *
    FROM audit_events
    ${query.sql}
    ORDER BY ${orderBy} ${sortDirection}, id DESC
    LIMIT ? OFFSET ?
  `).all(...query.values, limit, offset).map(rowToAuditEvent);
  const totalRow = db.prepare(`
    SELECT COUNT(*) AS total,
      COALESCE(SUM(CASE WHEN severity = 'Alta' THEN 1 ELSE 0 END), 0) AS high,
      COALESCE(SUM(CASE WHEN severity = 'Critica' THEN 1 ELSE 0 END), 0) AS critical,
      COALESCE(SUM(CASE WHEN severity = 'Media' THEN 1 ELSE 0 END), 0) AS medium
    FROM audit_events
    ${query.sql}
  `).get(...query.values);
  const pendingQuery = buildAuditQueryWhere({ ...params, status: "pending" });
  const pending = db.prepare(`
    SELECT COUNT(*) AS pending
    FROM audit_events
    ${pendingQuery.sql}
  `).get(...pendingQuery.values).pending || 0;
  const total = cleanInteger(totalRow.total);
  return {
    ok: true,
    partial: true,
    auditEvents: rows,
    metrics: {
      total,
      pending: cleanInteger(pending),
      high: cleanInteger(totalRow.high),
      critical: cleanInteger(totalRow.critical),
      medium: cleanInteger(totalRow.medium),
      limit,
      offset,
      hasMore: offset + rows.length < total
    }
  };
}


function requirePermission(user, permission) {
  if(!permission) return;
  if(!user) throw httpError(401, "Sesion requerida.");
  const permissions = getUserPermissions(user);
  const inventorySubPermissions = new Set(["inventoryCatalog", "inventoryAdjustments", "purchaseNotes", "purchaseOrders", "shoppingList", "promotions"]);
  if(user.role === "admin" || permissions.includes(permission) || (inventorySubPermissions.has(permission) && permissions.includes("inventory"))) return;
  throw httpError(403, `Permiso requerido: ${permission}`);
}

function userHasPermission(user, permission) {
  if(!permission || !user) return false;
  if(user.role === "admin") return true;
  return getUserPermissions(user).includes(permission);
}

function userHasAnyPermission(user, permissions = []) {
  return (Array.isArray(permissions) ? permissions : [permissions]).some(permission => userHasPermission(user, permission));
}

function permissionForRoute(route) {
  const map = {
    "/products": "inventoryCatalog",
    "/products/status": "inventoryCatalog",
    "/products/bulk": "inventoryCatalog",
    "/products/delete": "inventoryCatalog",
    "/catalog/departments": "manageCatalogs",
    "/catalog/departments/update": "manageCatalogs",
    "/catalog/departments/delete": "manageCatalogs",
    "/catalog/suppliers": "manageCatalogs",
    "/catalog/sku-ambiguities/scan": "inventoryCatalog",
    "/inventory-adjustments": "inventoryAdjustments",
    "/mobile/inventory-adjustment": "inventoryAdjustments",
    "/department-inventory": "departmentLabels",
    "/department-labels": "departmentLabels",
    "/cash-movements": "pos",
    "/internal-withdrawals": "withdrawals",
    "/sales": "pos",
    "/sales/delivery-status": "pos",
    "/quotations": "pos",
    "/quotations/delete": "pos",
    "/quotations/status": "pos",
    "/sales/cancel": "pos",
    "/purchase-notes": null,
    "/purchase-notes/review": "admin",
    "/purchase-notes/unreview": "admin",
    "/purchase-orders/update": "purchaseOrders",
    "/purchase-orders/receive": null,
    "/purchase-orders/delete": "purchaseOrders",
    "/shopping-list": "shoppingList",
    "/cashiers": "admin",
    "/cashiers/delete": "admin",
    "/cash-sessions/active": "admin",
    "/cash-sessions/force-close": "forceCloseSessions",
    "/customers": "customers",
    "/customers/delete": "customers",
    "/customers/payment": "customers",
    "/tasks": "admin",
    "/tasks/delete": "admin",
    "/tasks/archive": "admin",
    "/audit-expense-reconciliation/run": "admin",
    "/tasks/progress": null,
    "/settings": "admin",
    "/settings/export": "admin",
    "/settings/import": "admin",
    "/settings/snapshot": "admin",
    "/settings/restore": "admin",
    "/print-profiles": "admin",
    "/print-profiles/apply": "admin",
    "/utility-expenses": "admin",
    "/utility-expenses/delete": "admin",
    "/cuts": "caja",
    "/cuts/preview": "caja",
    "/backup": "caja",
    "/backups": "admin",
    "/database/reset": "admin",
    "/database/restore": "admin",
    "/database/restore-upload": "admin",
    "/license": "admin",
    "/license/validate-online": "admin",
    "/audit-events": "admin",
    "/admin/validate-pin": "caja",
    "/cuts/adjust": "admin"
  };
  return map[route];
}

function loginAttemptKey(name) {
  return cleanText(name).toLowerCase();
}

function clearLoginFailures(name) {
  const key = loginAttemptKey(name);
  if(!key) return;
  db.prepare("DELETE FROM login_attempts WHERE login_name = ?").run(key);
}

function assertLoginNotLocked(name) {
  const key = loginAttemptKey(name);
  if(!key) return;
  const row = db.prepare("SELECT * FROM login_attempts WHERE login_name = ?").get(key);
  const lockedUntil = row?.locked_until ? Date.parse(row.locked_until) : NaN;
  if(Number.isFinite(lockedUntil) && lockedUntil > Date.now()) {
    const minutes = Math.max(1, Math.ceil((lockedUntil - Date.now()) / 60000));
    const error = httpError(423, `Usuario bloqueado temporalmente. Intenta de nuevo en ${minutes} min.`);
    error.details = {
      locked: true,
      lockedUntil: row.locked_until,
      remainingMinutes: minutes,
      remainingAttempts: 0
    };
    throw error;
  }
  if(row && row.locked_until) clearLoginFailures(name);
}

function recordLoginFailure(name) {
  const key = loginAttemptKey(name);
  if(!key) throw httpError(401, "Usuario o PIN incorrecto.");
  const row = db.prepare("SELECT * FROM login_attempts WHERE login_name = ?").get(key);
  const failedAttempts = cleanInteger(row?.failed_attempts) + 1;
  const remainingAttempts = Math.max(0, LOGIN_MAX_FAILED_ATTEMPTS - failedAttempts);
  const now = nowIso();

  if(failedAttempts >= LOGIN_MAX_FAILED_ATTEMPTS) {
    const lockedUntil = new Date(Date.now() + LOGIN_LOCK_MINUTES * 60000).toISOString();
    db.prepare(`
      INSERT INTO login_attempts(login_name, failed_attempts, locked_until, last_failed_at)
      VALUES (?, ?, ?, ?)
      ON CONFLICT(login_name) DO UPDATE SET
        failed_attempts = excluded.failed_attempts,
        locked_until = excluded.locked_until,
        last_failed_at = excluded.last_failed_at
    `).run(key, failedAttempts, lockedUntil, now);
    const error = httpError(423, `Demasiados intentos incorrectos. Usuario bloqueado por ${LOGIN_LOCK_MINUTES} min.`);
    error.details = { locked: true, lockedUntil, remainingAttempts: 0, failedAttempts };
    throw error;
  }

  db.prepare(`
    INSERT INTO login_attempts(login_name, failed_attempts, locked_until, last_failed_at)
    VALUES (?, ?, '', ?)
    ON CONFLICT(login_name) DO UPDATE SET
      failed_attempts = excluded.failed_attempts,
      locked_until = '',
      last_failed_at = excluded.last_failed_at
  `).run(key, failedAttempts, now);

  const shouldWarn = remainingAttempts <= LOGIN_WARNING_REMAINING_ATTEMPTS;
  const message = shouldWarn
    ? `Usuario o PIN incorrecto. Quedan ${remainingAttempts} intento${remainingAttempts === 1 ? "" : "s"} antes del bloqueo.`
    : "Usuario o PIN incorrecto.";
  const error = httpError(401, message);
  error.details = { remainingAttempts, failedAttempts, locked: false };
  throw error;
}

function loginUser(input) {
  const name = cleanText(input.name || input.user || input.username);
  const pin = cleanText(input.pin || input.password);
  if(!name || !pin) throw httpError(400, "Usuario y PIN requeridos.");
  assertLoginNotLocked(name);
  const row = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?)").get(name);
  if(row && row.role === "admin" && verifyPin("0000", row.pin)) {
    throw httpError(409, "Por seguridad, configura un PIN nuevo para el Administrador antes de usar el sistema.");
  }
  if(!row || !verifyPin(pin, row.pin)) recordLoginFailure(name);
  if(row.status !== "Activo") throw httpError(403, "Usuario inactivo.");
  migrateCashierPinIfNeeded(row, pin);
  clearLoginFailures(row.name);
  const user = publicUser(row);
  const token = crypto.randomBytes(24).toString("hex");
  if(isMobileAppDevice(input)) {
    const activeMobileSession = findActiveMobileAuthSession(user.name, normalizeDeviceId(input));
    if(activeMobileSession) {
      const action = cleanText(input.action || input.mobileSessionAction || input.sessionAction).toLowerCase();
      if(action === "resume" || action === "reanudar") {
        closeMobileAuthSession(activeMobileSession.token, {
          status: "App movil reanudada",
          reason: "Sesion movil anterior reemplazada al reanudar en Android",
          closedBy: user.name
        });
      } else if(action === "terminate" || action === "end" || action === "close" || action === "cerrar" || action === "terminar") {
        closeMobileAuthSession(activeMobileSession.token, {
          status: "App movil terminada",
          reason: "Sesion movil anterior terminada desde Android",
          closedBy: user.name
        });
      } else {
        const error = httpError(409, `Ya hay una sesion Android abierta para ${user.name}. Puedes reanudarla o terminarla para iniciar de nuevo.`);
        error.details = {
          requiresMobileSessionChoice: true,
          canResume: true,
          canTerminate: true,
          activeSession: activeMobileSession.sessionContext.cashSession,
          user
        };
        throw error;
      }
    }
    const cashSession = buildMobileAuthSession(user, input);
    sessions.set(token, { user, cashSession, mobileApp: true });
    return { token, user, cashSession, serverBootId: SERVER_BOOT_ID, customerDisplayEnabled: getSettings().customerDisplayEnabled !== false };
  }

  const activeSession = findActivePcCashSessionForUserDevice(user.name, normalizeDeviceId(input));
  if(activeSession) {
    const activity = getCashSessionActivitySummary(activeSession.id);
    if(input.resumeSession === true || input.action === "resume") {
      const cashSession = rowToCashSession(activeSession);
      sessions.set(token, { user, cashSession, mobileApp: false, resumed: true });
      return { token, user, cashSession, resumed: true, activeSession: cashSession, activity, serverBootId: SERVER_BOOT_ID, customerDisplayEnabled: getSettings().customerDisplayEnabled !== false };
    }
    if(input.forceNewSession === true || input.action === "forceNew") {
      if(!canForceCloseSessions(user, input.adminPin || input.authorizationPin || "")) {
        throw httpError(403, "Solo administrador o usuario autorizado puede cerrar una sesion abierta para iniciar una nueva.");
      }
      closeCashSessionById(activeSession.id, {
        status: activity.hasActivity && !activity.hasCut ? "Cerrada sin corte" : "Cerrada",
        reason: "Cerrada para iniciar nueva sesion en el mismo dispositivo",
        closedBy: user.name
      });
    } else {
      const error = httpError(409, `Ya existe una sesion abierta para ${user.name} en este dispositivo. Reanuda esa sesion o pide a un administrador forzar el cierre antes de abrir una nueva.`);
      error.details = { requiresSessionChoice: true, activeSession: rowToCashSession(activeSession), activity };
      throw error;
    }
  }

  const cashSession = createCashSession(user, token, input);
  sessions.set(token, { user, cashSession, mobileApp: false });
  return { token, user, cashSession, serverBootId: SERVER_BOOT_ID, customerDisplayEnabled: getSettings().customerDisplayEnabled !== false };
}


function validateCurrentSessionPin(sessionContext, pin) {
  const cleanPin = cleanText(pin);
  const userName = cleanText(sessionContext?.user?.name);
  const attemptKey = `${SESSION_AUTH_ATTEMPT_PREFIX}${userName}`;
  if(!userName || !cleanPin) throw httpError(400, "PIN requerido para reanudar la sesión.");
  assertLoginNotLocked(attemptKey);
  const row = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?)").get(userName);
  if(!row || !verifyPin(cleanPin, row.pin) || row.status !== "Activo") recordLoginFailure(attemptKey);
  if(!row || !verifyPin(cleanPin, row.pin) || row.status !== "Activo") throw httpError(401, "PIN incorrecto. No se reanudó la sesión.");
  migrateCashierPinIfNeeded(row, cleanPin);
  clearLoginFailures(attemptKey);
  return publicUser(row);
}

function validateAdminPin(pin) {
  const cleanPin = cleanText(pin);
  if(!cleanPin) return null;
  assertLoginNotLocked(ADMIN_AUTH_ATTEMPT_KEY);
  try {
    const settings = getSettings();
    const tempPin = cleanText(settings.tempAdminPin);
    const expiresAt = cleanText(settings.tempAdminPinExpiresAt);
    if(tempPin && cleanPin === tempPin && expiresAt) {
      const expiresMs = Date.parse(expiresAt);
      if(Number.isFinite(expiresMs) && expiresMs > Date.now()) {
        clearLoginFailures(ADMIN_AUTH_ATTEMPT_KEY);
        return {
          name: "PIN temporal administrador",
          role: "admin",
          status: "Activo",
          permissions: ["admin"],
          temporary: true
        };
      }
    }
  } catch(error) {
    console.warn("No se pudo validar PIN temporal; se usará PIN admin normal.", error?.message || error);
  }
  const rows = db.prepare("SELECT * FROM cashier_users WHERE role = 'admin' AND status = 'Activo' ORDER BY name").all();
  const row = rows.find(candidate => verifyPin(cleanPin, candidate.pin));
  if(row) {
    migrateCashierPinIfNeeded(row, cleanPin);
    clearLoginFailures(ADMIN_AUTH_ATTEMPT_KEY);
  }
  if(!row) recordLoginFailure(ADMIN_AUTH_ATTEMPT_KEY);
  return row ? publicUser(row) : null;
}

function validateUserPinForPermission(name, pin, permission) {
  const cleanName = cleanText(name);
  const cleanPin = cleanText(pin);
  if(!cleanName || !cleanPin) return null;
  const row = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?) AND status = 'Activo'").get(cleanName);
  if(!row || !verifyPin(cleanPin, row.pin)) return null;
  migrateCashierPinIfNeeded(row, cleanPin);
  const user = publicUser(row);
  return userHasPermission(user, permission) ? user : null;
}

function validateUserPinForAnyPermission(name, pin, permissions = []) {
  const cleanName = cleanText(name);
  const cleanPin = cleanText(pin);
  const requestedPermissions = (Array.isArray(permissions) ? permissions : [permissions]).map(cleanText).filter(Boolean);
  if(!cleanName || !cleanPin || !requestedPermissions.length) return null;
  const row = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?) AND status = 'Activo'").get(cleanName);
  if(!row || !verifyPin(cleanPin, row.pin)) return null;
  migrateCashierPinIfNeeded(row, cleanPin);
  const user = publicUser(row);
  return userCanAccessAny(user, requestedPermissions) ? user : null;
}

function authorizeSaleHistoryAction(sessionContext, input = {}, permission) {
  if(userHasPermission(sessionContext?.user, permission)) {
    return { ok: true, user: sessionContext.user, method: "usuario activo" };
  }
  const authName = cleanText(input.authorizationUser || input.authorizedUser || input.approvedBy || input.userAuthorization || input.authUser);
  const authPin = cleanText(input.authorizationPin || input.authorizedPin || input.authPin || input.adminPin || input.pinAuthorization);
  const authorizedUser = validateUserPinForPermission(authName, authPin, permission);
  if(authorizedUser) return { ok: true, user: authorizedUser, method: "credencial autorizada" };

  const adminUser = validateAdminPin(authPin);
  if(adminUser && userHasPermission(adminUser, permission)) return { ok: true, user: adminUser, method: "PIN administrador" };

  throw httpError(403, permission === "returnSales"
    ? "Usuario sin permiso para devolver productos. Ingresa credenciales de usuario autorizado."
    : "Usuario sin permiso para cancelar tickets. Ingresa credenciales de usuario autorizado.");
}



function saleHasRegisteredCut(sale) {
  const sessionId = cleanText(sale?.session_id || sale?.sessionId);
  if(!sessionId) return false;
  const row = db.prepare("SELECT id FROM cut_reports WHERE session_id = ? LIMIT 1").get(sessionId);
  return Boolean(row);
}

function normalizeRefundMethod(method = "") {
  const clean = cleanText(method || "cash").toLowerCase();
  if(["cash", "efectivo"].includes(clean)) return "cash";
  if(["card", "tarjeta"].includes(clean)) return "card";
  if(["transfer", "transferencia"].includes(clean)) return "transfer";
  if(["points", "puntos"].includes(clean)) return "points";
  if(["credit", "credito", "crédito", "saldo"].includes(clean)) return "credit";
  if(["none", "sin", "sin_devolucion", "sin devolución"].includes(clean)) return "none";
  return "cash";
}

function applyRefundToPaymentBreakdown(payments, refundMethod, refundAmount, exchangeRate) {
  const next = { ...(payments || {}) };
  const amount = cleanMoney(refundAmount);
  if(amount <= 0 || refundMethod === "none") return next;
  const reduce = key => {
    const current = cleanMoney(next[key]);
    const applied = Math.min(current, amount);
    next[key] = cleanMoney(current - applied);
    return applied;
  };
  if(refundMethod === "cash") {
    let remaining = amount - reduce("cash");
    if(remaining > 0) {
      const usdRate = cleanNumber(exchangeRate || next.exchangeRate, 0);
      if(usdRate > 0) {
        const currentUsd = cleanNumber(next.usd);
        const usdToReduce = Math.min(currentUsd, cleanMoney(remaining / usdRate));
        next.usd = cleanMoney(currentUsd - usdToReduce);
      }
    }
  } else if(refundMethod === "card") reduce("card");
  else if(refundMethod === "transfer") reduce("transfer");
  else if(refundMethod === "points") reduce("points");
  else if(refundMethod === "credit") reduce("credit");
  return next;
}

function getSettings() {
  const settings = { ...defaults.settings };
  db.prepare("SELECT key, value_json FROM settings").all().forEach(row => {
    settings[row.key] = jsonParse(row.value_json, settings[row.key]);
  });
  return settings;
}

function getShoppingListNotes() {
  const notes = {};
  db.prepare("SELECT supplier, note FROM shopping_supplier_notes ORDER BY supplier").all().forEach(row => {
    if(row.note) notes[row.supplier] = row.note;
  });
  return notes;
}

function cleanupExpiredQuotations() {
  const today = new Date().toISOString().slice(0, 10);
  db.prepare("DELETE FROM quotations WHERE expires_at <> '' AND expires_at < ?").run(today);
}

function userCanAccessAny(user, permissions = []) {
  return permissions.some(permission => userHasPermission(user, permission));
}

function sameCashierName(a = "", b = "") {
  return cleanText(a).toLowerCase() === cleanText(b).toLowerCase();
}

function belongsToCurrentUserOrSession(item, sessionContext) {
  if(!item || !sessionContext) return false;
  const sessionId = sessionContext.cashSession?.id || "";
  const userName = sessionContext.user?.name || "";
  return Boolean(
    (sessionId && item.sessionId === sessionId) ||
    (userName && sameCashierName(item.cashier || item.userName || "", userName))
  );
}

function sanitizeLicenseForState(license, isAdmin) {
  if(isAdmin) return license;
  return {
    ok: license.ok,
    status: license.status,
    message: license.message,
    licenseType: license.licenseType,
    posPackage: license.posPackage,
    appPackage: license.appPackage,
    expiresAt: license.expiresAt,
    daysRemaining: license.daysRemaining
  };
}

function sanitizeSettingsForState(settings, isAdmin) {
  if(isAdmin) return settings;
  const allowed = new Set([
    "storeName", "branchName", "defaultExchangeRate", "printerSize", "directPrintEnabled", "directPrintCopies", "appTheme", "ticketFooter",
    "taskEvidenceRetention", "taskEvidenceDeleteEnabled", "taskEvidenceOriginalPolicy",
    "autoPrintTicket", "showLogoTicket", "showLogoQuote", "ticketDescriptionMode",
    "ticketShowSku", "ticketShowUnitPrice", "ticketShowChange", "ticketShowSavings",
    "ticketFontFamily", "ticketFontSize", "ticketFontWeight", "ticketLineHeight",
    "ticketTextAlign", "ticketMarginX", "quotePrintFormat", "purchasePrintFormat",
    "quoteTextAlign", "quoteMarginMm", "letterLogoWidthMm", "letterLogoAlign",
    "letterLogoOffsetX", "letterLogoOffsetY", "customLogo", "ticketHeaderLines",
    "ticketFooterLines", "ticketTopSpacing", "ticketBottomSpacing", "quoteHeaderLines",
    "quoteFooterLines", "quoteTopSpacing", "quoteBottomSpacing", "letterHeaderHtml",
    "letterFooterHtml", "saleLetterHeaderHtml", "saleLetterFooterHtml",
    "quoteLetterHeaderHtml", "quoteLetterFooterHtml", "purchaseLetterHeaderHtml",
    "purchaseLetterFooterHtml", "scannerMode", "cashDrawer", "scaleDevice",
    "pinpadDevice", "customerCreditEnabled", "loyaltyEnabled", "loyaltyProgramName",
    "loyaltyPointValue", "loyaltyEarnMode", "loyaltyAmountThreshold", "loyaltyPointsPerThreshold", "loyaltyEarnPercent", "loyaltyAskPhoneOnSale", "customerPromptTiming",
    "taxesEnabled", "taxPriceMode", "ivaEnabled", "ivaRate", "iepsEnabled",
    "iepsRate", "deliveryEnabled", "deliveryPartialCreditEnabled",
    "deliveryPricePerKm", "deliveryFreeFrom", "allowNegativeStockSales",
    "negativeStockSaleMode", "negativeStockNoticeAt", "requireWithdrawalAuth",
    "cashierCashIncomeEnabled", "cashierCashPaymentEnabled", "cashPaymentAuthMode",
    "cashPaymentPendingReview", "withdrawalLimit", "requireCashIncomeCount",
    "requireCashPaymentCount", "printCashIncomeTicket",
    "printCashPaymentTicket", "printInternalWithdrawalTicket", "cashTicketShowCashierSignature",
    "cashTicketShowReceivedSignature", "cashTicketPrintOriginalCopy", "purchaseNotesUsersCanCreateOrders",
    "purchaseNotesUsersCanReceiveOrders", "purchaseReceiptsPendingReview", "purchaseInventoryApplyMode",
    "purchaseCostChangeRequiresAdmin", "promotions", "customerDisplayEnabled",
    "customerDisplayTitle", "customerDisplayPlaylistText", "customerDisplayDefaultDuration"
  ]);
  return Object.fromEntries(Object.entries(settings || {}).filter(([key]) => allowed.has(key)));
}

function sanitizeCustomerForState(customer, canSeeAccount) {
  const basic = {
    id: customer.id,
    name: customer.name,
    address: customer.address,
    phone: customer.phone,
    addresses: customer.addresses,
    hasCredit: customer.hasCredit,
    loyaltyPoints: customer.loyaltyPoints,
    priceLevel: customer.priceLevel,
    status: customer.status
  };
  if(canSeeAccount) {
    basic.creditLimit = customer.creditLimit;
    basic.balance = customer.balance;
    basic.createdAt = customer.createdAt;
    basic.updatedAt = customer.updatedAt;
  }
  return basic;
}

function canSeeProductCostData(user) {
  return userCanAccessAny(user, [
    "admin",
    "inventory",
    "inventoryCatalog",
    "inventoryAdjustments",
    "purchaseNotes",
    "purchaseOrders",
    "shoppingList"
  ]);
}

function sanitizeBundleItemsForState(bundleItems = [], canSeeCost = false) {
  if(!Array.isArray(bundleItems)) return [];
  return bundleItems.map(item => {
    if(canSeeCost) return item;
    const { costPrice, cost, supplier, supplierOptions, supplierAliases, ...safeItem } = item || {};
    return safeItem;
  });
}

function sanitizeProductForState(product, canSeeCost = false) {
  if(canSeeCost) return product;
  const { costPrice, supplier, supplierOptions, supplierAliases, ...safeProduct } = product || {};
  safeProduct.bundleItems = sanitizeBundleItemsForState(product?.bundleItems || [], false);
  return safeProduct;
}

function sanitizeSaleLineForState(line, canSeeCost = false) {
  if(canSeeCost) return line;
  const { costPrice, cost, supplier, supplierOptions, supplierAliases, ...safeLine } = line || {};
  safeLine.bundleItems = sanitizeBundleItemsForState(line?.bundleItems || [], false);
  return safeLine;
}

function sanitizeSaleReportForState(report, canSeeCost = false) {
  if(canSeeCost) return report;
  return {
    ...report,
    lines: Array.isArray(report?.lines) ? report.lines.map(line => sanitizeSaleLineForState(line, false)) : []
  };
}

function taskVisibleForUser(task, user) {
  if(userHasPermission(user, "admin")) return true;
  const assignee = cleanText(task.cashier || "Todos").toLowerCase();
  const userName = cleanText(user?.name || "").toLowerCase();
  return !assignee || assignee === "todos" || assignee === userName;
}

function filterStateForUser(state, sessionContext) {
  const user = sessionContext?.user || null;
  const isAdmin = userHasPermission(user, "admin");
  if(isAdmin) return state;

  const canPos = userHasPermission(user, "pos");
  const canCaja = userHasPermission(user, "caja");
  const canCustomers = userHasPermission(user, "customers");
  const canCatalog = userCanAccessAny(user, ["inventory", "inventoryCatalog", "inventoryAdjustments", "purchaseNotes", "purchaseOrders", "shoppingList", "promotions", "departmentLabels", "pos"]);
  const canInventoryAdjustments = userCanAccessAny(user, ["inventory", "inventoryAdjustments"]);
  const canDepartmentLabels = userHasPermission(user, "departmentLabels");
  const canPurchases = userCanAccessAny(user, ["inventory", "purchaseNotes", "purchaseOrders", "shoppingList"]);
  const canPromotions = userCanAccessAny(user, ["inventory", "promotions"]);
  const canSeeCosts = canSeeProductCostData(user);

  const ownSales = canPos || canCaja
    ? state.saleReports.filter(sale => belongsToCurrentUserOrSession(sale, sessionContext)).slice(-80).map(sale => sanitizeSaleReportForState(sale, canSeeCosts))
    : [];
  const ownCashMovements = canPos || canCaja
    ? state.cashMovements.filter(movement => belongsToCurrentUserOrSession(movement, sessionContext)).slice(-120)
    : [];
  const ownCutReports = canCaja
    ? state.cutReports.filter(cut => belongsToCurrentUserOrSession(cut, sessionContext)).slice(-80)
    : [];

  return {
    ...state,
    cashSession: sessionContext?.cashSession || null,
    cashSessions: [],
    departments: canCatalog ? state.departments : [],
    suppliers: canSeeCosts ? state.suppliers : [],
    products: canCatalog ? state.products.map(product => sanitizeProductForState(product, canSeeCosts)) : [],
    departmentInventory: canDepartmentLabels ? state.departmentInventory : [],
    departmentLabels: canDepartmentLabels ? state.departmentLabels : [],
    saleReports: ownSales,
    quotations: canPos ? state.quotations : [],
    cashMovements: ownCashMovements,
    customers: (canPos || canCustomers) ? state.customers.map(customer => sanitizeCustomerForState(customer, canCustomers)) : [],
    customerMovements: canCustomers ? state.customerMovements : [],
    inventoryAdjustments: canInventoryAdjustments ? state.inventoryAdjustments : [],
    purchaseNotes: canPurchases ? state.purchaseNotes : [],
    purchaseOrders: canPurchases ? (state.purchaseOrders || []) : [],
    shoppingList: canPurchases ? state.shoppingList : [],
    shoppingListNotes: canPurchases ? state.shoppingListNotes : {},
    cutReports: ownCutReports,
    auditEvents: [],
    cashierUsers: [],
    cashierTasks: state.cashierTasks.filter(task => taskVisibleForUser(task, user)),
    utilityExpenses: [],
    license: sanitizeLicenseForState(state.license, false),
    adminSettings: sanitizeSettingsForState(state.adminSettings, false)
  };
}

function getState(sessionContext = null, options = {}) {
  cleanupExpiredQuotations();
  const saleLimit = Math.max(0, cleanInteger(options.saleLimit || 0));
  const saleOffset = Math.max(0, cleanInteger(options.saleOffset || 0));
  const partialState = Boolean(options.saleLimit || options.movementLimit || options.adjustmentLimit || options.departmentLabelLimit);
  const saleParams = saleLimit ? [saleLimit, saleOffset] : [];
  const saleSource = saleLimit ? "(SELECT * FROM sales ORDER BY id DESC LIMIT ? OFFSET ?) s" : "sales s";
  const saleReports = db.prepare(`
    SELECT s.id, s.session_id, s.date, s.folio, s.total, s.paid, s.change_amount, s.cashier,
      s.payments_json, s.ticket, s.status, s.cancel_reason, s.cancelled_at, s.cancelled_by, s.cancelled_session_id,
      s.customer_id, s.customer_name, s.loyalty_phone, s.loyalty_points,
      COALESCE(SUM(si.quantity), 0) AS items
    FROM ${saleSource}
    LEFT JOIN sale_items si ON si.sale_id = s.id
    GROUP BY s.id
    ORDER BY s.id
  `).all(...saleParams).map(rowToSaleReport);
  const saleLinesById = new Map();
  const saleIds = saleReports.map(report => report.id).filter(Boolean);
  const saleLinesWhere = saleLimit && saleIds.length ? ` WHERE sale_id IN (${saleIds.map(() => "?").join(",")})` : "";
  const saleLinesParams = saleLimit && saleIds.length ? saleIds : [];
  if(!saleLimit || saleIds.length) db.prepare(`SELECT id, sale_id, sku, name, quantity, price, cost_price, price_type, bundle_json, line_total FROM sale_items${saleLinesWhere} ORDER BY id`).all(...saleLinesParams).forEach(row => {
    if(!saleLinesById.has(row.sale_id)) saleLinesById.set(row.sale_id, []);
    saleLinesById.get(row.sale_id).push({
      id: row.id,
      saleItemId: row.id,
      sku: row.sku,
      name: row.name,
      quantity: row.quantity,
      price: row.price,
      costPrice: row.cost_price,
      priceType: row.price_type,
      bundleItems: jsonParse(row.bundle_json, []),
      source: row.price_type === "department-label" ? "department-label" : "inventory",
      lineTotal: row.line_total
    });
  });
  saleReports.forEach(report => {
    report.lines = saleLinesById.get(report.id) || [];
  });

  const state = {
    partialState,
    serverTime: nowIso(),
    cashSession: sessionContext?.cashSession || null,
    cashSessions: listCashSessionsForMonitor(),
    departments: db.prepare("SELECT name FROM departments ORDER BY name").all().map(row => row.name),
    suppliers: db.prepare("SELECT * FROM suppliers ORDER BY name").all().map(rowToSupplier),
    products: db.prepare("SELECT * FROM products ORDER BY name").all().map(rowToProduct),
    departmentInventory: db.prepare("SELECT * FROM department_inventory ORDER BY department, name").all().map(rowToDepartmentInventory),
    departmentLabels: db.prepare(options.departmentLabelLimit ? "SELECT * FROM department_labels ORDER BY created_at DESC LIMIT ? OFFSET ?" : "SELECT * FROM department_labels ORDER BY created_at").all(...(options.departmentLabelLimit ? [Math.max(1, cleanInteger(options.departmentLabelLimit)), Math.max(0, cleanInteger(options.departmentLabelOffset || 0))] : [])).map(rowToDepartmentLabel),
    saleReports,
    quotations: db.prepare("SELECT * FROM quotations ORDER BY id DESC LIMIT 250").all().map(rowToQuotation),
    cashMovements: db.prepare(options.movementLimit ? "SELECT * FROM cash_movements ORDER BY id DESC LIMIT ? OFFSET ?" : "SELECT * FROM cash_movements ORDER BY id").all(...(options.movementLimit ? [Math.max(1, cleanInteger(options.movementLimit)), Math.max(0, cleanInteger(options.movementOffset || 0))] : [])).map(rowToCashMovement),
    customers: db.prepare(options.customerLimit ? "SELECT * FROM customers ORDER BY name LIMIT ? OFFSET ?" : "SELECT * FROM customers ORDER BY name").all(...(options.customerLimit ? [Math.max(1, cleanInteger(options.customerLimit)), Math.max(0, cleanInteger(options.customerOffset || 0))] : [])).map(rowToCustomer),
    customerMovements: db.prepare(options.customerMovementLimit ? "SELECT * FROM customer_movements ORDER BY id DESC LIMIT ? OFFSET ?" : "SELECT * FROM customer_movements ORDER BY id DESC LIMIT 500").all(...(options.customerMovementLimit ? [Math.max(1, cleanInteger(options.customerMovementLimit)), Math.max(0, cleanInteger(options.customerMovementOffset || 0))] : [])).map(rowToCustomerMovement),
    inventoryAdjustments: db.prepare(options.adjustmentLimit ? "SELECT * FROM inventory_adjustments ORDER BY id DESC LIMIT ? OFFSET ?" : "SELECT * FROM inventory_adjustments ORDER BY id").all(...(options.adjustmentLimit ? [Math.max(1, cleanInteger(options.adjustmentLimit)), Math.max(0, cleanInteger(options.adjustmentOffset || 0))] : [])).map(rowToInventoryAdjustment),
    purchaseNotes: db.prepare(options.purchaseLimit ? "SELECT * FROM purchase_notes ORDER BY id DESC LIMIT ? OFFSET ?" : "SELECT * FROM purchase_notes ORDER BY id").all(...(options.purchaseLimit ? [Math.max(1, cleanInteger(options.purchaseLimit)), Math.max(0, cleanInteger(options.purchaseOffset || 0))] : [])).map(rowToPurchaseNote),
    shoppingList: db.prepare("SELECT sku, quantity, supplier FROM shopping_list ORDER BY updated_at DESC").all(),
    shoppingListNotes: getShoppingListNotes(),
    cutReports: db.prepare(options.cutLimit ? "SELECT id, session_id, client_request_id, date, cashier, shift, difference, notes, payload_json, created_at FROM cut_reports ORDER BY id DESC LIMIT ? OFFSET ?" : "SELECT id, session_id, client_request_id, date, cashier, shift, difference, notes, payload_json, created_at FROM cut_reports ORDER BY id").all(...(options.cutLimit ? [Math.max(1, cleanInteger(options.cutLimit)), Math.max(0, cleanInteger(options.cutOffset || 0))] : [])).map(rowToCutReport),
    auditEvents: db.prepare("SELECT * FROM audit_events ORDER BY id DESC LIMIT ? OFFSET ?").all(Math.max(80, Math.min(500, cleanInteger(options.auditLimit || 120))), Math.max(0, cleanInteger(options.auditOffset || 0))).map(rowToAuditEvent),
    cashierUsers: db.prepare("SELECT name, role, status, permissions_json FROM cashier_users ORDER BY name").all().map(publicUser),
    cashierTasks: db.prepare("SELECT t.*, (SELECT COUNT(*) FROM task_attachments a WHERE a.task_id = t.id) AS attachment_count FROM cashier_tasks t ORDER BY t.id").all().map(row => rowToTask(row, false)),
    utilityExpenses: db.prepare("SELECT * FROM utility_expenses ORDER BY date DESC, id DESC").all().map(rowToUtilityExpense),
    license: getLicenseStatus(),
    adminSettings: getSettings(),
    settingsSnapshots: listSettingsSnapshots(30),
    printProfiles: listPrintProfiles()
  };
  return filterStateForUser(state, sessionContext);
}

function getStatePageParams(searchParams, defaults = {}) {
  const defaultLimit = cleanInteger(defaults.limit || 120);
  const maxLimit = cleanInteger(defaults.max || 500);
  const limit = Math.max(1, Math.min(maxLimit, cleanInteger(searchParams.get("limit") || defaultLimit)));
  const offset = Math.max(0, cleanInteger(searchParams.get("offset") || 0));
  return { limit, offset };
}

function getStatePaginationMeta(tableName, limit, offset, where = "") {
  const total = safeDbCount(tableName, where);
  return {
    total,
    limit,
    offset,
    hasMore: offset + limit < total
  };
}

function createEmptyState(sessionContext = null) {
  return {
    partialState: true,
    serverTime: nowIso(),
    cashSession: sessionContext?.cashSession || null,
    cashSessions: [],
    departments: [],
    suppliers: [],
    products: [],
    departmentInventory: [],
    departmentLabels: [],
    saleReports: [],
    quotations: [],
    cashMovements: [],
    customers: [],
    customerMovements: [],
    inventoryAdjustments: [],
    purchaseNotes: [],
    purchaseOrders: [],
    shoppingList: [],
    shoppingListNotes: {},
    cutReports: [],
    auditEvents: [],
    cashierUsers: [],
    cashierTasks: [],
    utilityExpenses: [],
    license: getLicenseStatus(),
    adminSettings: getSettings()
  };
}

function loadSaleReportsPage(limit, offset) {
  const saleReports = db.prepare(`
    SELECT s.id, s.session_id, s.date, s.folio, s.total, s.paid, s.change_amount, s.cashier,
      s.payments_json, s.ticket, s.status, s.cancel_reason, s.cancelled_at, s.cancelled_by, s.cancelled_session_id,
      s.customer_id, s.customer_name, s.loyalty_phone, s.loyalty_points,
      COALESCE(SUM(si.quantity), 0) AS items
    FROM (SELECT * FROM sales ORDER BY id DESC LIMIT ? OFFSET ?) s
    LEFT JOIN sale_items si ON si.sale_id = s.id
    GROUP BY s.id
    ORDER BY s.id
  `).all(limit, offset).map(rowToSaleReport);
  const saleIds = saleReports.map(report => report.id).filter(Boolean);
  if(saleIds.length) {
    const saleLinesById = new Map();
    db.prepare(`SELECT id, sale_id, sku, name, quantity, price, cost_price, price_type, bundle_json, line_total FROM sale_items WHERE sale_id IN (${saleIds.map(() => "?").join(",")}) ORDER BY id`).all(...saleIds).forEach(row => {
      if(!saleLinesById.has(row.sale_id)) saleLinesById.set(row.sale_id, []);
      saleLinesById.get(row.sale_id).push({
        id: row.id,
        saleItemId: row.id,
        sku: row.sku,
        name: row.name,
        quantity: row.quantity,
        price: row.price,
        costPrice: row.cost_price,
        priceType: row.price_type,
        bundleItems: jsonParse(row.bundle_json, []),
        source: row.price_type === "department-label" ? "department-label" : "inventory",
        lineTotal: row.line_total
      });
    });
    saleReports.forEach(report => {
      report.lines = saleLinesById.get(report.id) || [];
    });
  }
  return saleReports;
}

function loadCoreCatalogState(state, { includeSuppliers = true, includeProducts = true } = {}) {
  state.departments = db.prepare("SELECT name FROM departments ORDER BY name").all().map(row => row.name);
  if(includeSuppliers) state.suppliers = db.prepare("SELECT * FROM suppliers ORDER BY name").all().map(rowToSupplier);
  if(includeProducts) state.products = db.prepare("SELECT * FROM products ORDER BY name").all().map(rowToProduct);
  return state;
}

function buildStateModuleResponse(sessionContext, moduleName, searchParams) {
  const page = getStatePageParams(searchParams, { limit: 120, max: 500 });
  const pagination = {};
  const state = createEmptyState(sessionContext);

  if(moduleName === "pos") {
    loadCoreCatalogState(state, { includeSuppliers: true, includeProducts: true });
    state.customers = db.prepare("SELECT * FROM customers ORDER BY name LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCustomer);
    state.saleReports = loadSaleReportsPage(page.limit, page.offset);
    state.quotations = db.prepare("SELECT * FROM quotations ORDER BY id DESC LIMIT ? OFFSET ?").all(Math.min(page.limit, 250), page.offset).map(rowToQuotation);
    state.cashMovements = db.prepare("SELECT * FROM cash_movements ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCashMovement);
    state.cashierTasks = db.prepare("SELECT t.*, (SELECT COUNT(*) FROM task_attachments a WHERE a.task_id = t.id) AS attachment_count FROM cashier_tasks t ORDER BY t.id").all().map(row => rowToTask(row, false));
    state.purchaseNotes = db.prepare(`
      SELECT * FROM purchase_notes
      WHERE COALESCE(status, '') NOT IN ('received', 'reviewed', 'cancelled')
        AND (
          status IN ('draft', 'ordered', 'receiving', 'partially_received')
          OR mode IN ('pedido_borrador', 'pedido', 'order', 'pedido_revision')
        )
      ORDER BY id DESC
      LIMIT 80
    `).all().map(rowToPurchaseNote);
    pagination.sales = getStatePaginationMeta("sales", page.limit, page.offset);
    pagination.cashMovements = getStatePaginationMeta("cash_movements", page.limit, page.offset);
  } else if(moduleName === "inventory") {
    loadCoreCatalogState(state, { includeSuppliers: true, includeProducts: true });
    state.departmentInventory = db.prepare("SELECT * FROM department_inventory ORDER BY department, name").all().map(rowToDepartmentInventory);
    state.departmentLabels = db.prepare("SELECT * FROM department_labels ORDER BY created_at DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToDepartmentLabel);
    state.inventoryAdjustments = db.prepare("SELECT * FROM inventory_adjustments ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToInventoryAdjustment);
    state.shoppingList = db.prepare("SELECT sku, quantity, supplier FROM shopping_list ORDER BY updated_at DESC").all();
    state.shoppingListNotes = getShoppingListNotes();
    pagination.inventoryAdjustments = getStatePaginationMeta("inventory_adjustments", page.limit, page.offset);
    pagination.departmentLabels = getStatePaginationMeta("department_labels", page.limit, page.offset);
  } else if(moduleName === "customers") {
    state.customers = db.prepare("SELECT * FROM customers ORDER BY name LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCustomer);
    state.customerMovements = db.prepare("SELECT * FROM customer_movements ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCustomerMovement);
    pagination.customers = getStatePaginationMeta("customers", page.limit, page.offset);
    pagination.customerMovements = getStatePaginationMeta("customer_movements", page.limit, page.offset);
  } else if(moduleName === "purchases") {
    loadCoreCatalogState(state, { includeSuppliers: true, includeProducts: true });
    state.purchaseNotes = db.prepare("SELECT * FROM purchase_notes ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToPurchaseNote);
    state.shoppingList = db.prepare("SELECT sku, quantity, supplier FROM shopping_list ORDER BY updated_at DESC").all();
    state.shoppingListNotes = getShoppingListNotes();
    pagination.purchaseNotes = getStatePaginationMeta("purchase_notes", page.limit, page.offset);
  } else if(moduleName === "reports") {
    state.cashSessions = listCashSessionsForMonitor();
    state.saleReports = loadSaleReportsPage(page.limit, page.offset);
    state.cashMovements = db.prepare("SELECT * FROM cash_movements ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCashMovement);
    state.cutReports = db.prepare("SELECT id, session_id, client_request_id, date, cashier, shift, difference, notes, payload_json, created_at FROM cut_reports ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCutReport);
    state.customerMovements = db.prepare("SELECT * FROM customer_movements ORDER BY id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToCustomerMovement);
    state.utilityExpenses = db.prepare("SELECT * FROM utility_expenses ORDER BY date DESC, id DESC LIMIT ? OFFSET ?").all(page.limit, page.offset).map(rowToUtilityExpense);
    state.inventoryAdjustments = db.prepare("SELECT * FROM inventory_adjustments ORDER BY id DESC LIMIT ? OFFSET ?").all(Math.max(page.limit, 500), page.offset).map(rowToInventoryAdjustment);
    state.auditEvents = db.prepare("SELECT * FROM audit_events ORDER BY id DESC LIMIT ? OFFSET ?").all(Math.min(page.limit, 120), page.offset).map(rowToAuditEvent);
    pagination.sales = getStatePaginationMeta("sales", page.limit, page.offset);
    pagination.cashMovements = getStatePaginationMeta("cash_movements", page.limit, page.offset);
    pagination.cutReports = getStatePaginationMeta("cut_reports", page.limit, page.offset);
  }

  const filteredState = filterStateForUser(state, sessionContext);
  const base = {
    ok: true,
    partial: true,
    module: moduleName,
    serverTime: filteredState.serverTime,
    cashSession: filteredState.cashSession,
    pagination
  };

  if(moduleName === "pos") {
    return {
      ...base,
      departments: filteredState.departments,
      suppliers: filteredState.suppliers,
      products: filteredState.products,
      customers: filteredState.customers,
      saleReports: filteredState.saleReports,
      quotations: filteredState.quotations,
      cashMovements: filteredState.cashMovements,
      cashierTasks: filteredState.cashierTasks,
      purchaseNotes: filteredState.purchaseNotes,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  if(moduleName === "inventory") {
    return {
      ...base,
      departments: filteredState.departments,
      suppliers: filteredState.suppliers,
      products: filteredState.products,
      departmentInventory: filteredState.departmentInventory,
      departmentLabels: filteredState.departmentLabels,
      inventoryAdjustments: filteredState.inventoryAdjustments,
      shoppingList: filteredState.shoppingList,
      shoppingListNotes: filteredState.shoppingListNotes,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  if(moduleName === "customers") {
    return {
      ...base,
      customers: filteredState.customers,
      customerMovements: filteredState.customerMovements,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  if(moduleName === "purchases") {
    return {
      ...base,
      departments: filteredState.departments,
      suppliers: filteredState.suppliers,
      products: filteredState.products,
      purchaseNotes: filteredState.purchaseNotes,
      purchaseOrders: filteredState.purchaseOrders,
      shoppingList: filteredState.shoppingList,
      shoppingListNotes: filteredState.shoppingListNotes,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  if(moduleName === "audit") {
    const auditResult = queryAuditEvents({
      limit: searchParams.get("limit"),
      offset: searchParams.get("offset"),
      status: searchParams.get("status"),
      exact: searchParams.get("exact"),
      from: searchParams.get("from"),
      to: searchParams.get("to"),
      q: searchParams.get("q"),
      sort: searchParams.get("sort"),
      direction: searchParams.get("direction")
    });
    return {
      ...base,
      auditEvents: auditResult.auditEvents,
      metrics: auditResult.metrics,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  if(moduleName === "reports") {
    return {
      ...base,
      cashSessions: filteredState.cashSessions,
      saleReports: filteredState.saleReports,
      cashMovements: filteredState.cashMovements,
      cutReports: filteredState.cutReports,
      customerMovements: filteredState.customerMovements,
      utilityExpenses: filteredState.utilityExpenses,
      inventoryAdjustments: filteredState.inventoryAdjustments,
      auditEvents: filteredState.auditEvents,
      adminSettings: filteredState.adminSettings,
      license: filteredState.license
    };
  }
  throw httpError(404, "Modulo de estado no encontrado.");
}

function upsertDepartment(name) {
  const cleanName = cleanText(name);
  if(!cleanName) throw httpError(400, "Departamento requerido.");
  db.prepare("INSERT OR IGNORE INTO departments(name) VALUES (?)").run(cleanName);
  return cleanName;
}

function updateDepartment(input) {
  const originalName = cleanText(input.originalName || input.previousName || input.oldName || input.name);
  const newName = cleanText(input.name || input.newName);
  if(!originalName) throw httpError(400, "Departamento original requerido.");
  if(!newName) throw httpError(400, "Nuevo nombre de departamento requerido.");
  const exists = db.prepare("SELECT name FROM departments WHERE name = ?").get(originalName);
  if(!exists) throw httpError(404, "Departamento no encontrado.");
  const duplicate = db.prepare("SELECT name FROM departments WHERE LOWER(name) = LOWER(?) AND name <> ?").get(newName, originalName);
  if(duplicate) throw httpError(409, "Ya existe otro departamento con ese nombre.");
  db.exec("BEGIN IMMEDIATE");
  try {
    if(originalName !== newName) {
      db.prepare("UPDATE departments SET name = ? WHERE name = ?").run(newName, originalName);
      db.prepare("UPDATE products SET category = ?, updated_at = ? WHERE category = ?").run(newName, nowIso(), originalName);
      db.prepare("UPDATE department_inventory SET department = ?, updated_at = ? WHERE department = ?").run(newName, nowIso(), originalName);
      db.prepare("UPDATE department_labels SET department = ? WHERE department = ?").run(newName, originalName);
    }
    db.exec("COMMIT");
  } catch(error) {
    db.exec("ROLLBACK");
    throw error;
  }
  return newName;
}

function deleteDepartment(input) {
  const name = cleanText(input.name);
  const reassignTo = cleanText(input.reassignTo || input.reassign_to);
  if(!name) throw httpError(400, "Departamento requerido.");
  const exists = db.prepare("SELECT name FROM departments WHERE name = ?").get(name);
  if(!exists) throw httpError(404, "Departamento no encontrado.");
  const productCount = cleanInteger(db.prepare("SELECT COUNT(*) AS count FROM products WHERE category = ?").get(name)?.count);
  const internalCount = cleanInteger(db.prepare("SELECT COUNT(*) AS count FROM department_inventory WHERE department = ?").get(name)?.count);
  const labelCount = cleanInteger(db.prepare("SELECT COUNT(*) AS count FROM department_labels WHERE department = ?").get(name)?.count);
  const hasUsage = productCount + internalCount + labelCount > 0;
  if(hasUsage && !reassignTo) {
    throw httpError(409, `No se puede eliminar porque tiene uso: ${productCount} producto(s), ${internalCount} interno(s), ${labelCount} etiqueta(s). Indica un departamento para reasignar.`);
  }
  if(hasUsage && reassignTo === name) throw httpError(409, "El departamento destino debe ser diferente.");
  db.exec("BEGIN IMMEDIATE");
  try {
    if(hasUsage) {
      upsertDepartment(reassignTo);
      db.prepare("UPDATE products SET category = ?, updated_at = ? WHERE category = ?").run(reassignTo, nowIso(), name);
      db.prepare("UPDATE department_inventory SET department = ?, updated_at = ? WHERE department = ?").run(reassignTo, nowIso(), name);
      db.prepare("UPDATE department_labels SET department = ? WHERE department = ?").run(reassignTo, name);
    }
    db.prepare("DELETE FROM departments WHERE name = ?").run(name);
    db.exec("COMMIT");
  } catch(error) {
    db.exec("ROLLBACK");
    throw error;
  }
  return true;
}

function upsertSupplier(supplier) {
  const cleanName = cleanText(supplier.name);
  if(!cleanName) throw httpError(400, "Proveedor requerido.");
  const originalName = cleanText(supplier.originalName || supplier.previousName);
  const previous = originalName
    ? db.prepare("SELECT * FROM suppliers WHERE name = ?").get(originalName)
    : db.prepare("SELECT * FROM suppliers WHERE name = ?").get(cleanName);
  const hasField = (...keys) => keys.some(key => Object.prototype.hasOwnProperty.call(supplier, key));
  const keepText = (camelKey, dbKey, fallback = "") => {
    if(hasField(camelKey, dbKey)) return cleanText(supplier[camelKey] ?? supplier[dbKey], fallback);
    return cleanText(previous?.[dbKey], fallback);
  };
  const keepAliases = () => {
    const previousAliases = jsonParse(previous?.aliases_json, []);
    if(!hasField("aliases", "aliases_json")) return previousAliases;
    const incomingAliases = Array.isArray(supplier.aliases) ? supplier.aliases : jsonParse(supplier.aliases_json, []);
    const merged = [...previousAliases, ...incomingAliases]
      .map(alias => cleanText(alias))
      .filter(Boolean);
    return [...new Set(merged)];
  };
  if(originalName && originalName !== cleanName) {
    db.prepare("UPDATE products SET supplier = ?, updated_at = ? WHERE supplier = ?").run(cleanName, nowIso(), originalName);
    db.prepare("UPDATE purchase_notes SET supplier = ? WHERE supplier = ?").run(cleanName, originalName);
    db.prepare("UPDATE shopping_supplier_notes SET supplier = ? WHERE supplier = ?").run(cleanName, originalName);
    db.prepare("DELETE FROM suppliers WHERE name = ?").run(originalName);
  }
  db.prepare(`
    INSERT OR REPLACE INTO suppliers(name, aliases_json, promoter, visit_days, delivery_days, visit_type, phone, supervisor, supervisor_phone, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    cleanName,
    jsonString(keepAliases()),
    keepText("promoter", "promoter"),
    keepText("visitDays", "visit_days"),
    keepText("deliveryDays", "delivery_days"),
    keepText("visitType", "visit_type", "Preventa"),
    keepText("phone", "phone"),
    keepText("supervisor", "supervisor"),
    keepText("supervisorPhone", "supervisor_phone"),
    nowIso()
  );
  return cleanName;
}

function priceInputValue(input, camelKey, snakeKey = "") {
  if(Object.prototype.hasOwnProperty.call(input, camelKey)) return input[camelKey];
  if(snakeKey && Object.prototype.hasOwnProperty.call(input, snakeKey)) return input[snakeKey];
  return undefined;
}

function protectedProductPrice(existing, product, camelKey, snakeKey, dbKey, fallback = 0) {
  const raw = priceInputValue(product, camelKey, snakeKey);
  const incoming = cleanPriceDecimal(raw, fallback);
  const existingValue = existing ? cleanNumber(existing[dbKey]) : 0;
  if(existing && product.allowZeroPrices !== true && incoming <= 0 && existingValue > 0) {
    return existingValue;
  }
  return incoming;
}

function analyzeBulkPriceResetRisk(products) {
  const risk = { sale: 0, cost: 0, skus: [] };
  if(!Array.isArray(products) || !products.length) return risk;
  products.forEach(product => {
    const sku = cleanText(product.sku).toUpperCase();
    if(!sku || product.allowZeroPrices === true) return;
    const existing = db.prepare("SELECT sku, cost_price, sale_price FROM products WHERE sku = ?").get(sku);
    if(!existing) return;
    const incomingSale = cleanNumber(priceInputValue(product, "salePrice", "sale_price"));
    const incomingCost = cleanNumber(priceInputValue(product, "costPrice", "cost_price"));
    let itemRisk = false;
    if(cleanNumber(existing.sale_price) > 0 && incomingSale <= 0) {
      risk.sale++;
      itemRisk = true;
    }
    if(cleanNumber(existing.cost_price) > 0 && incomingCost <= 0) {
      risk.cost++;
      itemRisk = true;
    }
    if(itemRisk && risk.skus.length < 8) risk.skus.push(sku);
  });
  return risk;
}

function upsertProduct(product, sessionContext = null) {
  const sku = cleanText(product.sku).toUpperCase();
  const originalSku = cleanText(product.originalSku || product.previousSku).toUpperCase();
  const name = cleanText(product.name, sku);
  if(!sku) throw httpError(400, "SKU requerido.");
  if(!name) throw httpError(400, "Nombre de producto requerido.");
  const existingExactSku = db.prepare("SELECT sku FROM products WHERE sku = ?").get(sku);
  if(!existingExactSku || (originalSku && originalSku !== sku)) {
    const collisions = findSkuVariantCollisions(sku, originalSku);
    if(collisions.length && product.allowSkuVariantCollision !== true) {
      const examples = collisions.slice(0, 4).map(item => `${item.sku} - ${item.name}`).join("; ");
      throw httpError(409, `Codigo ambiguo: ${sku} comparte una variante UPC/EAN sin ceros con ${examples}. Corrige el codigo o confirma bajo tu responsabilidad.`);
    }
  }
  if(originalSku && originalSku !== sku) {
    const target = db.prepare("SELECT sku FROM products WHERE sku = ?").get(sku);
    if(target) throw httpError(409, `Ya existe un producto con el SKU ${sku}.`);
    const original = db.prepare("SELECT sku FROM products WHERE sku = ?").get(originalSku);
    if(original) {
      db.prepare("UPDATE products SET sku = ?, updated_at = ? WHERE sku = ?").run(sku, nowIso(), originalSku);
      db.prepare("UPDATE shopping_list SET sku = ?, updated_at = ? WHERE sku = ?").run(sku, nowIso(), originalSku);
    }
  }
  if(product.category) upsertDepartment(product.category);
  if(product.supplier) upsertSupplier({ name: product.supplier });
  const existing = db.prepare("SELECT sku, cost_price, sale_price, wholesale_price, wholesale_price_2, inactivity_keep_active_at, photo FROM products WHERE sku = ?").get(sku);
  const costPrice = protectedProductPrice(existing, product, "costPrice", "cost_price", "cost_price");
  const salePrice = protectedProductPrice(existing, product, "salePrice", "sale_price", "sale_price");
  const wholesalePrice = protectedProductPrice(existing, product, "wholesalePrice", "wholesale_price", "wholesale_price", salePrice);
  const wholesalePrice2 = protectedProductPrice(existing, product, "wholesalePrice2", "wholesale_price_2", "wholesale_price_2");
  const supplierOptions = Array.isArray(product.supplierOptions) ? product.supplierOptions.map(option => ({
    supplier: cleanText(option.supplier),
    cost: cleanPriceDecimal(option.cost),
    code: cleanText(option.code),
    minimum: Math.max(0, cleanInteger(option.minimum)),
    preferred: Boolean(option.preferred)
  })).filter(option => option.supplier) : [];
  supplierOptions.forEach(option => upsertSupplier({ name: option.supplier, aliases: option.code ? [option.code] : [] }));
  db.prepare(`
    INSERT OR REPLACE INTO products(sku, name, supplier, category, stock, minimum_stock, cost_price, sale_price, wholesale_price, wholesale_price_2, wholesale_qty_1, wholesale_qty_2, package_pieces, bundle_json, location, photo, aliases_json, supplier_aliases_json, supplier_options_json, product_type, sale_mode, product_status, tax_enabled, tax_country, tax_name, tax_rate, inactivity_keep_active_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    sku,
    name,
    cleanText(product.supplier),
    cleanText(product.category, "General"),
    cleanNumber(product.stock),
    cleanNumber(product.minimum),
    costPrice,
    salePrice,
    wholesalePrice,
    wholesalePrice2,
    cleanInteger(product.wholesaleQty1),
    cleanInteger(product.wholesaleQty2),
    Math.max(1, cleanInteger(product.packagePieces, 1)),
    jsonString(Array.isArray(product.bundleItems) ? product.bundleItems : []),
    cleanText(product.location),
    String(product.photo || "").startsWith("/product-photo/") ? String(existing?.photo || "") : String(product.photo || ""),
    jsonString(Array.isArray(product.aliases) ? product.aliases : []),
    jsonString(product.supplierAliases && typeof product.supplierAliases === "object" ? product.supplierAliases : {}),
    jsonString(supplierOptions),
    cleanText(product.productType || product.product_type, "sale") === "supply" ? "supply" : "sale",
    cleanText(product.saleMode || product.sale_mode, "unit") === "bulk" ? "bulk" : "unit",
    cleanText(product.status || product.productStatus || product.product_status, "Activo") === "Pausado" ? "Pausado" : "Activo",
    product.taxEnabled || product.tax_enabled ? 1 : 0,
    cleanText(product.taxCountry || product.tax_country, "MX"),
    cleanText(product.taxName || product.tax_name, "IVA"),
    Math.max(0, cleanNumber(product.taxRate ?? product.tax_rate, 16)),
    cleanText(product.inactivityKeepActiveAt || product.inactivity_keep_active_at || existing?.inactivity_keep_active_at || ""),
    nowIso()
  );
  return sku;
}

function updateProductStatus(input = {}, sessionContext = null) {
  const sku = cleanText(input.sku).toUpperCase();
  const status = cleanText(input.status, "Activo") === "Pausado" ? "Pausado" : "Activo";
  if(!sku) throw httpError(400, "SKU requerido.");
  const existing = db.prepare("SELECT sku, name, category, product_status, supplier, inactivity_keep_active_at FROM products WHERE sku = ?").get(sku);
  if(!existing) throw httpError(404, "Producto no encontrado.");
  const pauseDepartment = "Productos en pausa";
  upsertDepartment(pauseDepartment);
  let nextCategory = cleanText(input.category, existing.category || "General");
  let previousCategory = cleanText(input.previousCategory || input.previous_category || existing.category || "General");
  if(status === "Pausado") {
    previousCategory = existing.category && existing.category !== pauseDepartment ? existing.category : previousCategory;
    nextCategory = pauseDepartment;
  } else {
    const requestedCategory = cleanText(input.restoreCategory || input.originalCategory || input.original_category || input.category);
    nextCategory = requestedCategory && requestedCategory !== pauseDepartment ? requestedCategory : (existing.category === pauseDepartment ? "General" : existing.category || "General");
  }
  const supplierAliases = jsonParse(db.prepare("SELECT supplier_aliases_json FROM products WHERE sku = ?").get(sku)?.supplier_aliases_json, {});
  if(status === "Pausado" && previousCategory && previousCategory !== pauseDepartment) {
    supplierAliases.__previousCategory = previousCategory;
  }
  const savedPrevious = supplierAliases.__previousCategory || previousCategory;
  if(status === "Activo" && existing.category === pauseDepartment && savedPrevious && savedPrevious !== pauseDepartment) {
    nextCategory = savedPrevious;
    delete supplierAliases.__previousCategory;
  }
  const keepActiveAt = status === "Activo" && (input.fromAuditAction === true || input.from_audit_action === true || input.keepActive === true || input.keep_active === true)
    ? nowIso()
    : cleanText(existing.inactivity_keep_active_at);
  db.prepare("UPDATE products SET product_status = ?, category = ?, supplier_aliases_json = ?, inactivity_keep_active_at = ?, updated_at = ? WHERE sku = ?")
    .run(status, nextCategory, jsonString(supplierAliases), keepActiveAt, nowIso(), sku);
  const skipAudit = input.fromAuditAction === true || input.from_audit_action === true || input.skipAudit === true || input.skip_audit === true;
  if(!skipAudit) {
    logAuditEvent({
      type: status === "Pausado" ? "Producto movido a departamento Productos en pausa" : "Producto reactivado",
      severity: "Media",
      reference: sku,
      details: status === "Pausado"
        ? `${existing.name} se movió de ${existing.category || "General"} a ${pauseDepartment}. Sigue disponible para venta mientras tenga stock.`
        : `${existing.name} quedó activo en ${nextCategory}.`,
      payload: { sku, status, previousCategory: existing.category || "", category: nextCategory }
    }, sessionContext);
  }
  return rowToProduct(db.prepare("SELECT * FROM products WHERE sku = ?").get(sku));
}

function deleteProduct(input, sessionContext = null) {
  const sku = cleanText(input.sku).toUpperCase();
  if(!sku) throw httpError(400, "SKU requerido.");
  const existing = db.prepare("SELECT sku, name, stock, sale_price FROM products WHERE sku = ?").get(sku);
  if(!existing) throw httpError(404, "Producto no encontrado.");
  db.prepare("DELETE FROM products WHERE sku = ?").run(sku);
  db.prepare("DELETE FROM shopping_list WHERE sku = ?").run(sku);
  logAuditEvent({
    type: "Producto eliminado",
    severity: "Alta",
    reference: sku,
    amount: cleanNumber(existing.sale_price),
    details: `Se elimino el producto ${existing.name} con stock ${existing.stock}.`,
    payload: existing
  }, sessionContext);
}

function upsertCustomer(input) {
  const id = cleanInteger(input.id);
  const name = cleanText(input.name);
  if(!name) throw httpError(400, "Nombre de cliente requerido.");
  const payload = {
    address: cleanText(input.address),
    phone: cleanText(input.phone),
    addresses: Array.isArray(input.addresses) ? input.addresses.map(item => cleanText(item)).filter(Boolean) : [],
    hasCredit: input.hasCredit === true || input.has_credit === true || input.hasCredit === 1 || input.has_credit === 1,
    creditLimit: Math.max(0, cleanNumber(input.creditLimit ?? input.credit_limit)),
    priceLevel: ["sale", "wholesale1", "wholesale2"].includes(cleanText(input.priceLevel || input.price_level)) ? cleanText(input.priceLevel || input.price_level) : "sale",
    status: cleanText(input.status, "Activo") === "Inactivo" ? "Inactivo" : "Activo"
  };
  if(id) {
    const existing = db.prepare("SELECT id FROM customers WHERE id = ?").get(id);
    if(!existing) throw httpError(404, "Cliente no encontrado.");
    db.prepare(`
      UPDATE customers SET name = ?, address = ?, phone = ?, addresses_json = ?, has_credit = ?, credit_limit = ?, price_level = ?, status = ?, updated_at = ?
      WHERE id = ?
    `).run(name, payload.address, payload.phone, jsonString(payload.addresses), payload.hasCredit ? 1 : 0, payload.creditLimit, payload.priceLevel, payload.status, nowIso(), id);
    return id;
  }
  db.prepare(`
    INSERT INTO customers(name, address, phone, addresses_json, has_credit, credit_limit, balance, loyalty_points, price_level, status, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(name, payload.address, payload.phone, jsonString(payload.addresses), payload.hasCredit ? 1 : 0, payload.creditLimit, cleanNumber(input.balance), cleanNumber(input.loyaltyPoints || input.loyalty_points), payload.priceLevel, payload.status, nowIso(), nowIso());
  return scalar("SELECT last_insert_rowid() AS id").id;
}

function deleteCustomer(input, sessionContext = null) {
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "Cliente requerido.");
  const existing = db.prepare("SELECT * FROM customers WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Cliente no encontrado.");
  if(cleanNumber(existing.balance) > 0) throw httpError(409, "No puedes eliminar un cliente con adeudo.");
  db.prepare("DELETE FROM customers WHERE id = ?").run(id);
  db.prepare("DELETE FROM customer_movements WHERE customer_id = ?").run(id);
  logAuditEvent({
    type: "Cliente eliminado",
    severity: "Media",
    reference: String(id),
    details: `Se elimino el cliente ${existing.name}.`,
    payload: existing
  }, sessionContext);
}

function registerCustomerPayment(input, sessionContext = null) {
  const id = cleanInteger(input.customerId || input.customer_id || input.id);
  const amount = cleanNumber(input.amount);
  if(!id) throw httpError(400, "Cliente requerido.");
  if(amount <= 0) throw httpError(400, "Monto requerido.");
  const method = cleanText(input.method, "Efectivo");
  const date = cleanText(input.date, new Date().toLocaleString("es-MX"));
  const cashier = sessionContext?.user?.name || cleanText(input.cashier);
  const sessionId = sessionContext?.cashSession?.id || cleanText(input.sessionId);
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  db.exec("BEGIN IMMEDIATE");
  try {
    if(clientRequestId) {
      const existing = db.prepare("SELECT id FROM customer_movements WHERE client_request_id = ?").get(clientRequestId);
      if(existing) {
        db.exec("COMMIT");
        return existing.id;
      }
    }
    const customer = db.prepare("SELECT * FROM customers WHERE id = ?").get(id);
    if(!customer) throw httpError(404, "Cliente no encontrado.");
    const currentBalance = cleanNumber(customer.balance);
    if(amount > currentBalance + 0.011) throw httpError(409, "El abono no puede ser mayor al adeudo actual.");
    const nextBalance = Math.max(0, currentBalance - amount);
    db.prepare("UPDATE customers SET balance = ?, updated_at = ? WHERE id = ?").run(nextBalance, nowIso(), id);
    db.prepare(`
      INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier, client_request_id)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(id, date, "abono", amount, 0, method, cleanText(input.reference), cleanText(input.notes), cashier, clientRequestId);
    const customerMovementId = scalar("SELECT last_insert_rowid() AS id").id;
    db.prepare(`
      INSERT INTO cash_movements(session_id, type, amount, method, concept, reference, cashier, date, client_request_id)
      VALUES (?, 'income', ?, ?, ?, ?, ?, ?, ?)
    `).run(sessionId, amount, method, `Abono cliente: ${customer.name}`, cleanText(input.reference), cashier, date, clientRequestId);
    db.exec("COMMIT");
    return customerMovementId;
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function upsertUtilityExpense(input) {
  const concept = cleanText(input.concept);
  const amount = cleanNumber(input.amount);
  if(!concept) throw httpError(400, "Concepto requerido.");
  if(amount <= 0) throw httpError(400, "Importe requerido.");
  const type = cleanText(input.type, "daily") === "fixed" ? "fixed" : "daily";
  const date = cleanDateOnly(input.date, localDateInputValue());
  db.prepare(`
    INSERT INTO utility_expenses(date, type, concept, amount, active, created_at)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(date, type, concept, amount, input.active === false ? 0 : 1, nowIso());
}

function deleteUtilityExpense(input, sessionContext = null) {
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "ID requerido.");
  const existing = db.prepare("SELECT * FROM utility_expenses WHERE id = ?").get(id);
  db.prepare("DELETE FROM utility_expenses WHERE id = ?").run(id);
  if(existing) logAuditEvent({
    type: "Gasto/utilidad eliminado",
    severity: "Media",
    reference: String(id),
    amount: cleanNumber(existing.amount),
    details: `Se elimino gasto: ${existing.concept || "sin concepto"}.`,
    payload: existing
  }, sessionContext);
}

function upsertDepartmentInventory(product) {
  const department = cleanText(product.department, "General");
  const sku = cleanDepartmentSku(product.sku || `${department}-${product.name || ""}`);
  const name = cleanText(product.name, sku);
  if(!sku) throw httpError(400, "SKU interno requerido.");
  if(!name) throw httpError(400, "Nombre de producto interno requerido.");
  db.prepare(`
    INSERT INTO department_inventory(department, sku, name, stock, unit, price, status, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(sku) DO UPDATE SET
      department = excluded.department,
      name = excluded.name,
      stock = excluded.stock,
      unit = excluded.unit,
      price = excluded.price,
      status = excluded.status,
      updated_at = excluded.updated_at
  `).run(
    department,
    sku,
    name,
    cleanNumber(product.stock),
    cleanText(product.unit, "Pieza"),
    cleanNumber(product.price),
    cleanText(product.status, "Activo"),
    nowIso()
  );
  return sku;
}

function createDepartmentLabel(input) {
  const rawLines = Array.isArray(input.lines) && input.lines.length
    ? input.lines
    : [{ sku: input.sku, quantity: input.quantity, unitPrice: input.unitPrice ?? input.unit_price }];

  db.exec("BEGIN IMMEDIATE");
  try {
    const productBySku = db.prepare("SELECT * FROM department_inventory WHERE sku = ?");
    const requests = rawLines.map((line, index) => {
      const sku = cleanDepartmentSku(line.sku);
      const quantity = cleanNumber(line.quantity);
      if(!sku) throw httpError(400, `SKU interno requerido en linea ${index + 1}.`);
      if(quantity <= 0) throw httpError(400, `Cantidad mayor a cero requerida en linea ${index + 1}.`);
      const product = productBySku.get(sku);
      if(!product) throw httpError(404, `Producto interno no encontrado: ${sku}`);
      if(cleanText(product.status, "Activo") !== "Activo") throw httpError(409, `Producto interno inactivo: ${product.name}`);
      return {
        product,
        quantity,
        unitPrice: cleanNumber(line.unitPrice ?? line.unit_price, cleanNumber(product.price))
      };
    });
    if(!requests.length) throw httpError(400, "Agrega productos al ticket interno.");

    const department = cleanText(input.department, requests[0].product.department);
    requests.forEach(request => {
      if(request.product.department !== department) throw httpError(409, "El ticket interno debe pertenecer a un solo departamento.");
    });
    const requestedBySku = new Map();
    requests.forEach(request => {
      const current = requestedBySku.get(request.product.sku) || 0;
      requestedBySku.set(request.product.sku, current + request.quantity);
    });
    requests.forEach(request => {
      const requested = requestedBySku.get(request.product.sku) || request.quantity;
      if(cleanNumber(request.product.stock) < requested) throw httpError(409, `Inventario insuficiente en ${request.product.department} para ${request.product.name}. Stock actual: ${request.product.stock}`);
    });

    const lines = requests.map(request => ({
      sku: request.product.sku,
      name: request.product.name,
      quantity: request.quantity,
      unit: cleanText(request.product.unit, "Pieza"),
      unitPrice: request.unitPrice,
      total: request.quantity * request.unitPrice
    }));

    let code = cleanDepartmentSku(input.code) || makeDepartmentLabelCode();
    while(db.prepare("SELECT code FROM department_labels WHERE code = ?").get(code)) code = makeDepartmentLabelCode();
    const total = lines.reduce((sum, line) => sum + line.total, 0);
    const totalQuantity = lines.reduce((sum, line) => sum + line.quantity, 0);
    const productName = cleanText(input.productName || input.product_name, lines.length > 1 ? `Ticket ${department} (${lines.length} conceptos)` : lines[0].name);
    const createdAt = cleanText(input.createdAt || input.created_at, new Date().toLocaleString("es-MX"));

    const updateStock = db.prepare("UPDATE department_inventory SET stock = stock - ?, updated_at = ? WHERE sku = ?");
    lines.forEach(line => updateStock.run(line.quantity, nowIso(), line.sku));
    db.prepare(`
      INSERT INTO department_labels(code, department, sku, product_name, quantity, unit, unit_price, total, status, created_at, charged_at, cashier, notes, lines_json)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      code,
      department,
      lines[0].sku,
      productName,
      totalQuantity,
      lines.length > 1 ? "Ticket" : lines[0].unit,
      lines.length > 1 ? total : lines[0].unitPrice,
      total,
      "Pendiente",
      createdAt,
      "",
      cleanText(input.cashier),
      cleanText(input.notes),
      jsonString(lines)
    );
    db.exec("COMMIT");
    return rowToDepartmentLabel(db.prepare("SELECT * FROM department_labels WHERE code = ?").get(code));
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function createInventoryAdjustment(input, sessionContext = null) {
  const sku = cleanText(input.sku).toUpperCase();
  const type = cleanText(input.type, "add");
  const quantity = Math.max(0, cleanNumber(input.quantity));
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  if(!sku) throw httpError(400, "SKU requerido.");
  if(type !== "set" && quantity <= 0) throw httpError(400, "Cantidad mayor a cero requerida.");

  db.exec("BEGIN IMMEDIATE");
  try {
    if(clientRequestId) {
      const existing = db.prepare("SELECT id FROM inventory_adjustments WHERE client_request_id = ?").get(clientRequestId);
      if(existing) {
        db.exec("COMMIT");
        return existing.id;
      }
    }
    const product = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
    if(!product) throw httpError(404, "Producto no encontrado.");
    const beforeStock = cleanNumber(product.stock);
    let delta;
    let afterStock;
    if(type === "set") {
      afterStock = Math.max(0, quantity);
      delta = afterStock - beforeStock;
    } else {
      delta = type === "add" ? quantity : -quantity;
      afterStock = beforeStock + delta;
    }
    if(afterStock < 0) throw httpError(409, "El ajuste dejaria inventario negativo.");

    db.prepare("UPDATE products SET stock = ?, updated_at = ? WHERE sku = ?").run(afterStock, nowIso(), sku);
    db.prepare(`
      INSERT INTO inventory_adjustments(date, sku, product_name, supplier, type, quantity, delta, before_stock, after_stock, reference, reason, client_request_id)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      cleanText(input.date, new Date().toLocaleDateString("es-MX")),
      sku,
      product.name,
      product.supplier,
      type,
      quantity,
      delta,
      beforeStock,
      afterStock,
      cleanText(input.reference),
      cleanText(input.reason),
      clientRequestId
    );
    const adjustmentId = scalar("SELECT last_insert_rowid() AS id").id;
    db.exec("COMMIT");
    if(delta < 0 || type === "set") {
      logAuditEvent({
        type: "Ajuste sensible de inventario",
        severity: delta < 0 ? "Alta" : "Media",
        reference: sku,
        amount: Math.abs(delta),
        details: `${product.name}: stock ${beforeStock} -> ${afterStock}. Motivo: ${cleanText(input.reason, "Sin motivo")}.`,
        payload: { sku, productName: product.name, type, quantity, delta, beforeStock, afterStock, reference: cleanText(input.reference), reason: cleanText(input.reason) }
      }, sessionContext);
    }
    logNegativeStockCorrectionIfNeeded({
      sku,
      productName: product.name,
      supplier: product.supplier,
      beforeStock,
      afterStock,
      quantity: Math.max(delta, 0),
      source: type === "set" ? "ajuste físico/set" : "entrada de inventario",
      reference: cleanText(input.reference),
      reason: cleanText(input.reason)
    }, sessionContext);
    return adjustmentId;
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function logNegativeStockCorrectionIfNeeded({ sku, productName, supplier, beforeStock, afterStock, quantity, source, reference, reason }, sessionContext = null) {
  const before = cleanNumber(beforeStock);
  const after = cleanNumber(afterStock);
  if(before >= 0 || after < 0) return;
  const added = cleanNumber(quantity, Math.max(after - before, 0));
  logAuditEvent({
    type: "Inventario negativo corregido",
    severity: "Media",
    reference: cleanText(sku).toUpperCase(),
    amount: added,
    details: `${cleanText(productName, sku)} pasó de stock ${before} a ${after}. Corrección registrada por ${cleanText(source, "ajuste de inventario")}.`,
    payload: {
      action: "negativeStockCorrected",
      sku: cleanText(sku).toUpperCase(),
      productName: cleanText(productName, sku),
      supplier: cleanText(supplier),
      beforeStock: before,
      afterStock: after,
      quantity: added,
      source: cleanText(source),
      reference: cleanText(reference),
      reason: cleanText(reason)
    }
  }, sessionContext);
}


function getCashMovementPolicy(settings = null) {
  const source = settings || getSettings();
  let authMode = cleanText(source.cashPaymentAuthMode || (source.requireWithdrawalAuth === false ? "none" : "aboveLimit"));
  if(!["none", "aboveLimit", "always"].includes(authMode)) authMode = "aboveLimit";
  return {
    cashierIncomeEnabled: source.cashierCashIncomeEnabled !== false,
    cashierPaymentEnabled: source.cashierCashPaymentEnabled !== false,
    paymentAuthMode: authMode,
    paymentPendingReview: source.cashPaymentPendingReview !== false,
    withdrawalLimit: Math.max(0, cleanNumber(source.withdrawalLimit, 0))
  };
}

function isAdminUser(user) {
  return cleanText(user?.role).toLowerCase() === "admin";
}

function cashPaymentNeedsAdminAuthorization(policy, amount, user) {
  if(isAdminUser(user)) return false;
  if(policy.paymentAuthMode === "always") return true;
  if(policy.paymentAuthMode === "aboveLimit") return policy.withdrawalLimit > 0 && amount > policy.withdrawalLimit;
  return false;
}

function validateCashMovementPolicy(input, type, amount, sessionContext) {
  const user = sessionContext?.user;
  const policy = getCashMovementPolicy();
  if(type === "income") {
    if(!policy.cashierIncomeEnabled && !isAdminUser(user)) {
      throw httpError(403, "Las entradas de efectivo solo pueden registrarlas administradores segun la configuracion de caja.");
    }
    return { policy, reviewStatus: "", adminAuthorizedBy: "" };
  }

  if(type === "payment") {
    if(!policy.cashierPaymentEnabled && !isAdminUser(user)) {
      throw httpError(403, "Las salidas de efectivo solo pueden registrarlas administradores segun la configuracion de caja.");
    }
    let adminAuthorizedBy = "";
    if(cashPaymentNeedsAdminAuthorization(policy, amount, user)) {
      const adminPin = cleanText(input.adminPin || input.authorizationPin || input.authorizationCode || input.temporaryAdminPin);
      if(!adminPin) {
        const label = policy.paymentAuthMode === "always" ? "para registrar salidas" : `para salidas mayores a ${policy.withdrawalLimit}`;
        throw httpError(403, `Se requiere PIN de administrador ${label}.`);
      }
      const adminUser = validateAdminPin(adminPin);
      if(!adminUser) throw httpError(403, "PIN de administrador invalido.");
      adminAuthorizedBy = cleanText(adminUser.name, "Administrador");
    }
    const reviewStatus = adminAuthorizedBy
      ? "authorized"
      : (policy.paymentPendingReview && !isAdminUser(user) ? "pending_review" : "");
    return { policy, reviewStatus, adminAuthorizedBy };
  }
  return { policy, reviewStatus: "", adminAuthorizedBy: "" };
}

function createCashMovement(input, sessionContext) {
  if(sessionContext?.mobileApp || isMobileAppDevice({ device: sessionContext?.cashSession?.device || input.device || input.terminal })) {
    logAuditEvent({
      type: "Movimiento de efectivo bloqueado desde app",
      severity: "Alta",
      reference: cleanText(input.reference || input.concept),
      amount: cleanNumber(input.amount),
      details: "La app Android no puede registrar entradas ni salidas de efectivo; debe usarse la caja de la PC.",
      payload: { source: "Android", type: cleanText(input.type), concept: cleanText(input.concept), method: cleanText(input.method), reference: cleanText(input.reference) }
    }, sessionContext);
    throw httpError(403, "La app Android no tiene caja propia. Las entradas y salidas de efectivo deben registrarse en la PC.");
  }
  const type = cleanText(input.type);
  const amount = cleanNumber(input.amount);
  if(!["income", "payment"].includes(type)) throw httpError(400, "Tipo de movimiento invalido.");
  if(amount <= 0) throw httpError(400, "Monto mayor a cero requerido.");
  const policyResult = validateCashMovementPolicy(input, type, amount, sessionContext);
  const reviewStatus = policyResult.reviewStatus || "";
  const adminAuthorizedBy = policyResult.adminAuthorizedBy || "";
  const method = cleanText(input.method, "Efectivo");
  const concept = cleanText(input.concept);
  const reference = cleanText(input.reference);
  if(!concept || concept === "Pago de caja" || concept === "Entrada de efectivo") {
    throw httpError(400, "Captura concepto, proveedor o referencia clara para auditar el movimiento.");
  }
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  if(clientRequestId) {
    const existing = db.prepare("SELECT id FROM cash_movements WHERE client_request_id = ?").get(clientRequestId);
    if(existing) return existing.id;
  }
  const cashier = sessionContext?.user?.name || cleanText(input.cashier);
  const sessionId = cleanText(input.sessionId) || sessionContext?.cashSession?.id || "";
  db.prepare(`
    INSERT INTO cash_movements(session_id, type, amount, method, concept, reference, cashier, date, client_request_id, review_status, admin_authorized_by)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    sessionId,
    type,
    amount,
    method,
    concept,
    reference,
    cashier,
    cleanText(input.date, new Date().toLocaleString("es-MX")),
    clientRequestId,
    reviewStatus,
    adminAuthorizedBy
  );
  const movementId = scalar("SELECT last_insert_rowid() AS id").id;
  if(type === "payment") {
    logAuditEvent({
      type: reviewStatus === "pending_review" ? "Salida de efectivo pendiente de revisión" : "Salida de efectivo / proveedor",
      severity: reviewStatus === "pending_review" || amount >= 500 ? "Alta" : "Media",
      reference,
      amount,
      details: `${concept} por ${method}.${reviewStatus === "pending_review" ? " Pendiente de revisión en Auditoría IA." : ""}${adminAuthorizedBy ? ` Autorizado por ${adminAuthorizedBy}.` : ""}`,
      payload: {
        action: reviewStatus === "pending_review" ? "cashMovementReview" : "cashMovement",
        reviewed: reviewStatus === "pending_review" ? false : Boolean(adminAuthorizedBy),
        movementId,
        type,
        amount,
        method,
        concept,
        reference,
        cashier,
        sessionId,
        reviewStatus,
        adminAuthorizedBy
      }
    }, sessionContext);
  }
  return movementId;
}


function authorizeNegativeStockSale(input = {}, sessionContext = null, settingsInput = null) {
  let settings = settingsInput;
  try {
    settings = settings || getSettings();
  } catch(error) {
    settings = {};
  }
  const allowNegative = settings.allowNegativeStockSales === true || settings.allowNegativeStockSales === 1 || settings.allowNegativeStockSales === "true";
  if(!allowNegative) return { allowed: false, reason: "La venta con inventario negativo está desactivada en configuración." };
  const mode = cleanText(settings.negativeStockSaleMode || "admin");
  if(mode === "all") return { allowed: true, user: sessionContext?.user || null, method: "permitido a todos" };
  if(mode === "selected" && userHasPermission(sessionContext?.user, "sellNegativeStock")) {
    return { allowed: true, user: sessionContext.user, method: "usuario seleccionado" };
  }
  if(sessionContext?.user?.role === "admin") {
    return { allowed: true, user: sessionContext.user, method: "administrador activo" };
  }
  const adminPin = cleanText(input.negativeStockAdminPin || input.adminPin || input.authorizationCode || input.temporaryAdminPin);
  const adminUser = validateAdminPin(adminPin);
  if(adminUser) return { allowed: true, user: adminUser, method: adminUser.temporary ? "PIN temporal administrador" : "PIN administrador" };
  return { allowed: false, reason: mode === "selected" ? "Tu usuario no tiene permiso para vender sin inventario. Usa un PIN de administrador o PIN temporal." : "Se requiere PIN de administrador o PIN temporal para vender sin inventario." };
}

function dateKeyFromStoredValue(value, fallback = "") {
  const text = cleanText(value);
  if(!text) return fallback;
  const isoDate = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:$|[T\s])/);
  if(isoDate && !text.includes("T")) return `${isoDate[1]}-${isoDate[2]}-${isoDate[3]}`;
  const mx = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
  if(mx) {
    const year = Number(mx[3]) < 100 ? 2000 + Number(mx[3]) : Number(mx[3]);
    return `${year}-${String(mx[2]).padStart(2, "0")}-${String(mx[1]).padStart(2, "0")}`;
  }
  const parsed = new Date(text);
  return Number.isFinite(parsed.getTime()) ? localDateInputValue(parsed) : fallback;
}

function getPreviousLocalDateKey() {
  const date = new Date();
  date.setDate(date.getDate() - 1);
  return localDateInputValue(date);
}

function reconcileDailyExpenseOutputs(dateKey = getPreviousLocalDateKey(), options = {}) {
  const cleanDate = cleanDateOnly(dateKey, getPreviousLocalDateKey());
  const movementRows = db.prepare("SELECT id, amount, method, concept, reference, cashier, date FROM cash_movements WHERE type = 'payment'").all()
    .filter(row => dateKeyFromStoredValue(row.date) === cleanDate);
  const receiptRows = db.prepare(`
    SELECT id, supplier, folio, date, total, receipt_total, received_at, status, mode
    FROM purchase_notes
    WHERE status = 'received' OR mode IN ('pedido_recibido', 'manual', 'image')
  `).all().filter(row => dateKeyFromStoredValue(row.received_at || row.date) === cleanDate);
  const utilityRows = db.prepare("SELECT id, date, type, concept, amount, active FROM utility_expenses WHERE active = 1").all()
    .filter(row => row.type === "fixed" || dateKeyFromStoredValue(row.date) === cleanDate);

  const cashOutputs = movementRows.reduce((sum, row) => sum + cleanNumber(row.amount), 0);
  const receivedPurchases = receiptRows.reduce((sum, row) => sum + cleanNumber(row.receipt_total || row.total), 0);
  const dailyExpenses = utilityRows.reduce((sum, row) => sum + cleanNumber(row.amount), 0);
  const documentedTotal = receivedPurchases + dailyExpenses;
  const difference = Math.round((cashOutputs - documentedTotal) * 100) / 100;
  const hasActivity = cashOutputs > 0.009 || documentedTotal > 0.009;
  const matches = Math.abs(difference) < 0.01;
  const cashierTotals = {};
  movementRows.forEach(row => {
    const cashier = cleanText(row.cashier, "Sin cajero");
    cashierTotals[cashier] = Math.round(((cashierTotals[cashier] || 0) + cleanNumber(row.amount)) * 100) / 100;
  });
  const result = {
    date: cleanDate,
    cashOutputs: Math.round(cashOutputs * 100) / 100,
    receivedPurchases: Math.round(receivedPurchases * 100) / 100,
    dailyExpenses: Math.round(dailyExpenses * 100) / 100,
    documentedTotal: Math.round(documentedTotal * 100) / 100,
    difference,
    matches,
    hasActivity,
    movementCount: movementRows.length,
    receiptCount: receiptRows.length,
    utilityExpenseCount: utilityRows.length,
    cashierTotals
  };
  if(hasActivity && !matches && options.logAlert !== false) {
    const reference = `expense-reconciliation-${cleanDate}`;
    const alreadyLogged = db.prepare("SELECT id FROM audit_events WHERE type = ? AND reference = ? LIMIT 1").get("Gastos no corresponden", reference);
    if(!alreadyLogged) {
      const sign = difference > 0 ? "+" : "-";
      logAuditEvent({
        type: "Gastos no corresponden",
        severity: "Alta",
        reference,
        amount: difference,
        details: `Las salidas/gastos no corresponden con los pedidos recibidos ni con los gastos diarios capturados en Utilidades. Diferencia: ${sign}$${Math.abs(difference).toFixed(2)}. Salidas: $${cashOutputs.toFixed(2)}; pedidos recibidos: $${receivedPurchases.toFixed(2)}; gastos del dia: $${dailyExpenses.toFixed(2)}.`,
        payload: { action: "dailyExpenseReconciliation", ...result }
      }, null);
      result.auditCreated = true;
    } else {
      result.auditCreated = false;
      result.auditEventId = alreadyLogged.id;
    }
  }
  return result;
}

let dailyExpenseReconciliationTimer = null;
function scheduleDailyExpenseReconciliation() {
  if(dailyExpenseReconciliationTimer) clearTimeout(dailyExpenseReconciliationTimer);
  const now = new Date();
  const nextRun = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 10, 0, 0);
  dailyExpenseReconciliationTimer = setTimeout(() => {
    try {
      reconcileDailyExpenseOutputs(getPreviousLocalDateKey(), { logAlert: true });
    } catch(error) {
      console.warn("No se pudo conciliar gastos diarios:", error.message || error);
    } finally {
      scheduleDailyExpenseReconciliation();
    }
  }, Math.max(1000, nextRun.getTime() - now.getTime()));
  dailyExpenseReconciliationTimer.unref?.();
}

function listInternalWithdrawals(sessionContext, requestedSessionId = "") {
  const sessionId = cleanText(requestedSessionId || sessionContext?.cashSession?.id);
  if(!sessionId) return [];
  if(sessionId !== cleanText(sessionContext?.cashSession?.id) && !isAdminUser(sessionContext?.user)) {
    throw httpError(403, "No puedes consultar retiros de otra caja.");
  }
  return db.prepare("SELECT * FROM internal_withdrawals WHERE session_id = ? ORDER BY id").all(sessionId).map(row => ({
    id: row.id,
    sessionId: row.session_id,
    time: row.time_text,
    concept: row.concept,
    amount: cleanNumber(row.amount_mxn),
    originalAmount: cleanNumber(row.original_amount),
    currency: row.currency,
    detail: row.detail_html,
    cashier: row.cashier,
    clientRequestId: row.client_request_id,
    createdAt: row.created_at
  }));
}

function createInternalWithdrawal(input, sessionContext) {
  const sessionId = cleanText(sessionContext?.cashSession?.id || input.sessionId);
  if(!sessionId) throw httpError(409, "No hay caja activa para registrar el retiro.");
  const amount = cleanNumber(input.amount || input.amountMxn || input.m);
  if(amount <= 0) throw httpError(400, "El retiro debe ser mayor a cero.");
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  if(clientRequestId) {
    const existing = db.prepare("SELECT id FROM internal_withdrawals WHERE client_request_id = ?").get(clientRequestId);
    if(existing) return existing.id;
  }
  db.prepare(`
    INSERT INTO internal_withdrawals(session_id, time_text, concept, amount_mxn, original_amount, currency, detail_html, cashier, client_request_id, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    sessionId,
    cleanText(input.time || input.h),
    cleanText(input.concept || input.n, "Retiro"),
    amount,
    cleanNumber(input.originalAmount || input.ori, amount),
    cleanText(input.currency || input.iso, "MXN").toUpperCase(),
    cleanText(input.detail || input.det),
    cleanText(sessionContext?.user?.name || input.cashier),
    clientRequestId,
    nowIso()
  );
  return scalar("SELECT last_insert_rowid() AS id").id;
}

function deleteInternalWithdrawal(input, sessionContext) {
  const id = cleanInteger(input.id);
  const row = db.prepare("SELECT * FROM internal_withdrawals WHERE id = ?").get(id);
  if(!row) throw httpError(404, "Retiro no encontrado.");
  if(cleanText(row.session_id) !== cleanText(sessionContext?.cashSession?.id) && !isAdminUser(sessionContext?.user)) {
    throw httpError(403, "No puedes borrar retiros de otra caja.");
  }
  db.prepare("DELETE FROM internal_withdrawals WHERE id = ?").run(id);
  return id;
}

function getSkuLookupVariants(value) {
  const clean = cleanText(value).replace(/\s+/g, "").toUpperCase();
  if(!clean) return [];
  const variants = [clean];
  if(/^\d+$/.test(clean)) {
    const stripped = clean.replace(/^0+/, "");
    if(stripped && stripped !== clean) variants.push(stripped);
    if(clean.length < 12) variants.push(clean.padStart(12, "0"));
    if(clean.length < 13) variants.push(clean.padStart(13, "0"));
    if(clean.length === 12) {
      if(clean.startsWith("0")) variants.push(clean.slice(1));
      variants.push(`0${clean}`);
    }
    if(clean.length === 13 && clean.startsWith("0")) {
      variants.push(clean.slice(1));
      const noEanZero = clean.slice(1).replace(/^0+/, "");
      if(noEanZero) variants.push(noEanZero);
    }
  }
  return [...new Set(variants.filter(Boolean))];
}

function getSkuCollisionKeys(value) {
  const clean = cleanText(value).replace(/\s+/g, "").toUpperCase();
  if(!clean || !/^\d+$/.test(clean)) return [];
  const stripped = clean.replace(/^0+/, "");
  return stripped ? [stripped] : [clean];
}

function findSkuVariantCollisions(sku, originalSku = "") {
  const cleanSku = cleanText(sku).replace(/\s+/g, "").toUpperCase();
  const cleanOriginal = cleanText(originalSku).replace(/\s+/g, "").toUpperCase();
  const keys = new Set(getSkuCollisionKeys(cleanSku));
  if(!keys.size) return [];
  return db.prepare("SELECT sku, name FROM products").all()
    .filter(product => {
      const productSku = cleanText(product.sku).replace(/\s+/g, "").toUpperCase();
      if(!productSku || productSku === cleanSku || productSku === cleanOriginal) return false;
      return getSkuCollisionKeys(productSku).some(key => keys.has(key));
    });
}

function getSkuAmbiguityGroupsFromDatabase() {
  const groups = new Map();
  db.prepare("SELECT sku, name, supplier, category FROM products ORDER BY sku").all().forEach(product => {
    const sku = cleanText(product.sku).replace(/\s+/g, "").toUpperCase();
    getSkuCollisionKeys(sku).forEach(key => {
      if(!groups.has(key)) groups.set(key, []);
      groups.get(key).push({
        sku,
        name: cleanText(product.name),
        supplier: cleanText(product.supplier),
        category: cleanText(product.category)
      });
    });
  });
  return [...groups.entries()].map(([key, products]) => ({
    key,
    products: [...new Map(products.map(product => [product.sku, product])).values()]
  })).filter(group => group.products.length > 1)
    .sort((a, b) => b.products.length - a.products.length || a.key.localeCompare(b.key));
}

function scanSkuAmbiguitiesAndAudit(sessionContext = null) {
  const groups = getSkuAmbiguityGroupsFromDatabase();
  const auditIds = [];
  groups.forEach(group => {
    const products = [...group.products].sort((a, b) => a.sku.localeCompare(b.sku));
    const skuList = products.map(product => product.sku).join(", ");
    const id = logAuditEvent({
      type: "Código SKU ambiguo",
      severity: "Alta",
      reference: `SKU-AMBIGUO:${group.key}`,
      details: `La lectura ${group.key} puede corresponder a ${products.length} productos: ${skuList}. Corrige los códigos antes de escanearlos en venta.`,
      payload: {
        action: "sku_ambiguity_detected",
        collisionKey: group.key,
        skus: products.map(product => product.sku),
        products,
        requiresAdminReview: true
      }
    }, sessionContext);
    if(id) auditIds.push(id);
  });
  const auditEvents = auditIds.length
    ? db.prepare(`SELECT * FROM audit_events WHERE id IN (${auditIds.map(() => "?").join(",")}) ORDER BY id DESC`).all(...auditIds).map(rowToAuditEvent)
    : [];
  return { groups, auditEvents };
}

function findProductBySkuVariant(statement, sku) {
  const cleanSku = cleanText(sku).replace(/\s+/g, "").toUpperCase();
  const matches = [];
  const seen = new Set();
  for(const variant of getSkuLookupVariants(sku)) {
    const product = statement.get(variant);
    if(product && !seen.has(product.sku)) {
      seen.add(product.sku);
      matches.push(product);
    }
  }
  if(!matches.length) return null;
  const exact = matches.find(product => cleanText(product.sku).replace(/\s+/g, "").toUpperCase() === cleanSku);
  if(exact) return exact;
  if(matches.length > 1) {
    const examples = matches.slice(0, 4).map(product => `${product.sku} - ${product.name}`).join("; ");
    throw httpError(409, `Codigo ambiguo: ${cleanSku} puede corresponder a varios productos (${examples}). Escanea el codigo completo o corrige el SKU.`);
  }
  return matches[0];
}


function createSale(input, sessionContext) {
  const cart = Array.isArray(input.cart) ? input.cart : [];
  if(!cart.length) throw httpError(400, "La venta no tiene productos.");
  const payments = input.payments && typeof input.payments === "object" ? input.payments : {};
  const settings = getSettings();
  const customerId = cleanInteger(input.customerId || input.customer_id);
  const loyaltyPhone = cleanText(input.loyaltyPhone || input.loyalty_phone);
  const cashier = sessionContext?.user?.name || cleanText(input.cashier);
  const currentSessionId = sessionContext?.cashSession?.id || "";
  const isMobileAppSale = Boolean(sessionContext?.mobileApp || isMobileAppDevice({ device: sessionContext?.cashSession?.device || input.device || input.terminal || input.source }));
  const shouldUseActivePcSession = Boolean(input.useActiveCashierSession || input.use_active_cashier_session || isMobileAppSale);
  const targetCashSession = shouldUseActivePcSession ? findActivePcCashSessionForUser(cashier, currentSessionId) : null;
  if(isMobileAppSale && !targetCashSession?.id) {
    logAuditEvent({
      type: "Venta movil pendiente sin caja PC",
      severity: "Alta",
      reference: cleanText(input.clientRequestId || input.client_request_id),
      amount: cleanNumber(input.total || input.paid),
      details: `La app Android intento enviar una venta de ${cashier}, pero no hay una caja abierta en PC para ese mismo usuario. Abre sesion en la PC y reintenta la sincronizacion desde la app.`,
      payload: { source: "Android", clientRequestId: cleanText(input.clientRequestId || input.client_request_id), cartCount: Array.isArray(input.cart) ? input.cart.length : 0 }
    }, sessionContext);
    throw httpError(409, "No hay caja abierta en PC para este usuario. Abre sesion en la PC y reintenta la sincronizacion de la venta desde la app.");
  }
  const sessionId = targetCashSession?.id || (!isMobileAppSale ? cleanText(input.sessionId) || currentSessionId : "");
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  if(clientRequestId) {
    const existing = db.prepare("SELECT id, folio, total, paid, change_amount FROM sales WHERE client_request_id = ?").get(clientRequestId);
    if(existing) return { saleId: existing.id, folio: existing.folio, total: existing.total, paid: existing.paid, change: existing.change_amount };
  }
  const isOfflineSale = Boolean(input.offlineSale || input.offline_sale);
  const requestedNegativeStockSale = Boolean(input.allowNegativeStock || input.allow_negative_stock);
  const negativeStockAuthorization = requestedNegativeStockSale && !isOfflineSale ? authorizeNegativeStockSale(input, sessionContext, settings) : { allowed: Boolean(isOfflineSale), method: isOfflineSale ? "venta offline" : "" };
  const allowNegativeStockForSale = isOfflineSale || (requestedNegativeStockSale && negativeStockAuthorization.allowed);
  if(requestedNegativeStockSale && !isOfflineSale && !negativeStockAuthorization.allowed) throw httpError(403, negativeStockAuthorization.reason || "Venta con inventario negativo no autorizada.");
  const offlineStockAlerts = [];
  const negativeStockSaleAlerts = [];
  const belowCostSaleAlerts = [];

  db.exec("BEGIN IMMEDIATE");
  try {
    const productBySku = db.prepare("SELECT * FROM products WHERE sku = ?");
    const labelByCode = db.prepare("SELECT * FROM department_labels WHERE code = ?");
    const lines = cart.map((item, index) => {
      const sku = cleanText(item.sku).toUpperCase();
      const quantity = Math.max(0.001, cleanNumber(item.quantity, 1));
      const label = labelByCode.get(sku);
      if(label || cleanText(item.source) === "department-label") {
        if(!label) throw httpError(404, `Etiqueta interna no encontrada: ${sku}`);
        if(label.status !== "Pendiente") throw httpError(409, `La etiqueta ${sku} ya fue usada o cancelada.`);
        if(quantity !== 1) throw httpError(409, `La etiqueta ${sku} solo puede cobrarse una vez.`);
        if(cleanNumber(label.total) <= 0) throw httpError(409, `La etiqueta ${sku} tiene total en cero. Revisa el ticket interno antes de cobrar.`);
        return {
          sku,
          name: `${label.product_name} - ${label.department}`,
          quantity: 1,
          priceType: "department-label",
          price: label.total,
          costPrice: 0,
          lineTotal: label.total,
          source: "department-label",
          departmentSku: label.sku,
          departmentQuantity: label.quantity,
          index
        };
      }

      const product = findProductBySkuVariant(productBySku, sku);
      if(!product) throw httpError(404, `Producto no encontrado: ${sku}`);
      const saleSku = product.sku;
      const bundleItems = normalizeBundleItems(jsonParse(product.bundle_json, []));
      if(bundleItems.length) {
        bundleItems.forEach(component => {
          if(component.sku === saleSku) throw httpError(409, `El paquete ${product.name} no puede contenerse a si mismo.`);
          const componentProduct = productBySku.get(component.sku);
          if(!componentProduct) throw httpError(409, `El paquete ${product.name} contiene un SKU inexistente: ${component.sku}`);
          const required = component.quantity * quantity;
          const beforeStock = cleanNumber(componentProduct.stock);
          const afterStock = cleanNumber(beforeStock - required);
          if(beforeStock < required && !allowNegativeStockForSale) throw httpError(409, `Inventario insuficiente para ${componentProduct.name}. Requiere ${required}, stock actual: ${beforeStock}`);
          if(beforeStock < required) negativeStockSaleAlerts.push({ sku: componentProduct.sku, name: componentProduct.name, beforeStock, quantity: required, afterStock, parentSku: product.sku, parentName: product.name });
        });
      } else {
        const beforeStock = cleanNumber(product.stock);
        const afterStock = cleanNumber(beforeStock - quantity);
        if(beforeStock < quantity && !allowNegativeStockForSale) throw httpError(409, `Inventario insuficiente para ${product.name}. Stock actual: ${beforeStock}`);
        if(beforeStock < quantity) negativeStockSaleAlerts.push({ sku: product.sku, name: product.name, beforeStock, quantity, afterStock });
      }
      let priceType = cleanText(item.priceType, "sale");
      if(priceType === "wholesale") priceType = "wholesale1";
      if(!["sale", "wholesale1", "wholesale2", "forced", "percent", "utility"].includes(priceType)) priceType = "sale";
      if(priceType === "forced" && !userHasAnyPermission(sessionContext?.user, ["forcePrice", "posEditLinePrice", "posAdjustTotalAmount"])) throw httpError(403, "Permiso requerido: editar precio o ajustar total.");
      if(priceType === "percent" && !userHasAnyPermission(sessionContext?.user, ["discounts", "posAdjustTotalPercent"])) throw httpError(403, "Permiso requerido: aplicar descuento por porcentaje.");
      if(priceType === "utility") requirePermission(sessionContext?.user, "discounts");
      const salePrice = cleanNumber(product.sale_price);
      const wholesale1Price = cleanNumber(product.wholesale_price);
      const wholesale2Price = cleanNumber(product.wholesale_price_2);
      const discountPercent = Math.max(0, Math.min(99.99, cleanNumber(item.discountPercent || item.discount_percent)));
      const utilityPercent = Math.max(0, cleanNumber(item.utilityPercent || item.utility_percent));
      const basePrice = priceType === "wholesale1"
        ? (wholesale1Price > 0 ? wholesale1Price : salePrice)
        : priceType === "wholesale2"
          ? (wholesale2Price > 0 ? wholesale2Price : (wholesale1Price > 0 ? wholesale1Price : salePrice))
          : priceType === "forced"
            ? (cleanNumber(item.forcedPrice) > 0 ? cleanNumber(item.forcedPrice) : salePrice)
            : priceType === "percent"
              ? cleanMoney(salePrice * (1 - discountPercent / 100))
              : priceType === "utility"
                ? (cleanNumber(product.cost_price) > 0 ? cleanMoney(cleanNumber(product.cost_price) * (1 + utilityPercent / 100)) : salePrice)
            : salePrice;
      const promotion = getActivePromotionForProduct(product);
      const lineTotal = cleanMoney(["forced", "percent", "utility"].includes(priceType) ? basePrice * quantity : getPromotionLineTotal(product, basePrice, quantity));
      const price = cleanMoney(lineTotal / Math.max(quantity, 0.001));
      const priceLabel = priceType === "sale" ? "venta" : priceType === "wholesale2" ? "mayoreo 2" : priceType === "forced" ? "forzado" : priceType === "percent" ? "porcentaje" : priceType === "utility" ? "por utilidad" : "mayoreo 1";
      if(cleanNumber(price) <= 0) throw httpError(409, `El producto ${product.name} tiene precio ${priceLabel} en cero. Corrige el precio antes de cobrar.`);
      if(cleanNumber(product.cost_price) > 0 && price + 0.004 < cleanNumber(product.cost_price)) {
        belowCostSaleAlerts.push({ sku: saleSku, name: product.name, price, costPrice: cleanNumber(product.cost_price), priceType, discountPercent, utilityPercent });
      }
      return {
        sku: saleSku,
        name: product.name,
        quantity,
        priceType,
        price,
        costPrice: product.cost_price,
        lineTotal,
        loyaltyMultiplier: promotion?.discountType === "points3x" ? 3 : (promotion?.discountType === "points2x" ? 2 : 1),
        source: "inventory",
        bundleItems,
        index
      };
    });

    const deliveryFee = Math.max(0, cleanMoney(input.deliveryFee || input.delivery_fee || payments.deliveryFee || payments.delivery_fee || payments.deliveryOrder?.deliveryFee));
    const lineTotal = cleanMoney(lines.reduce((sum, line) => sum + line.lineTotal, 0));
    const total = cleanMoney(lineTotal + deliveryFee);
    if(total <= 0) throw httpError(409, "No se puede cobrar una venta con total en cero.");
    payments.deliveryFee = deliveryFee;
    if(payments.deliveryOrder && typeof payments.deliveryOrder === "object") {
      payments.deliveryOrder.deliveryFee = deliveryFee;
      payments.deliveryOrder.status = cleanText(payments.deliveryOrder.status, "Pendiente");
    }
    const requestedCredit = cleanMoney(payments.credit);
    const redeemedPoints = Math.max(0, cleanMoney(payments.points));
    const paid = cleanMoney(input.paid, total);
    const epsilon = 0.011;
    const nonCashTolerance = 2;
    const nonCashPaid = cleanMoney(cleanMoney(payments.card) + cleanMoney(payments.transfer) + requestedCredit);
    const cashLikePaid = cleanMoney(cleanMoney(payments.cash) + cleanMoney(payments.usd) * cleanNumber(payments.exchangeRate));
    if(nonCashPaid - total > nonCashTolerance + epsilon) throw httpError(400, "Tarjeta, transferencia y credito pueden variar maximo $2.00.");
    const coverageTolerance = nonCashPaid > epsilon ? nonCashTolerance : 0;
    if(paid + coverageTolerance + epsilon < total) throw httpError(400, "El pago no cubre el total de la venta.");
    const rawChangeAmount = cleanMoney(Math.max(Math.min(cleanMoney(paid - total), cashLikePaid), 0));
    const requestedChangeToPoints = cleanMoney(Math.max(0, cleanNumber(payments.changeToPoints || payments.change_to_points)));
    const changeToPoints = cleanMoney(Math.min(requestedChangeToPoints, rawChangeAmount));
    const changeAmount = cleanMoney(Math.max(rawChangeAmount - changeToPoints, 0));
    let customer = null;
    if(customerId) {
      customer = db.prepare("SELECT * FROM customers WHERE id = ? AND status = 'Activo'").get(customerId);
      if(!customer) throw httpError(404, "Cliente no encontrado o inactivo.");
    }
    if(changeToPoints > 0) {
      if(!settings.loyaltyEnabled) throw httpError(409, "Programa de puntos desactivado.");
      if(!customerId) throw httpError(400, "Selecciona cliente para enviar cambio a puntos.");
    }
    if(requestedCredit > 0) {
      if(!settings.customerCreditEnabled) throw httpError(409, "Credito de clientes desactivado.");
      if(!customer) throw httpError(400, "Selecciona cliente para venta a credito.");
      if(!customer.has_credit) throw httpError(409, "Este cliente no tiene credito activo.");
      const nextBalance = cleanNumber(customer.balance) + requestedCredit;
      if(cleanNumber(customer.credit_limit) > 0 && nextBalance > cleanNumber(customer.credit_limit)) throw httpError(409, "La venta excede el limite de credito del cliente.");
    }
    if(redeemedPoints > 0) {
      if(!settings.loyaltyEnabled) throw httpError(409, "Programa de puntos desactivado.");
      if(!customer) throw httpError(400, "Selecciona cliente para redimir puntos.");
      const availablePoints = cleanMoney(Math.max(0, cleanNumber(customer.loyalty_points)));
      if(redeemedPoints > availablePoints + epsilon) throw httpError(409, `El cliente solo tiene ${availablePoints} punto(s) disponibles.`);
      if(redeemedPoints > total + epsilon) throw httpError(409, "No puedes redimir mas puntos que el total de la venta.");
    }
    const loyaltySaleBase = lines.reduce((sum, line) => sum + cleanNumber(line.lineTotal) * Math.max(1, cleanNumber(line.loyaltyMultiplier, 1)), 0) + deliveryFee;
    const loyaltyProfitBase = lines.reduce((sum, line) => {
      const profit = Math.max(0, cleanNumber(line.lineTotal) - cleanNumber(line.costPrice) * cleanNumber(line.quantity));
      return sum + profit * Math.max(1, cleanNumber(line.loyaltyMultiplier, 1));
    }, 0);
    const earnMode = ["amount", "salePercent", "profitPercent"].includes(cleanText(settings.loyaltyEarnMode)) ? cleanText(settings.loyaltyEarnMode) : "amount";
    let baseLoyaltyPoints = 0;
    if(settings.loyaltyEnabled) {
      if(earnMode === "salePercent") baseLoyaltyPoints = cleanMoney(loyaltySaleBase * Math.max(0, cleanNumber(settings.loyaltyEarnPercent, 1)) / 100);
      else if(earnMode === "profitPercent") baseLoyaltyPoints = cleanMoney(loyaltyProfitBase * Math.max(0, cleanNumber(settings.loyaltyEarnPercent, 1)) / 100);
      else {
        const threshold = Math.max(0.01, cleanNumber(settings.loyaltyAmountThreshold, 100));
        const pointsPerThreshold = Math.max(0, cleanNumber(settings.loyaltyPointsPerThreshold, settings.loyaltyPointValue || 1));
        baseLoyaltyPoints = cleanMoney(Math.floor((loyaltySaleBase + 0.000001) / threshold) * pointsPerThreshold);
      }
    }
    const loyaltyPoints = baseLoyaltyPoints + changeToPoints;
    if(changeToPoints > 0) payments.changeToPoints = changeToPoints;
    const folio = `V-${String((scalar("SELECT COALESCE(MAX(id), 0) + 1 AS nextId FROM sales").nextId)).padStart(6, "0")}`;
    const date = cleanText(input.date, new Date().toLocaleString("es-MX"));

    const rawTicket = cleanText(input.ticket);
    const storedTicket = rawTicket && !/(^|\n)FOLIO:/i.test(rawTicket)
      ? rawTicket.replace(/(NOTA DE COMPRA|TICKET DE VENTA)\n/i, `$1\nFOLIO: ${folio}\n`)
      : rawTicket;

    db.prepare(`
      INSERT INTO sales(folio, session_id, date, total, paid, change_amount, cashier, payments_json, ticket, customer_id, customer_name, loyalty_phone, loyalty_points, client_request_id)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(folio, sessionId, date, total, paid, changeAmount, cashier, jsonString(payments), storedTicket, customer?.id || 0, customer?.name || "", loyaltyPhone || customer?.phone || "", loyaltyPoints, clientRequestId);
    const saleId = scalar("SELECT last_insert_rowid() AS id").id;

    const insertLine = db.prepare(`
      INSERT INTO sale_items(sale_id, sku, name, quantity, price, cost_price, price_type, bundle_json, line_total)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);
    const updateStock = db.prepare("UPDATE products SET stock = stock - ?, updated_at = ? WHERE sku = ?");
    const markLabelCharged = db.prepare("UPDATE department_labels SET status = 'Cobrado', charged_at = ?, cashier = ? WHERE code = ?");
    lines.forEach(line => {
      insertLine.run(saleId, line.sku, line.name, line.quantity, line.price, cleanNumber(line.costPrice), line.priceType, jsonString(line.bundleItems || []), line.lineTotal);
      if(line.source === "department-label") {
        markLabelCharged.run(date, cashier, line.sku);
      } else {
        const bundleItems = normalizeBundleItems(line.bundleItems);
        if(bundleItems.length) bundleItems.forEach(component => updateStock.run(component.quantity * line.quantity, nowIso(), component.sku));
        else updateStock.run(line.quantity, nowIso(), line.sku);
      }
    });
    if(negativeStockSaleAlerts.length) {
      const reason = cleanText(input.negativeStockReason || input.negative_stock_reason, "Producto fisico en tienda, inventario no capturado");
      const threshold = Math.max(1, cleanInteger(settings.negativeStockNoticeAt || 3, 3));
      negativeStockSaleAlerts.forEach(alertItem => {
        const suggestedQuantity = Math.max(1, Math.ceil(Math.abs(cleanNumber(alertItem.afterStock))));
        logAuditEvent({
          type: "Venta con inventario negativo",
          severity: alertItem.afterStock <= -threshold ? "Critica" : "Alta",
          reference: alertItem.sku,
          amount: cleanNumber(alertItem.quantity),
          details: `${alertItem.name}: stock ${alertItem.beforeStock} -> ${alertItem.afterStock} por venta ${folio}. Cajero: ${cashier || "S/N"}. Motivo: ${reason}. ${alertItem.afterStock <= -threshold ? "Avisar al administrador." : ""}`,
          payload: {
            action: "addStock",
            sku: alertItem.sku,
            productName: alertItem.name,
            saleFolio: folio,
            beforeStock: alertItem.beforeStock,
            soldQuantity: alertItem.quantity,
            afterStock: alertItem.afterStock,
            suggestedQuantity,
            reason,
            authorizedBy: negativeStockAuthorization?.user?.name || "",
            authorizationMethod: negativeStockAuthorization?.method || (isOfflineSale ? "offline" : "")
          }
        }, sessionContext);
      });
    }

    if(isOfflineSale) {
      const checkedSkus = new Set();
      lines.forEach(line => {
        if(line.source === "department-label") return;
        const bundleItems = normalizeBundleItems(line.bundleItems);
        if(bundleItems.length) bundleItems.forEach(component => checkedSkus.add(component.sku));
        else checkedSkus.add(line.sku);
      });
      checkedSkus.forEach(sku => {
        const product = db.prepare("SELECT sku, name, stock FROM products WHERE sku = ?").get(sku);
        if(product && cleanNumber(product.stock) < 0) {
          offlineStockAlerts.push({
            sku: product.sku,
            name: product.name,
            afterStock: cleanNumber(product.stock),
            suggestedQuantity: Math.ceil(Math.abs(cleanNumber(product.stock)))
          });
        }
      });
    }
    if(customer) {
      const creditAmount = cleanNumber(payments.credit);
      if(creditAmount > 0) {
        db.prepare("UPDATE customers SET balance = balance + ?, updated_at = ? WHERE id = ?").run(creditAmount, nowIso(), customer.id);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'cargo', ?, 0, 'Credito', ?, ?, ?)")
          .run(customer.id, date, creditAmount, folio, "Venta a credito", cashier);
      }
      if(redeemedPoints > 0) {
        db.prepare("UPDATE customers SET loyalty_points = MAX(loyalty_points - ?, 0), updated_at = ? WHERE id = ?").run(redeemedPoints, nowIso(), customer.id);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'redencion', ?, ?, 'Puntos', ?, ?, ?)")
          .run(customer.id, date, redeemedPoints, -redeemedPoints, folio, "Puntos redimidos en venta", cashier);
      }
      if(loyaltyPoints > 0) {
        db.prepare("UPDATE customers SET loyalty_points = loyalty_points + ?, updated_at = ? WHERE id = ?").run(loyaltyPoints, nowIso(), customer.id);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'puntos', 0, ?, 'Lealtad', ?, ?, ?)")
          .run(customer.id, date, loyaltyPoints, folio, changeToPoints > 0 ? `Puntos por compra + cambio enviado a puntos (${changeToPoints})` : "Puntos por compra", cashier);
      }
    }
    db.exec("COMMIT");
    if(isMobileAppSale) {
      logAuditEvent({
        type: "Venta movil ligada a caja PC",
        severity: isOfflineSale ? "Alta" : "Media",
        reference: folio,
        amount: total,
        details: `Venta de app Android registrada en la caja PC ${sessionId} del usuario ${cashier}.`,
        payload: { clientRequestId, linkedCashSessionId: sessionId, offlineSale: isOfflineSale, source: "Android" }
      }, sessionContext);
    }
    if(isOfflineSale) {
      logAuditEvent({
        type: "Venta offline sincronizada",
        severity: offlineStockAlerts.length ? "Alta" : "Media",
        reference: folio,
        amount: total,
        details: offlineStockAlerts.length
          ? `Venta offline sincronizada; ${offlineStockAlerts.length} producto(s) quedaron con inventario negativo.`
          : "Venta offline sincronizada correctamente.",
        payload: { clientRequestId, offlineStockAlerts }
      }, sessionContext);
      offlineStockAlerts.forEach(alertItem => {
        logAuditEvent({
          type: "Inventario negativo por venta offline",
          severity: "Critica",
          reference: alertItem.sku,
          amount: alertItem.afterStock,
          details: `El producto ${alertItem.name} quedó con inventario ${alertItem.afterStock}. Agrega al menos ${alertItem.suggestedQuantity} pieza(s).`,
          payload: { action: "addStock", sku: alertItem.sku, suggestedQuantity: alertItem.suggestedQuantity, saleFolio: folio }
        }, sessionContext);
      });
    }
    if(belowCostSaleAlerts.length) {
      logAuditEvent({
        type: "Venta con precio bajo costo",
        severity: "Alta",
        reference: folio,
        amount: belowCostSaleAlerts.reduce((sum, item) => sum + cleanNumber(item.costPrice - item.price), 0),
        details: `Venta ${folio} incluye ${belowCostSaleAlerts.length} producto(s) por debajo del costo.`,
        payload: { folio, items: belowCostSaleAlerts }
      }, sessionContext);
    }
    const affectedSkus = new Set();
    lines.forEach(line => {
      if(line.source === "department-label") return;
      const bundleItems = normalizeBundleItems(line.bundleItems);
      if(bundleItems.length) bundleItems.forEach(component => affectedSkus.add(component.sku));
      else affectedSkus.add(line.sku);
    });
    const updatedProductStatement = db.prepare("SELECT * FROM products WHERE sku = ?");
    const updatedProducts = [...affectedSkus].map(sku => updatedProductStatement.get(sku)).filter(Boolean).map(rowToProduct);
    return { saleId, folio, total, paid, change: changeAmount, ticket: storedTicket, customerId: customer?.id || 0, customerName: customer?.name || "", offlineStockAlerts, negativeStockSaleAlerts, belowCostSaleAlerts, linkedCashSessionId: sessionId, mobileLinkedToPcSession: Boolean(isMobileAppSale && sessionId), updatedProducts };
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function updateSaleDeliveryStatus(input = {}, sessionContext = null) {
  const saleId = cleanInteger(input.saleId || input.id, 0);
  const folio = cleanText(input.folio);
  const allowed = new Set(["Pendiente", "En ruta", "Enviado parcial", "Entregado", "Cancelado"]);
  const status = cleanText(input.status, "Pendiente");
  if(!allowed.has(status)) throw httpError(400, "Estatus de envio no valido.");
  const sale = saleId
    ? db.prepare("SELECT * FROM sales WHERE id = ?").get(saleId)
    : db.prepare("SELECT * FROM sales WHERE folio = ?").get(folio);
  if(!sale) throw httpError(404, "Venta no encontrada.");
  const payments = jsonParse(sale.payments_json, {});
  const incomingOrder = input.deliveryOrder && typeof input.deliveryOrder === "object" ? input.deliveryOrder : null;
  if((!payments.deliveryOrder || typeof payments.deliveryOrder !== "object") && incomingOrder) {
    payments.deliveryOrder = incomingOrder;
  }
  if(!payments.deliveryOrder || typeof payments.deliveryOrder !== "object") {
    throw httpError(404, "La venta no tiene orden de envio.");
  }
  if(incomingOrder) {
    payments.deliveryOrder = { ...payments.deliveryOrder, ...incomingOrder };
  }
  payments.deliveryOrder.status = status;
  payments.deliveryOrder.updatedAt = new Date().toLocaleString("es-MX");
  payments.deliveryOrder.updatedBy = cleanText(sessionContext?.user?.name, "");
  db.prepare("UPDATE sales SET payments_json = ? WHERE id = ?").run(jsonString(payments), sale.id);
  return { saleId: sale.id, folio: sale.folio, status };
}


function generateUniqueQuotationFolio() {
  const maxPerLetter = 999999;
  const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  const prefixToIndex = (prefix = "") => {
    const cleanPrefix = cleanText(prefix).toUpperCase();
    if(!/^[A-Z]{1,2}$/.test(cleanPrefix)) return -1;
    if(cleanPrefix.length === 1) return letters.indexOf(cleanPrefix);
    const first = letters.indexOf(cleanPrefix[0]);
    const second = letters.indexOf(cleanPrefix[1]);
    if(first < 0 || second < 0) return -1;
    return 26 + (first * 26) + second;
  };
  const indexToPrefix = (index) => {
    const safeIndex = Math.max(0, cleanInteger(index, 0));
    if(safeIndex < 26) return letters[safeIndex];
    const doubleIndex = safeIndex - 26;
    const first = Math.floor(doubleIndex / 26);
    const second = doubleIndex % 26;
    if(first >= 26) throw httpError(409, "Se agotaron los folios de cotizacion disponibles.");
    return `${letters[first]}${letters[second]}`;
  };
  const folioToOrdinal = (folio = "") => {
    const modern = cleanText(folio).match(/^([A-Z]{1,2})(\d{6,})$/i);
    if(!modern) return 0;
    const prefixIndex = prefixToIndex(modern[1]);
    if(prefixIndex < 0) return 0;
    const number = cleanInteger(modern[2], 0);
    if(number <= 0) return 0;
    return (prefixIndex * maxPerLetter) + Math.min(number, maxPerLetter);
  };
  const ordinalToFolio = (ordinal) => {
    const safeOrdinal = Math.max(1, cleanInteger(ordinal, 1));
    const zeroBased = safeOrdinal - 1;
    const prefixIndex = Math.floor(zeroBased / maxPerLetter);
    const number = (zeroBased % maxPerLetter) + 1;
    return `${indexToPrefix(prefixIndex)}${String(number).padStart(6, "0")}`;
  };
  const rows = db.prepare("SELECT folio FROM quotations").all();
  const lastSeq = rows.reduce((max, row) => {
    const value = cleanText(row.folio);
    const modernOrdinal = folioToOrdinal(value);
    if(modernOrdinal) return Math.max(max, modernOrdinal);
    const legacy = value.match(/^COT-\d{8}-(\d{5,})$/i);
    if(legacy) return Math.max(max, cleanInteger(legacy[1], 0));
    return max;
  }, 0);
  for(let seq = lastSeq + 1; seq < lastSeq + 10000; seq += 1) {
    const folio = ordinalToFolio(seq);
    const exists = db.prepare("SELECT 1 AS existsFlag FROM quotations WHERE folio = ?").get(folio);
    if(!exists) return folio;
  }
  return ordinalToFolio(lastSeq + 10000);
}

function normalizeQuotationStoredText(text = "", folio = "") {
  const clean = cleanText(text);
  if(!clean || !folio) return clean;
  if(clean.includes(folio)) return clean;
  const replaced = clean.replace(/(?:COT-\d{8}(?:-\d{5,})?|[A-Z]{1,2}\d{6,})/gi, folio);
  return replaced === clean ? clean.replace(/FOLIO:\s*[^\n\r]+/i, `FOLIO: ${folio}`) : replaced;
}

function createQuotation(input, sessionContext = null) {
  cleanupExpiredQuotations();
  const cart = Array.isArray(input.cart) ? input.cart : [];
  if(!cart.length) throw httpError(400, "La cotizacion no tiene productos.");
  const date = cleanText(input.date, new Date().toLocaleString("es-MX"));
  const expiresAt = cleanText(input.expiresAt || input.expires_at);
  const cashier = cleanText(input.cashier || sessionContext?.user?.name || "");
  const format = cleanText(input.format, "ticket") === "letter" ? "letter" : "ticket";
  const inputQuoteText = cleanText(input.quoteText || input.quote_text);
  const total = cleanNumber(input.total);
  const customerId = cleanInteger(input.customerId || input.customer_id, 0);
  const customerName = cleanText(input.customerName || input.customer_name);
  if(total <= 0) throw httpError(400, "La cotizacion no puede tener total en cero.");
  let folio = generateUniqueQuotationFolio();
  let quoteText = normalizeQuotationStoredText(inputQuoteText, folio);
  const insert = db.prepare(`
    INSERT INTO quotations(folio, date, expires_at, cashier, total, format, status, customer_id, customer_name, cart_json, quote_text, created_at)
    VALUES (?, ?, ?, ?, ?, ?, 'Activa', ?, ?, ?, ?, ?)
  `);
  for(let attempt = 0; attempt < 5; attempt += 1) {
    try {
      insert.run(folio, date, expiresAt, cashier, total, format, customerId, customerName, jsonString(cart), quoteText, nowIso());
      return rowToQuotation(db.prepare("SELECT * FROM quotations WHERE folio = ?").get(folio));
    } catch(error) {
      if(!String(error?.message || "").toLowerCase().includes("unique")) throw error;
      folio = generateUniqueQuotationFolio();
      quoteText = normalizeQuotationStoredText(inputQuoteText, folio);
    }
  }
  throw httpError(409, "No se pudo generar un folio unico de cotizacion. Intenta de nuevo.");
}

function deleteQuotation(input = {}, sessionContext = null) {
  const id = cleanInteger(input.id, 0);
  const folio = cleanText(input.folio);
  const quote = id
    ? db.prepare("SELECT * FROM quotations WHERE id = ?").get(id)
    : db.prepare("SELECT * FROM quotations WHERE folio = ?").get(folio);
  if(!quote) throw httpError(404, "Cotizacion no encontrada.");
  db.prepare("DELETE FROM quotations WHERE id = ?").run(quote.id);
  logAuditEvent({
    type: "Cotizacion borrada",
    severity: "Media",
    reference: quote.folio,
    amount: cleanNumber(quote.total),
    details: `Se borro la cotizacion ${quote.folio}.`,
    payload: { folio: quote.folio, total: quote.total, cashier: quote.cashier }
  }, sessionContext);
  return { id: quote.id, folio: quote.folio };
}


function updateQuotationStatus(input = {}, sessionContext = null) {
  const id = cleanInteger(input.id, 0);
  const folio = cleanText(input.folio);
  const status = cleanText(input.status, "Procesada");
  const allowed = new Set(["Activa", "Procesada", "Vencida", "Cancelada"]);
  if(!allowed.has(status)) throw httpError(400, "Estado de cotizacion no valido.");
  const quote = id
    ? db.prepare("SELECT * FROM quotations WHERE id = ?").get(id)
    : db.prepare("SELECT * FROM quotations WHERE folio = ?").get(folio);
  if(!quote) throw httpError(404, "Cotizacion no encontrada.");
  const quoteText = cleanText(input.quoteText || input.quote_text);
  if(quoteText) db.prepare("UPDATE quotations SET status = ?, quote_text = ? WHERE id = ?").run(status, quoteText, quote.id);
  else db.prepare("UPDATE quotations SET status = ? WHERE id = ?").run(status, quote.id);
  logAuditEvent({
    type: "Cotizacion actualizada",
    severity: "Baja",
    reference: quote.folio,
    amount: cleanNumber(quote.total),
    details: `Cotizacion ${quote.folio} marcada como ${status}.`,
    payload: { folio: quote.folio, status }
  }, sessionContext);
  return rowToQuotation(db.prepare("SELECT * FROM quotations WHERE id = ?").get(quote.id));
}

function cancelSale(input, sessionContext) {
  const saleId = cleanInteger(input.saleId || input.id, 0);
  const folio = cleanText(input.folio);
  const action = cleanText(input.action) === "return" ? "return" : "cancel";
  const requiredPermission = action === "return" ? "returnSales" : "cancelSales";
  const authorization = authorizeSaleHistoryAction(sessionContext, input, requiredPermission);
  const reason = cleanText(input.reason, action === "return" ? "Devolucion de cliente" : "Cancelacion de venta");
  const cashier = sessionContext?.user?.name || cleanText(input.cashier);
  const cancelSessionId = sessionContext?.cashSession?.id || cleanText(input.sessionId);
  const cancelledAt = new Date().toLocaleString("es-MX");
  const requestedReturnLines = Array.isArray(input.returnLines) ? input.returnLines : [];
  const isPartialReturn = action === "return" && requestedReturnLines.length > 0;
  const refundMethod = normalizeRefundMethod(input.refundMethod || input.refund_method || "cash");

  db.exec("BEGIN IMMEDIATE");
  try {
    const sale = saleId
      ? db.prepare("SELECT * FROM sales WHERE id = ?").get(saleId)
      : db.prepare("SELECT * FROM sales WHERE folio = ?").get(folio);
    if(!sale) throw httpError(404, "Venta no encontrada.");
    if((sale.status || "Completada") !== "Completada") throw httpError(409, "La venta ya fue cancelada o devuelta.");

    const cutLocked = saleHasRegisteredCut(sale);
    if(action === "cancel" && cutLocked) {
      throw httpError(409, "Este ticket ya pertenece a un corte cerrado. No se puede cancelar; usa Devolver productos con autorización para no alterar el corte anterior.");
    }

    const items = db.prepare("SELECT * FROM sale_items WHERE sale_id = ?").all(sale.id);
    if(!items.length) throw httpError(409, "La venta no tiene detalle de productos.");

    const updateStock = db.prepare("UPDATE products SET stock = stock + ?, updated_at = ? WHERE sku = ?");
    const productBySku = db.prepare("SELECT sku, name, supplier, stock FROM products WHERE sku = ?");
    const labelByCode = db.prepare("SELECT * FROM department_labels WHERE code = ?");
    const departmentProductBySku = db.prepare("SELECT sku, name, department, stock FROM department_inventory WHERE sku = ?");
    const updateDepartmentStock = db.prepare("UPDATE department_inventory SET stock = stock + ?, updated_at = ? WHERE sku = ?");
    const updateDepartmentLabelStatus = db.prepare("UPDATE department_labels SET status = ? WHERE code = ?");
    const insertAdjustment = db.prepare(`
      INSERT INTO inventory_adjustments(date, sku, product_name, supplier, type, quantity, delta, before_stock, after_stock, reference, reason)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

    const requestByItemId = new Map();
    const requestBySku = new Map();
    requestedReturnLines.forEach(line => {
      const quantity = cleanNumber(line.quantity || line.qty);
      if(quantity <= 0) return;
      const itemId = cleanInteger(line.saleItemId || line.itemId || line.id, 0);
      const sku = cleanText(line.sku).toUpperCase();
      if(itemId) requestByItemId.set(itemId, (requestByItemId.get(itemId) || 0) + quantity);
      if(sku) requestBySku.set(sku, (requestBySku.get(sku) || 0) + quantity);
    });

    const selectedItems = [];
    items.forEach(item => {
      const originalQty = cleanNumber(item.quantity, 0);
      if(originalQty <= 0) return;
      let returnQty = originalQty;
      if(isPartialReturn) {
        if(requestByItemId.has(item.id)) {
          returnQty = requestByItemId.get(item.id);
        } else {
          const itemSku = cleanText(item.sku).toUpperCase();
          const remainingBySku = requestBySku.get(itemSku) || 0;
          returnQty = Math.min(remainingBySku, originalQty);
          if(remainingBySku > 0) requestBySku.set(itemSku, Math.max(remainingBySku - returnQty, 0));
        }
      }
      returnQty = Math.min(Math.max(cleanNumber(returnQty), 0), originalQty);
      if(returnQty > 0) selectedItems.push({ item, returnQty, originalQty });
    });

    if(!selectedItems.length) throw httpError(400, "Selecciona al menos un producto y cantidad para devolver.");

    const restoreSaleItem = (item, restoreQty, status) => {
      const label = labelByCode.get(item.sku);
      if(label) {
        if(restoreQty < 1) throw httpError(409, "Las etiquetas internas solo pueden devolverse completas.");
        const lines = jsonParse(label.lines_json, []);
        const restoreLines = Array.isArray(lines) && lines.length
          ? lines
          : [{ sku: label.sku, name: label.product_name, quantity: label.quantity }];
        restoreLines.forEach(line => {
          const lineSku = cleanDepartmentSku(line.sku);
          const departmentProduct = departmentProductBySku.get(lineSku);
          const beforeStock = departmentProduct ? cleanNumber(departmentProduct.stock, 0) : 0;
          const quantity = cleanNumber(line.quantity, 0);
          const afterStock = beforeStock + quantity;
          if(departmentProduct) updateDepartmentStock.run(quantity, nowIso(), lineSku);
          insertAdjustment.run(cancelledAt, lineSku, cleanText(line.name, label.product_name), "Etiqueta interna",
            action === "return" ? "devolucion" : "cancelacion", quantity, quantity, beforeStock, afterStock, sale.folio, `${status}: ${reason}`);
        });
        updateDepartmentLabelStatus.run(status, item.sku);
        return;
      }

      const bundleItems = normalizeBundleItems(jsonParse(item.bundle_json, []));
      const restoreItems = bundleItems.length
        ? bundleItems.map(component => ({ sku: component.sku, quantity: component.quantity * restoreQty, packageName: item.name }))
        : [{ sku: item.sku, quantity: restoreQty, packageName: "" }];
      restoreItems.forEach(component => {
        const product = productBySku.get(component.sku);
        const beforeStock = product ? cleanNumber(product.stock, 0) : 0;
        const afterStock = beforeStock + component.quantity;
        updateStock.run(component.quantity, nowIso(), component.sku);
        insertAdjustment.run(cancelledAt, component.sku, product ? product.name : item.name, product ? product.supplier : "",
          action === "return" ? "devolucion" : "cancelacion", component.quantity, component.quantity, beforeStock, afterStock, sale.folio,
          `${status}: ${reason}${component.packageName ? ` | Paquete: ${component.packageName}` : ""}`);
      });
    };

    const originalTotal = Math.max(cleanNumber(sale.total), 0);
    const returnedAmount = cleanMoney(selectedItems.reduce((sum, entry) => {
      const lineTotal = cleanNumber(entry.item.line_total, cleanNumber(entry.item.price) * entry.originalQty);
      return sum + (lineTotal / Math.max(entry.originalQty, 1)) * entry.returnQty;
    }, 0));
    const requestedRefundAmount = cleanMoney(input.refundAmount ?? input.refund_amount ?? returnedAmount);
    const refundAmount = cleanMoney(Math.max(0, Math.min(requestedRefundAmount, returnedAmount)));
    const remainingTotal = cleanMoney(Math.max(originalTotal - returnedAmount, 0));
    const returnRatio = originalTotal > 0 ? Math.min(returnedAmount / originalTotal, 1) : 1;
    const remainingRatio = originalTotal > 0 ? Math.max(remainingTotal / originalTotal, 0) : 0;
    const isFullReturn = remainingTotal <= 0.009 || selectedItems.length === items.length && selectedItems.every(entry => entry.returnQty >= entry.originalQty - 0.0001);
    const status = action === "return" ? (isFullReturn ? "Devuelta" : "Completada") : "Cancelada";
    const salePaymentsOriginal = jsonParse(sale.payments_json, {});
    const exchangeRate = cleanNumber(salePaymentsOriginal.exchangeRate, cleanNumber(getSettings().defaultExchangeRate, 0));

    selectedItems.forEach(entry => restoreSaleItem(entry.item, entry.returnQty, action === "return" ? (isFullReturn ? "Devuelta" : "Devolucion parcial") : "Cancelada"));

    if(isPartialReturn && !isFullReturn) {
      const updateItem = db.prepare("UPDATE sale_items SET quantity = ?, line_total = ? WHERE id = ?");
      const deleteItem = db.prepare("DELETE FROM sale_items WHERE id = ?");
      selectedItems.forEach(entry => {
        const nextQty = cleanNumber(entry.originalQty - entry.returnQty);
        if(nextQty <= 0.0001) deleteItem.run(entry.item.id);
        else updateItem.run(nextQty, cleanMoney(cleanNumber(entry.item.price) * nextQty), entry.item.id);
      });
      const salePayments = refundMethod === "none"
        ? { ...salePaymentsOriginal }
        : applyRefundToPaymentBreakdown(salePaymentsOriginal, refundMethod, refundAmount, exchangeRate);
      const nextPaid = refundMethod === "none" ? cleanNumber(sale.paid) : cleanMoney(Math.max(cleanNumber(sale.paid) - refundAmount, 0));
      const nextChange = refundMethod === "cash" ? cleanMoney(Math.max(cleanNumber(sale.change_amount) - refundAmount, 0)) : cleanNumber(sale.change_amount);
      const nextLoyaltyPoints = Math.max(0, Math.floor(cleanNumber(sale.loyalty_points) * remainingRatio));
      db.prepare(`
        UPDATE sales
        SET total = ?, paid = ?, change_amount = ?, payments_json = ?, loyalty_points = ?,
            cancel_reason = ?, cancelled_at = ?, cancelled_by = ?, cancelled_session_id = ?
        WHERE id = ?
      `).run(
        remainingTotal, nextPaid, nextChange, jsonString(salePayments), nextLoyaltyPoints,
        `Devolucion parcial: ${reason} | Importe devuelto ${returnedAmount.toFixed(2)} | Método ${refundMethod} | Autorizó ${authorization.user.name}`,
        cancelledAt, cashier, cancelSessionId, sale.id
      );
    } else {
      db.prepare(`
        UPDATE sales
        SET status = ?, cancel_reason = ?, cancelled_at = ?, cancelled_by = ?, cancelled_session_id = ?
        WHERE id = ?
      `).run(status, `${reason} | Método ${refundMethod} | Autorizó ${authorization.user.name}`, cancelledAt, cashier, cancelSessionId, sale.id);
    }

    const saleCustomerId = cleanInteger(sale.customer_id, 0);
    if(saleCustomerId) {
      const creditAmount = cleanMoney(cleanNumber(salePaymentsOriginal.credit) * (isPartialReturn && !isFullReturn ? returnRatio : 1));
      const redeemedPoints = Math.max(0, cleanMoney(cleanNumber(salePaymentsOriginal.points) * (isPartialReturn && !isFullReturn ? returnRatio : 1)));
      const awardedPoints = Math.max(0, Math.floor(cleanNumber(sale.loyalty_points, 0) * (isPartialReturn && !isFullReturn ? returnRatio : 1)));
      if(creditAmount > 0) {
        db.prepare("UPDATE customers SET balance = MAX(balance - ?, 0), updated_at = ? WHERE id = ?").run(creditAmount, nowIso(), saleCustomerId);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'cancelacion_credito', ?, 0, 'Credito', ?, ?, ?)")
          .run(saleCustomerId, cancelledAt, creditAmount, sale.folio, `${status}: se reversa venta a credito`, cashier);
      }
      if(redeemedPoints > 0) {
        db.prepare("UPDATE customers SET loyalty_points = loyalty_points + ?, updated_at = ? WHERE id = ?").run(redeemedPoints, nowIso(), saleCustomerId);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'devolucion_puntos', ?, ?, 'Puntos', ?, ?, ?)")
          .run(saleCustomerId, cancelledAt, redeemedPoints, redeemedPoints, sale.folio, `${status}: se regresan puntos redimidos`, cashier);
      }
      if(awardedPoints > 0) {
        db.prepare("UPDATE customers SET loyalty_points = MAX(loyalty_points - ?, 0), updated_at = ? WHERE id = ?").run(awardedPoints, nowIso(), saleCustomerId);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'reversa_puntos', 0, ?, 'Lealtad', ?, ?, ?)")
          .run(saleCustomerId, cancelledAt, -awardedPoints, sale.folio, `${status}: se descuentan puntos otorgados`, cashier);
      }
      if(action === "return" && refundMethod === "points" && refundAmount > 0) {
        db.prepare("UPDATE customers SET loyalty_points = loyalty_points + ?, updated_at = ? WHERE id = ?").run(Math.floor(refundAmount), nowIso(), saleCustomerId);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'reembolso_puntos', ?, ?, 'Puntos', ?, ?, ?)")
          .run(saleCustomerId, cancelledAt, refundAmount, Math.floor(refundAmount), sale.folio, "Reembolso de devolucion en puntos", cashier);
      }
      if(action === "return" && refundMethod === "credit" && refundAmount > 0) {
        db.prepare("UPDATE customers SET balance = MAX(balance - ?, 0), updated_at = ? WHERE id = ?").run(refundAmount, nowIso(), saleCustomerId);
        db.prepare("INSERT INTO customer_movements(customer_id, date, type, amount, points, method, reference, notes, cashier) VALUES (?, ?, 'saldo_devolucion', ?, 0, 'Credito', ?, ?, ?)")
          .run(saleCustomerId, cancelledAt, refundAmount, sale.folio, "Reembolso aplicado a saldo/credito del cliente", cashier);
      }
    }

    if(action === "return" && refundMethod === "cash" && refundAmount > 0 && cancelSessionId && (sale.session_id !== cancelSessionId || cutLocked)) {
      db.prepare(`
        INSERT INTO cash_movements(session_id, type, amount, method, concept, reference, cashier, date, client_request_id)
        VALUES (?, 'payment', ?, 'Efectivo', ?, ?, ?, ?, ?)
      `).run(cancelSessionId, refundAmount, `Devolucion efectivo venta ${sale.folio}`, sale.folio, cashier, cancelledAt, `RETURN-${sale.id}-${Date.now()}`);
    }

    db.exec("COMMIT");
    logAuditEvent({
      type: action === "return" ? (isPartialReturn && !isFullReturn ? "Devolucion parcial de venta" : "Devolucion de venta") : "Cancelacion de venta",
      severity: cutLocked ? "Critica" : "Alta",
      reference: sale.folio,
      amount: action === "return" ? returnedAmount : cleanNumber(sale.total),
      details: `${action === "return" ? (isFullReturn ? "Devuelta" : "Devolucion parcial") : "Cancelada"} por ${cashier || "S/N"}. Autorizo ${authorization.user.name}. Motivo: ${reason}.`,
      payload: {
        saleId: sale.id, folio: sale.folio, total: sale.total, returnedAmount, refundAmount, refundMethod, remainingTotal, reason, action,
        partial: Boolean(isPartialReturn && !isFullReturn), cutLocked,
        authorization: { user: authorization.user.name, method: authorization.method },
        returnLines: selectedItems.map(entry => ({ saleItemId: entry.item.id, sku: entry.item.sku, name: entry.item.name, quantity: entry.returnQty, amount: cleanMoney((cleanNumber(entry.item.line_total) / Math.max(entry.originalQty, 1)) * entry.returnQty) }))
      }
    }, sessionContext);
    return {
      id: sale.id, folio: sale.folio, status, partial: Boolean(isPartialReturn && !isFullReturn), returnedAmount, refundAmount, refundMethod,
      cutLocked, authorizedBy: authorization.user.name, reason,
      returnLines: selectedItems.map(entry => ({ saleItemId: entry.item.id, sku: entry.item.sku, name: entry.item.name, quantity: entry.returnQty, amount: cleanMoney((cleanNumber(entry.item.line_total) / Math.max(entry.originalQty, 1)) * entry.returnQty) }))
    };
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function getPurchasePolicy(settings = null) {
  const source = settings || getSettings();
  const rawInventoryMode = cleanText(source.purchaseInventoryApplyMode || "provisional");
  const inventoryApplyMode = ["onReceive", "provisional", "afterReview"].includes(rawInventoryMode) ? rawInventoryMode : "provisional";
  return {
    notesUsersCanCreateOrders: source.purchaseNotesUsersCanCreateOrders !== false,
    notesUsersCanReceiveOrders: source.purchaseNotesUsersCanReceiveOrders !== false,
    receiptsPendingReview: source.purchaseReceiptsPendingReview !== false,
    inventoryApplyMode,
    costChangeRequiresAdmin: source.purchaseCostChangeRequiresAdmin === true || source.purchaseCostChangeRequiresAdmin === 1 || source.purchaseCostChangeRequiresAdmin === "true"
  };
}

function userCanManagePurchaseDocuments(user) {
  return userCanAccessAny(user, ["admin", "inventory", "purchaseOrders", "purchaseNotes", "shoppingList"]);
}

function requirePurchaseDocumentAction(action, sessionContext = null, input = {}) {
  const user = sessionContext?.user || null;
  if(!user) throw httpError(401, "Sesion requerida.");
  if(userHasPermission(user, "admin") || userHasPermission(user, "inventory")) return;
  const policy = getPurchasePolicy();
  const mode = cleanText(input.mode, "manual");
  const isOrderMode = ["pedido", "pedido_borrador", "order", "pedido_revision"].includes(mode);
  if(action === "create") {
    if(isOrderMode) {
      if(userHasPermission(user, "purchaseOrders")) return;
      if(policy.notesUsersCanCreateOrders && userHasPermission(user, "purchaseNotes")) return;
      throw httpError(403, "Tu usuario no puede crear pedidos. Activa esta opcion en Configuracion > Pedidos/recepciones o usa un usuario autorizado.");
    }
    if(userHasPermission(user, "purchaseNotes") || userHasPermission(user, "purchaseOrders")) return;
  }
  if(action === "receive") {
    if(userHasPermission(user, "purchaseOrders")) return;
    if(policy.notesUsersCanReceiveOrders && userHasPermission(user, "purchaseNotes")) return;
    throw httpError(403, "Tu usuario no puede recibir pedidos. Activa esta opcion en Configuracion > Pedidos/recepciones o usa un usuario autorizado.");
  }
  if(action === "approve") {
    requirePermission(user, "admin");
    return;
  }
  throw httpError(403, "Permiso de compras requerido.");
}

function shouldApplyPurchaseInventoryNow(policy, sessionContext = null) {
  if(policy.inventoryApplyMode === "afterReview") return false;
  return true;
}

function getPurchaseInventoryStatusAfterReceive(policy, options = {}) {
  const applyNow = options.applyNow !== false;
  if(!applyNow) return "pending_review";
  if(options.quickReceive || policy.inventoryApplyMode === "provisional") return "provisional";
  if(policy.receiptsPendingReview && options.byNonAdmin) return "provisional";
  return "applied";
}

function getReceiptReviewStatus(policy, hasSensitiveChange = false, inventoryStatus = "applied") {
  if(policy.receiptsPendingReview || hasSensitiveChange || inventoryStatus === "pending_review" || inventoryStatus === "provisional") return "pending_review";
  return "reviewed";
}

function extractStoredReceiptLines(row = {}) {
  const receipt = jsonParse(row.receipt_lines_json, []);
  if(Array.isArray(receipt) && receipt.length) return receipt;
  return jsonParse(row.lines_json, []);
}

function getPurchaseLineKey(line = {}) {
  return cleanText(line.sku || line.matchedProductSku || line.description || line.name).toUpperCase();
}

function summarizePurchaseReceiptDifferences(orderedLines = [], receivedLines = []) {
  const ordered = new Map();
  const received = new Map();
  const add = (map, line) => {
    if(!line || line.meta || line.sku === "__NOTA_META__") return;
    const key = getPurchaseLineKey(line);
    if(!key) return;
    const current = map.get(key) || { sku: key, description: cleanText(line.description || line.name || key), quantity: 0 };
    current.quantity += cleanNumber(line.quantity);
    map.set(key, current);
  };
  orderedLines.forEach(line => add(ordered, line));
  receivedLines.forEach(line => add(received, line));
  const differences = [];
  const keys = new Set([...ordered.keys(), ...received.keys()]);
  keys.forEach(key => {
    const o = ordered.get(key);
    const r = received.get(key);
    const orderedQty = cleanNumber(o?.quantity);
    const receivedQty = cleanNumber(r?.quantity);
    if(!o && r) differences.push({ type: "extra", sku: key, description: r.description, orderedQty: 0, receivedQty, delta: receivedQty });
    else if(o && !r) differences.push({ type: "missing", sku: key, description: o.description, orderedQty, receivedQty: 0, delta: -orderedQty });
    else if(orderedQty !== receivedQty) differences.push({ type: "quantity", sku: key, description: r.description || o.description, orderedQty, receivedQty, delta: receivedQty - orderedQty });
  });
  return differences;
}

function logPurchaseReceiptAudit({ purchaseNoteId, supplierName, folio, receiptFolio, total, sourceOrder, differences = [], priceChanges = [], reviewStatus = "", inventoryStatus = "", direct = false }, sessionContext = null) {
  const hasDifferences = Array.isArray(differences) && differences.length > 0;
  const hasPriceChanges = Array.isArray(priceChanges) && priceChanges.length > 0;
  if(reviewStatus === "pending_review" || hasDifferences || direct) {
    const parts = [];
    if(direct) parts.push("recepcion directa");
    if(hasDifferences) parts.push(`${differences.length} diferencia(s)`);
    if(inventoryStatus === "pending_review") parts.push("inventario pendiente de aplicar");
    if(inventoryStatus === "provisional") parts.push("inventario aplicado provisionalmente");
    logAuditEvent({
      type: hasDifferences ? "Recepcion con diferencias" : "Recepcion pendiente de revision",
      severity: hasDifferences || inventoryStatus === "pending_review" || inventoryStatus === "provisional" ? "Alta" : "Media",
      reference: cleanText(folio || receiptFolio || purchaseNoteId),
      amount: cleanNumber(total),
      details: `${sourceOrder ? "Pedido recibido" : "Recepcion directa"} de ${supplierName || "sin proveedor"}${parts.length ? `: ${parts.join(", ")}` : ""}.`,
      payload: {
        action: "purchaseReceiptReview",
        purchaseNoteId,
        supplier: supplierName,
        folio,
        receiptFolio,
        source: sourceOrder ? "order_receipt" : "direct_receipt",
        reviewStatus,
        inventoryStatus,
        differences,
        priceChanges
      }
    }, sessionContext);
  }
}

function shouldApplyDeferredPurchaseInventoryStatus(status) {
  return ["", "pending_review", "pending_apply"].includes(cleanText(status));
}

function getReceiptLinesFromReviewInput(input = {}) {
  const candidates = [input.correctedLines, input.corrected_lines, input.receiptLines, input.receipt_lines, input.lines];
  for(const candidate of candidates) {
    if(Array.isArray(candidate) && candidate.length) return normalizePurchaseLines(candidate);
  }
  return [];
}

function buildPurchaseLineQuantityMap(lines = []) {
  const map = new Map();
  lines.forEach((line, index) => {
    if(!line || line.meta || line.sku === "__NOTA_META__") return;
    const sku = cleanText(line.sku || line.matchedProductSku || makeSku(line.description, index)).toUpperCase();
    if(!sku) return;
    const current = map.get(sku) || { sku, description: cleanText(line.description || line.name || sku), quantity: 0, line };
    current.quantity += Math.max(0, cleanNumber(line.quantity));
    current.line = { ...current.line, ...line, sku, description: cleanText(line.description || line.name || current.description || sku) };
    map.set(sku, current);
  });
  return map;
}

function applyProvisionalPurchaseReceiptCorrection(rowOrId, correctedLines = [], sessionContext = null) {
  const row = typeof rowOrId === "object" ? rowOrId : db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(cleanInteger(rowOrId));
  if(!row) throw httpError(404, "Recepcion no encontrada.");
  const normalizedCorrected = normalizePurchaseLines(correctedLines);
  if(!normalizedCorrected.length) return false;
  const previousLinesForStorage = extractStoredReceiptLines(row).filter(line => !line?.meta && line?.sku !== "__NOTA_META__");
  const previous = buildPurchaseLineQuantityMap(normalizePurchaseLines(previousLinesForStorage));
  const next = buildPurchaseLineQuantityMap(normalizedCorrected);
  const keys = new Set([...previous.keys(), ...next.keys()]);
  const supplierName = cleanText(row.supplier, "Proveedor general");
  const noteReference = cleanText(row.receipt_folio || row.folio) || `Recepcion ${row.id}`;
  const date = cleanText(row.date, new Date().toLocaleDateString("es-MX"));
  const insertAdjustment = db.prepare(`
    INSERT INTO inventory_adjustments(date, sku, product_name, supplier, type, quantity, delta, before_stock, after_stock, reference, reason, client_request_id)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  keys.forEach((sku, index) => {
    const prev = previous.get(sku);
    const nxt = next.get(sku);
    const prevQty = cleanNumber(prev?.quantity);
    const nextQty = cleanNumber(nxt?.quantity);
    const delta = nextQty - prevQty;
    if(delta === 0) return;
    const line = nxt?.line || prev?.line || { sku, description: sku };
    const existing = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
    const quantity = Math.abs(delta);
    if(existing) {
      const beforeStock = cleanNumber(existing.stock);
      const afterStock = Math.max(0, beforeStock + delta);
      db.prepare("UPDATE products SET stock = ?, updated_at = ? WHERE sku = ?").run(afterStock, nowIso(), sku);
      insertAdjustment.run(date, sku, existing.name, existing.supplier, delta > 0 ? "purchase_correction" : "purchase_correction_reverse", quantity, delta, beforeStock, afterStock, noteReference, `Ajuste por revision de recepcion provisional ${noteReference}`, `${row.client_request_id || `REVIEW-${row.id}`}-CORR-${index}`);
    } else if(delta > 0) {
      upsertProduct({
        sku,
        name: cleanText(line.description || line.name || sku),
        supplier: supplierName,
        category: "General",
        stock: delta,
        minimum: 1,
        costPrice: cleanNumber(line.finalCost || line.final_cost || line.cost),
        salePrice: cleanNumber(line.sale),
        wholesalePrice: cleanNumber(line.wholesalePrice || line.wholesale_price),
        wholesalePrice2: cleanNumber(line.wholesalePrice2 || line.wholesale_price_2),
        wholesaleQty1: cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1),
        wholesaleQty2: cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2),
        location: "S/U",
        aliases: line.ocrAlias ? [line.ocrAlias] : [],
        supplierAliases: {}
      });
      insertAdjustment.run(date, sku, cleanText(line.description || line.name || sku), supplierName, "purchase_correction", delta, delta, 0, delta, noteReference, `Alta por correccion de recepcion provisional ${noteReference}`, `${row.client_request_id || `REVIEW-${row.id}`}-CORR-${index}`);
    }
  });
  const total = normalizedCorrected.reduce((sum, line) => sum + cleanNumber(line.quantity) * cleanNumber(line.finalCost || line.final_cost || line.cost), 0);
  db.prepare(`
    UPDATE purchase_notes SET
      receipt_lines_json = ?,
      lines_json = ?,
      line_count = ?,
      receipt_total = ?,
      total = CASE WHEN source IN ('order_receipt','direct_receipt') OR mode IN ('manual','pedido_recibido') THEN ? ELSE total END
    WHERE id = ?
  `).run(jsonString(normalizedCorrected), jsonString(normalizedCorrected), normalizedCorrected.length, total, total, row.id);
  logAuditEvent({
    type: "Recepcion provisional corregida",
    severity: "Media",
    reference: noteReference,
    amount: total,
    details: `Se ajusto inventario provisional de recepcion ${noteReference} durante revision admin.`,
    payload: { action: "purchaseReceiptCorrection", purchaseNoteId: row.id, correctedLines: normalizedCorrected }
  }, sessionContext);
  return true;
}

function getPurchaseLineEditFlags(line = {}) {
  const raw = line.priceEditFlags || line.price_edit_flags || line.editedFields || line.edited_fields || {};
  if(typeof raw === "string") return jsonParse(raw, {});
  return raw && typeof raw === "object" ? raw : {};
}

function purchaseLineFieldWasEdited(line = {}, ...fields) {
  const flags = getPurchaseLineEditFlags(line);
  return fields.some(field => {
    const value = flags[field] ?? flags[field.toLowerCase()] ?? line[`${field}Edited`] ?? line[`_${field}Edited`];
    return value === true || value === 1 || value === "1" || value === "true";
  });
}

function getPurchasePriceEditPermissions(sessionContext = null) {
  const user = sessionContext?.user;
  const canEditSalePrices = userHasPermission(user, "admin") || userHasPermission(user, "purchaseEditSalePrice");
  const canEditWholesalePrices = canEditSalePrices && (userHasPermission(user, "admin") || userHasPermission(user, "purchaseEditWholesalePrice"));
  return {
    canEditCost: userHasPermission(user, "admin") || userHasPermission(user, "purchaseEditCost"),
    canEditSalePrices,
    canEditWholesale1: canEditWholesalePrices,
    canEditWholesale2: canEditWholesalePrices
  };
}

function listCashierTaskSummaries(sessionContext = null) {
  return db.prepare("SELECT t.*, (SELECT COUNT(*) FROM task_attachments a WHERE a.task_id = t.id) AS attachment_count FROM cashier_tasks t ORDER BY t.id")
    .all()
    .filter(row => !sessionContext || taskVisibleForUser(row, sessionContext.user))
    .map(row => rowToTask(row, false));
}

function getCashierTaskDetail(taskId, sessionContext) {
  const row = db.prepare("SELECT t.*, (SELECT COUNT(*) FROM task_attachments a WHERE a.task_id = t.id) AS attachment_count FROM cashier_tasks t WHERE t.id = ?").get(cleanInteger(taskId));
  if(!row) throw httpError(404, "No encontre la tarea.");
  if(!taskVisibleForUser(row, sessionContext?.user)) throw httpError(403, "Esta tarea no esta asignada a tu usuario.");
  return rowToTask(row, true);
}

function collectNegativeUtilityWarning(warnings, line, productName, supplierName, cost, salePrice, wholesalePrice, wholesalePrice2) {
  const safeCost = cleanNumber(cost);
  if(!warnings || safeCost <= 0) return;
  const issues = [];
  const sale = cleanNumber(salePrice);
  const wholesale = cleanNumber(wholesalePrice);
  const wholesale2 = cleanNumber(wholesalePrice2);
  if(sale > 0 && sale < safeCost) issues.push({ field: "venta", price: sale, cost: safeCost });
  if(wholesale > 0 && wholesale < safeCost) issues.push({ field: "mayoreo1", price: wholesale, cost: safeCost });
  if(wholesale2 > 0 && wholesale2 < safeCost) issues.push({ field: "mayoreo2", price: wholesale2, cost: safeCost });
  if(!issues.length) return;
  warnings.push({
    sku: cleanText(line.sku),
    productName: cleanText(productName || line.description || line.name),
    supplier: cleanText(supplierName),
    cost: safeCost,
    salePrice: sale,
    wholesalePrice: wholesale,
    wholesalePrice2: wholesale2,
    issues
  });
}

function applyDeferredPurchaseInventory(rowOrId, sessionContext = null) {
  const row = typeof rowOrId === "object" ? rowOrId : db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(cleanInteger(rowOrId));
  if(!row) throw httpError(404, "Recepcion no encontrada.");
  if(cleanText(row.inventory_status) === "applied" || cleanText(row.inventory_status) === "provisional") return false;
  const linesForStorage = extractStoredReceiptLines(row);
  const lines = normalizePurchaseLines(linesForStorage.filter(line => !line?.meta && line?.sku !== "__NOTA_META__"));
  if(!lines.length) {
    db.prepare("UPDATE purchase_notes SET inventory_status = ? WHERE id = ?").run("applied", row.id);
    return false;
  }
  const metaRows = linesForStorage.filter(line => line?.meta || line?.sku === "__NOTA_META__");
  const exchangePurchaseLines = normalizePurchaseLines(metaRows.flatMap(line => line.exchangePurchaseLines || line.exchange_purchase_lines || []));
  const noteReference = cleanText(row.receipt_folio || row.folio) || `Recepcion ${row.id}`;
  const supplierName = cleanText(row.supplier, "Proveedor general");
  const date = cleanText(row.date, new Date().toLocaleDateString("es-MX"));
  const { canEditCost, canEditSalePrices, canEditWholesale1, canEditWholesale2 } = getPurchasePriceEditPermissions(sessionContext);
  const negativeUtilityWarnings = [];
  const insertAdjustment = db.prepare(`
    INSERT INTO inventory_adjustments(date, sku, product_name, supplier, type, quantity, delta, before_stock, after_stock, reference, reason, client_request_id)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  lines.forEach((line, index) => {
    const sku = cleanText(line.sku, makeSku(line.description, index)).toUpperCase();
    const quantity = Math.max(0, cleanNumber(line.quantity));
    if(!sku || quantity <= 0) return;
    const existing = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
    if(existing) {
      const beforeStock = cleanNumber(existing.stock);
      const afterStock = beforeStock + quantity;
      const requestedCost = cleanNumber(line.finalCost || line.final_cost || line.cost);
      const nextCost = canEditCost && purchaseLineFieldWasEdited(line, "cost", "finalCost", "final_cost") && requestedCost > 0 ? requestedCost : cleanNumber(existing.cost_price);
      const nextSale = canEditSalePrices && purchaseLineFieldWasEdited(line, "sale") && cleanNumber(line.sale) > 0 ? cleanNumber(line.sale) : cleanNumber(existing.sale_price);
      const nextWholesale = canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price") && cleanNumber(line.wholesalePrice || line.wholesale_price) > 0 ? cleanNumber(line.wholesalePrice || line.wholesale_price) : cleanNumber(existing.wholesale_price);
      const nextWholesale2 = canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2") && cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) > 0 ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : cleanNumber(existing.wholesale_price_2);
      const nextWholesaleQty1 = canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesaleQty1", "wholesale_qty_1") && cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) > 0 ? cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) : cleanInteger(existing.wholesale_qty_1);
      const nextWholesaleQty2 = canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesaleQty2", "wholesale_qty_2") && cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) > 0 ? cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) : cleanInteger(existing.wholesale_qty_2);
      collectNegativeUtilityWarning(negativeUtilityWarnings, line, existing.name, existing.supplier, requestedCost || nextCost, nextSale, nextWholesale, nextWholesale2);
      db.prepare(`
        UPDATE products SET
          stock = stock + ?,
          cost_price = CASE WHEN ? > 0 THEN ? ELSE cost_price END,
          sale_price = CASE WHEN ? > 0 THEN ? ELSE sale_price END,
          wholesale_price = CASE WHEN ? > 0 THEN ? ELSE wholesale_price END,
          wholesale_price_2 = CASE WHEN ? > 0 THEN ? ELSE wholesale_price_2 END,
          wholesale_qty_1 = CASE WHEN ? > 0 THEN ? ELSE wholesale_qty_1 END,
          wholesale_qty_2 = CASE WHEN ? > 0 THEN ? ELSE wholesale_qty_2 END,
          updated_at = ?
        WHERE sku = ?
      `).run(quantity, nextCost, nextCost, nextSale, nextSale, nextWholesale, nextWholesale, nextWholesale2, nextWholesale2, nextWholesaleQty1, nextWholesaleQty1, nextWholesaleQty2, nextWholesaleQty2, nowIso(), sku);
      insertAdjustment.run(date, sku, existing.name, existing.supplier, "purchase", quantity, quantity, beforeStock, afterStock, noteReference, `Entrada por revision de recepcion ${noteReference}`, row.client_request_id ? `${row.client_request_id}-REVIEW-${index}` : "");
    } else {
      collectNegativeUtilityWarning(
        negativeUtilityWarnings,
        line,
        cleanText(line.description, sku),
        supplierName,
        cleanNumber(line.finalCost || line.final_cost || line.cost),
        canEditSalePrices && purchaseLineFieldWasEdited(line, "sale") ? cleanNumber(line.sale) : 0,
        canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price") ? cleanNumber(line.wholesalePrice || line.wholesale_price) : 0,
        canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2") ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : 0
      );
      upsertProduct({
        sku,
        name: cleanText(line.description, sku),
        supplier: supplierName,
        category: "General",
        stock: quantity,
        minimum: 1,
        costPrice: canEditCost && purchaseLineFieldWasEdited(line, "cost", "finalCost", "final_cost") ? cleanNumber(line.finalCost || line.final_cost || line.cost) : 0,
        salePrice: canEditSalePrices && purchaseLineFieldWasEdited(line, "sale") ? cleanNumber(line.sale) : 0,
        wholesalePrice: canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price") ? cleanNumber(line.wholesalePrice || line.wholesale_price) : 0,
        wholesalePrice2: canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2") ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : 0,
        wholesaleQty1: canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesaleQty1", "wholesale_qty_1") ? cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) : 0,
        wholesaleQty2: canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesaleQty2", "wholesale_qty_2") ? cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) : 0,
        location: "S/U",
        aliases: line.ocrAlias ? [line.ocrAlias] : [],
        supplierAliases: {}
      });
      insertAdjustment.run(date, sku, cleanText(line.description, sku), supplierName, "purchase", quantity, quantity, 0, quantity, noteReference, `Alta y entrada por revision de recepcion ${noteReference}`, row.client_request_id ? `${row.client_request_id}-REVIEW-${index}` : "");
    }
  });
  if(negativeUtilityWarnings.length) {
    logAuditEvent({
      type: "Recepcion con utilidad negativa",
      severity: "Alta",
      reference: noteReference,
      amount: negativeUtilityWarnings.length,
      details: `La recepcion ${noteReference} contiene ${negativeUtilityWarnings.length} producto(s) con precio menor al costo recibido.`,
      payload: { action: "openPurchaseNote", purchaseNoteId: row.id, supplier: supplierName, folio: noteReference, negativeUtilityWarnings }
    }, sessionContext);
  }
  exchangePurchaseLines.forEach((line, index) => {
    const sku = cleanText(line.sku).toUpperCase();
    const quantity = Math.max(0, cleanNumber(line.quantity));
    if(!sku || quantity <= 0) return;
    const existing = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
    if(!existing) return;
    const beforeStock = cleanNumber(existing.stock);
    const afterStock = Math.max(0, beforeStock - quantity);
    db.prepare("UPDATE products SET stock = ?, updated_at = ? WHERE sku = ?").run(afterStock, nowIso(), sku);
    insertAdjustment.run(date, sku, existing.name, existing.supplier, "exchange_purchase", quantity, -quantity, beforeStock, afterStock, noteReference, `Compra por cambio aplicada al revisar recepcion ${noteReference}`, row.client_request_id ? `${row.client_request_id}-REVIEW-EXCHANGE-${index}` : "");
  });
  db.prepare("UPDATE purchase_notes SET inventory_status = ? WHERE id = ?").run("applied", row.id);
  return true;
}

function createPurchaseNote(input, sessionContext = null) {
  const mode = cleanText(input.mode, "manual");
  const supplierName = cleanText(input.supplier);
  let lines = normalizePurchaseLines(input.lines);
  const physicalExchangeLines = normalizePurchaseLines(input.physicalExchangeLines || input.physical_exchange_lines);
  const exchangePurchaseLines = normalizePurchaseLines(input.exchangePurchaseLines || input.exchange_purchase_lines);
  const sourceOrderId = cleanInteger(input.sourceOrderId || input.source_order_id, 0);
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id);
  const quickReceive = input.quickReceive === true || input.quick_receive === true || cleanText(input.receiveMode || input.receive_mode) === "quick";
  const settings = getSettings();
  const purchasePolicy = getPurchasePolicy(settings);
  const { canEditCost, canEditSalePrices, canEditWholesale1, canEditWholesale2 } = getPurchasePriceEditPermissions(sessionContext);
  const purchasePriceChanges = [];
  const negativeUtilityWarnings = [];

  db.exec("BEGIN IMMEDIATE");
  try {
    if(["manual", "image", "pedido", "pedido_borrador", "order", "pedido_revision"].includes(mode) && !supplierName) {
      throw httpError(400, "Selecciona proveedor.");
    }
    if(clientRequestId) {
      const existing = db.prepare("SELECT id FROM purchase_notes WHERE client_request_id = ?").get(clientRequestId);
      if(existing) {
        db.exec("COMMIT");
        return existing.id;
      }
    }
    if(["pedido", "pedido_borrador", "order", "pedido_revision"].includes(mode)) {
      const previewTotal = Math.max(0, lines.reduce((sum, line) => sum + cleanNumber(line.quantity) * cleanNumber(line.finalCost || line.final_cost || line.cost), 0));
      const duplicate = db.prepare(`
        SELECT id FROM purchase_notes
        WHERE supplier = ?
          AND mode IN ('pedido', 'pedido_borrador', 'order', 'pedido_revision')
          AND status IN ('ordered', 'draft', 'receiving')
          AND line_count = ?
          AND ABS(total - ?) < 0.001
          AND lines_json = ?
          AND created_at >= ?
        ORDER BY id DESC LIMIT 1
      `).get(supplierName, lines.length, previewTotal, jsonString(lines.map(line => ({ ...line }))), new Date(Date.now() - 120000).toISOString());
      if(duplicate) {
        db.exec("COMMIT");
        return duplicate.id;
      }
    }
    let sourceOrder = null;
    if(mode === "manual" && sourceOrderId) {
      sourceOrder = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(sourceOrderId);
      if(!sourceOrder) throw httpError(404, "Pedido origen no encontrado.");
      if(sourceOrder.mode === "pedido_recibido" || getPurchaseDocumentStatus(sourceOrder) === "received") throw httpError(409, "Este pedido ya fue recibido.");
      if(!["pedido_borrador", "pedido", "order", "pedido_revision"].includes(sourceOrder.mode)) throw httpError(409, "Este pedido no esta listo para recibirse.");
    }
    const shouldApplyInventoryNow = mode === "manual" ? shouldApplyPurchaseInventoryNow(purchasePolicy, sessionContext) : false;
    const receiptInventoryStatus = getPurchaseInventoryStatusAfterReceive(purchasePolicy, {
      applyNow: shouldApplyInventoryNow,
      quickReceive,
      byNonAdmin: !userHasPermission(sessionContext?.user, "admin")
    });
    if(mode === "manual") {
      const noteReference = cleanText(input.folio) || `Nota ${new Date().toLocaleDateString("es-MX")}`;
      if(physicalExchangeLines.length && !input.adjustedForPhysicalExchange && !input.adjusted_for_physical_exchange) {
        const exchangeBySku = new Map();
        physicalExchangeLines.forEach(line => exchangeBySku.set(line.sku, (exchangeBySku.get(line.sku) || 0) + cleanNumber(line.quantity)));
        lines = lines.map(line => {
          const exchangeQty = Math.min(cleanNumber(line.quantity), exchangeBySku.get(line.sku) || 0);
          if(exchangeQty > 0) exchangeBySku.set(line.sku, Math.max(0, (exchangeBySku.get(line.sku) || 0) - exchangeQty));
          return { ...line, quantity: Math.max(0, cleanNumber(line.quantity) - exchangeQty), exchangedQuantity: exchangeQty };
        }).filter(line => line.quantity > 0);
      }
      const insertAdjustment = db.prepare(`
        INSERT INTO inventory_adjustments(date, sku, product_name, supplier, type, quantity, delta, before_stock, after_stock, reference, reason, client_request_id)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `);
      lines.forEach((line, index) => {
        const sku = cleanText(line.sku, makeSku(line.description, index)).toUpperCase();
        const quantity = Math.max(0, cleanNumber(line.quantity));
        if(!sku || quantity <= 0) return;
        const existing = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
        if(existing) {
          const beforeStock = cleanNumber(existing.stock);
          const afterStock = beforeStock + quantity;
          const previousCost = cleanNumber(existing.cost_price);
          const requestedCost = cleanNumber(line.finalCost || line.final_cost || line.cost);
          const costEdited = purchaseLineFieldWasEdited(line, "cost", "finalCost", "final_cost");
          const saleEdited = purchaseLineFieldWasEdited(line, "sale");
          const wholesale1Edited = purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price");
          const wholesale2Edited = purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2");
          const wholesaleQty1Edited = purchaseLineFieldWasEdited(line, "wholesaleQty1", "wholesale_qty_1");
          const wholesaleQty2Edited = purchaseLineFieldWasEdited(line, "wholesaleQty2", "wholesale_qty_2");
          const nextCost = canEditCost && costEdited && requestedCost > 0 ? requestedCost : previousCost;
          const nextSale = canEditSalePrices && saleEdited && cleanNumber(line.sale) > 0 ? cleanNumber(line.sale) : cleanNumber(existing.sale_price);
          const nextWholesale = canEditWholesale1 && wholesale1Edited && cleanNumber(line.wholesalePrice || line.wholesale_price) > 0 ? cleanNumber(line.wholesalePrice || line.wholesale_price) : cleanNumber(existing.wholesale_price);
          const nextWholesale2 = canEditWholesale2 && wholesale2Edited && cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) > 0 ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : cleanNumber(existing.wholesale_price_2);
          const nextWholesaleQty1 = canEditWholesale1 && wholesaleQty1Edited && cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) > 0 ? cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) : cleanInteger(existing.wholesale_qty_1);
          const nextWholesaleQty2 = canEditWholesale2 && wholesaleQty2Edited && cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) > 0 ? cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) : cleanInteger(existing.wholesale_qty_2);
          const previousSale = cleanNumber(existing.sale_price);
          const previousWholesale = cleanNumber(existing.wholesale_price);
          const previousWholesale2 = cleanNumber(existing.wholesale_price_2);
          const costChanged = canEditCost && costEdited && requestedCost > 0 && previousCost > 0 && requestedCost !== previousCost;
          const saleChanged = canEditSalePrices && saleEdited && nextSale > 0 && previousSale > 0 && nextSale !== previousSale;
          const wholesaleChanged = canEditWholesale1 && wholesale1Edited && nextWholesale > 0 && previousWholesale > 0 && nextWholesale !== previousWholesale;
          const wholesale2Changed = canEditWholesale2 && wholesale2Edited && nextWholesale2 > 0 && previousWholesale2 > 0 && nextWholesale2 !== previousWholesale2;
          collectNegativeUtilityWarning(negativeUtilityWarnings, line, existing.name, existing.supplier, requestedCost || nextCost, nextSale, nextWholesale, nextWholesale2);
          if(costChanged || saleChanged || wholesaleChanged || wholesale2Changed) {
            const change = {
              action: "purchasePriceChange",
              sku,
              productName: existing.name,
              supplier: existing.supplier,
              purchaseFolio: noteReference,
              cost: { before: previousCost, after: nextCost, direction: nextCost > previousCost ? "sube" : nextCost < previousCost ? "baja" : "igual" },
              sale: { before: previousSale, after: nextSale, direction: nextSale > previousSale ? "sube" : nextSale < previousSale ? "baja" : "igual" },
              wholesale: { before: previousWholesale, after: nextWholesale, direction: nextWholesale > previousWholesale ? "sube" : nextWholesale < previousWholesale ? "baja" : "igual" },
              wholesale2: { before: previousWholesale2, after: nextWholesale2, direction: nextWholesale2 > previousWholesale2 ? "sube" : nextWholesale2 < previousWholesale2 ? "baja" : "igual" }
            };
            line.priceChange = change;
            purchasePriceChanges.push(change);
          }
          if(shouldApplyInventoryNow) {
            db.prepare(`
            UPDATE products SET
              stock = stock + ?,
              cost_price = CASE WHEN ? > 0 THEN ? ELSE cost_price END,
              sale_price = CASE WHEN ? > 0 THEN ? ELSE sale_price END,
              wholesale_price = CASE WHEN ? > 0 THEN ? ELSE wholesale_price END,
              wholesale_price_2 = CASE WHEN ? > 0 THEN ? ELSE wholesale_price_2 END,
              wholesale_qty_1 = CASE WHEN ? > 0 THEN ? ELSE wholesale_qty_1 END,
              wholesale_qty_2 = CASE WHEN ? > 0 THEN ? ELSE wholesale_qty_2 END,
              updated_at = ?
            WHERE sku = ?
          `).run(
            quantity,
            nextCost, nextCost,
            nextSale, nextSale,
            nextWholesale, nextWholesale,
            nextWholesale2, nextWholesale2,
            nextWholesaleQty1, nextWholesaleQty1,
            nextWholesaleQty2, nextWholesaleQty2,
            nowIso(),
            sku
          );
          insertAdjustment.run(
            cleanText(input.date, new Date().toLocaleDateString("es-MX")),
            sku,
            existing.name,
            existing.supplier,
            "purchase",
            quantity,
            quantity,
            beforeStock,
            afterStock,
            noteReference,
            `${sourceOrder ? "Entrada por recepcion de pedido" : "Entrada por recepcion directa"} ${noteReference}`,
            clientRequestId ? `${clientRequestId}-LINE-${index}` : ""
          );
          logNegativeStockCorrectionIfNeeded({
            sku,
            productName: existing.name,
            supplier: existing.supplier,
            beforeStock,
            afterStock,
            quantity,
            source: sourceOrder ? "recepcion de pedido" : "recepcion directa",
            reference: noteReference,
            reason: `${sourceOrder ? "Entrada por recepcion de pedido" : "Entrada por recepcion directa"} ${noteReference}`
          }, sessionContext);
          }
        } else {
          collectNegativeUtilityWarning(
            negativeUtilityWarnings,
            line,
            cleanText(line.description, sku),
            supplierName,
            cleanNumber(line.finalCost || line.final_cost || line.cost),
            canEditSalePrices && purchaseLineFieldWasEdited(line, "sale") ? cleanNumber(line.sale) : 0,
            canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price") ? cleanNumber(line.wholesalePrice || line.wholesale_price) : 0,
            canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2") ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : 0
          );
          if(shouldApplyInventoryNow) {
          upsertProduct({
            sku,
            name: cleanText(line.description, sku),
            supplier: cleanText(supplierName, "Proveedor general"),
            category: "General",
            stock: quantity,
            minimum: 1,
            costPrice: canEditCost && purchaseLineFieldWasEdited(line, "cost", "finalCost", "final_cost") ? cleanNumber(line.finalCost || line.final_cost || line.cost) : 0,
            salePrice: canEditSalePrices && purchaseLineFieldWasEdited(line, "sale") ? cleanNumber(line.sale) : 0,
            wholesalePrice: canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesalePrice", "wholesale_price") ? cleanNumber(line.wholesalePrice || line.wholesale_price) : 0,
            wholesalePrice2: canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesalePrice2", "wholesale_price_2") ? cleanNumber(line.wholesalePrice2 || line.wholesale_price_2) : 0,
            wholesaleQty1: canEditWholesale1 && purchaseLineFieldWasEdited(line, "wholesaleQty1", "wholesale_qty_1") ? cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1) : 0,
            wholesaleQty2: canEditWholesale2 && purchaseLineFieldWasEdited(line, "wholesaleQty2", "wholesale_qty_2") ? cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2) : 0,
            location: "S/U",
            aliases: line.ocrAlias ? [line.ocrAlias] : [],
            supplierAliases: {}
          });
          insertAdjustment.run(
            cleanText(input.date, new Date().toLocaleDateString("es-MX")),
            sku,
            cleanText(line.description, sku),
            cleanText(supplierName, "Proveedor general"),
            "purchase",
            quantity,
            quantity,
            0,
            quantity,
            noteReference,
            `${sourceOrder ? "Alta y entrada por recepcion de pedido" : "Alta y entrada por recepcion directa"} ${noteReference}`,
            clientRequestId ? `${clientRequestId}-LINE-${index}` : ""
          );
          }
        }
      });
      if(shouldApplyInventoryNow) exchangePurchaseLines.forEach((line, index) => {
        const sku = cleanText(line.sku).toUpperCase();
        const quantity = Math.max(0, cleanNumber(line.quantity));
        if(!sku || quantity <= 0) return;
        const existing = db.prepare("SELECT * FROM products WHERE sku = ?").get(sku);
        if(!existing) return;
        const beforeStock = cleanNumber(existing.stock);
        const afterStock = Math.max(0, beforeStock - quantity);
        db.prepare("UPDATE products SET stock = ?, updated_at = ? WHERE sku = ?").run(afterStock, nowIso(), sku);
        insertAdjustment.run(
          cleanText(input.date, new Date().toLocaleDateString("es-MX")),
          sku,
          existing.name,
          existing.supplier,
          "exchange_purchase",
          quantity,
          -quantity,
          beforeStock,
          afterStock,
          noteReference,
          `Compra por cambio aplicada a recepcion ${noteReference}`,
          clientRequestId ? `${clientRequestId}-EXCHANGE-${index}` : ""
        );
      });
    }

    const exchangePurchaseTotal = exchangePurchaseLines.reduce((sum, line) => sum + cleanNumber(line.quantity) * cleanNumber(line.finalCost || line.final_cost || line.cost), 0);
    const total = cleanNumber(input.total, Math.max(0, lines.reduce((sum, line) => sum + cleanNumber(line.quantity) * cleanNumber(line.finalCost || line.final_cost || line.cost), 0) - exchangePurchaseTotal));
    const linesForStorage = lines.map(line => ({ ...line }));
    if(physicalExchangeLines.length || exchangePurchaseLines.length || input.paymentCurrency || input.payment_currency || input.freight) {
      linesForStorage.push({
        sku: "__NOTA_META__",
        description: "Metadatos de recepcion",
        quantity: 0,
        meta: true,
        paymentCurrency: cleanText(input.paymentCurrency || input.payment_currency, "MXN"),
        freight: cleanNumber(input.freight),
        physicalExchangeLines,
        exchangePurchaseLines
      });
    }
    let purchaseNoteId = 0;
    if(sourceOrder) {
      const orderedLines = jsonParse(sourceOrder.ordered_lines_json, []);
      const fallbackOrderedLines = orderedLines.length ? orderedLines : jsonParse(sourceOrder.lines_json, []);
      const receiptFolio = cleanText(input.folio) || (sourceOrder.folio ? `${sourceOrder.folio}-REC` : `REC-${sourceOrder.id}`);
      const receiptNotes = cleanText(input.notes);
      const receivedAt = nowIso();
      const receivedBy = getReceivedByFromSession(sessionContext);
      const differences = summarizePurchaseReceiptDifferences(fallbackOrderedLines, linesForStorage);
      const inventoryStatus = receiptInventoryStatus;
      const receiptReviewStatus = getReceiptReviewStatus(purchasePolicy, differences.length > 0 || purchasePriceChanges.length > 0, inventoryStatus);
      db.prepare(`
        UPDATE purchase_notes SET
          mode = 'pedido_recibido',
          status = 'received',
          source = ?,
          line_count = ?,
          total = ?,
          notes = ?,
          receipt_folio = ?,
          receipt_notes = ?,
          receipt_total = ?,
          received_at = ?,
          received_by = ?,
          review_status = CASE WHEN review_status <> '' THEN review_status ELSE ? END,
          inventory_status = ?,
          receipt_lines_json = ?,
          ordered_lines_json = CASE WHEN ordered_lines_json <> '[]' THEN ordered_lines_json ELSE ? END,
          lines_json = ?,
          client_request_id = CASE WHEN ? <> '' THEN ? ELSE client_request_id END
        WHERE id = ?
      `).run(
        quickReceive ? "quick_receipt" : "order_receipt",
        cleanInteger(input.lineCount, lines.length),
        total,
        receiptNotes || `Recibido desde pedido ${sourceOrder.folio || sourceOrder.id}.`,
        receiptFolio,
        receiptNotes,
        total,
        receivedAt,
        receivedBy,
        receiptReviewStatus,
        inventoryStatus,
        jsonString(linesForStorage),
        jsonString(fallbackOrderedLines),
        jsonString(linesForStorage),
        clientRequestId,
        clientRequestId,
        sourceOrder.id
      );
      purchaseNoteId = sourceOrder.id;
      logAuditEvent({
        type: "Pedido recibido",
        severity: "Media",
        reference: sourceOrder.folio || receiptFolio || String(sourceOrder.id),
        amount: total,
        details: `Se recibio pedido ${sourceOrder.folio || sourceOrder.id} de ${supplierName}.`,
        payload: {
          action: "openPurchaseOrder",
          purchaseNoteId,
          supplier: supplierName,
          folio: sourceOrder.folio,
          receiptFolio,
          lineCount: cleanInteger(input.lineCount, lines.length),
          source: quickReceive ? "quick_receipt" : "order_receipt"
        }
      }, sessionContext);
      logPurchaseReceiptAudit({ purchaseNoteId, supplierName, folio: sourceOrder.folio, receiptFolio, total, sourceOrder, differences, priceChanges: purchasePriceChanges, reviewStatus: receiptReviewStatus, inventoryStatus }, sessionContext);
    } else {
      const isReceiptMode = ["manual", "image"].includes(mode);
      const documentStatus = mode === "pedido_borrador" ? "draft" : (mode === "pedido" || mode === "order" ? "ordered" : (mode === "pedido_revision" ? "receiving" : "received"));
      const documentSource = mode === "manual" ? "direct_receipt" : (mode === "image" ? "receipt_image" : "order");
      const receivedAt = isReceiptMode ? nowIso() : "";
      const receivedBy = isReceiptMode ? getReceivedByFromSession(sessionContext) : "";
      const inventoryStatus = mode === "manual" ? receiptInventoryStatus : (mode === "image" ? "pending_review" : "");
      const reviewStatus = isReceiptMode ? getReceiptReviewStatus(purchasePolicy, purchasePriceChanges.length > 0, inventoryStatus) : "";
      db.prepare(`
        INSERT INTO purchase_notes(mode, source_order_id, supplier, folio, date, line_count, total, notes, image, ocr_text, lines_json, client_request_id, status, source, received_at, received_by, review_status, inventory_status, receipt_folio, receipt_notes, receipt_total, receipt_image, receipt_lines_json, ordered_lines_json, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        mode,
        sourceOrderId,
        supplierName,
        cleanText(input.folio),
        cleanText(input.date, new Date().toLocaleDateString("es-MX")),
        cleanInteger(input.lineCount, lines.length),
        total,
        cleanText(input.notes),
        String(input.image || ""),
        cleanText(input.ocrText),
        jsonString(linesForStorage),
        clientRequestId,
        documentStatus,
        documentSource,
        receivedAt,
        receivedBy,
        reviewStatus,
        inventoryStatus,
        ["manual", "image"].includes(mode) ? cleanText(input.folio) : "",
        ["manual", "image"].includes(mode) ? cleanText(input.notes) : "",
        ["manual", "image"].includes(mode) ? total : 0,
        mode === "image" ? String(input.image || "") : "",
        ["manual", "image"].includes(mode) ? jsonString(linesForStorage) : "[]",
        ["pedido", "pedido_borrador", "order", "pedido_revision"].includes(mode) ? jsonString(linesForStorage) : "[]",
        nowIso()
      );
      purchaseNoteId = scalar("SELECT last_insert_rowid() AS id").id;
      if(isReceiptMode) {
        logPurchaseReceiptAudit({ purchaseNoteId, supplierName, folio: cleanText(input.folio), receiptFolio: cleanText(input.folio), total, sourceOrder: null, differences: [], priceChanges: purchasePriceChanges, reviewStatus, inventoryStatus, direct: true }, sessionContext);
      }
    }
    if(mode === "manual" && negativeUtilityWarnings.length) {
      logAuditEvent({
        type: sourceOrder ? "Recepcion de pedido con utilidad negativa" : "Recepcion directa con utilidad negativa",
        severity: "Alta",
        reference: cleanText(input.folio) || String(purchaseNoteId),
        amount: negativeUtilityWarnings.length,
        details: `${sourceOrder ? "Pedido" : "Recepcion"} ${cleanText(input.folio) || purchaseNoteId} de ${supplierName}: ${negativeUtilityWarnings.length} producto(s) quedarian con precio menor al costo recibido.`,
        payload: {
          action: "openPurchaseNote",
          purchaseNoteId,
          supplier: supplierName,
          folio: cleanText(input.folio),
          negativeUtilityWarnings,
          priceChanges: purchasePriceChanges
        }
      }, sessionContext);
    }
    db.exec("COMMIT");
    if(["pedido", "pedido_borrador", "order", "pedido_revision"].includes(mode) && !sourceOrder && !input.suppressAuditSummary) {
      logAuditEvent({
        type: "Pedido generado",
        severity: "Baja",
        reference: cleanText(input.folio) || String(purchaseNoteId),
        amount: total,
        details: `Se generó pedido para ${supplierName} con ${cleanInteger(input.lineCount, lines.length)} producto(s).`,
        payload: {
          action: "openPurchaseOrder",
          purchaseNoteId,
          supplier: supplierName,
          folio: cleanText(input.folio),
          lineCount: cleanInteger(input.lineCount, lines.length),
          source: cleanText(input.source || input.origin || "pedido")
        }
      }, sessionContext);
    }
    return purchaseNoteId;
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function normalizePurchaseLines(lines) {
  return (Array.isArray(lines) ? lines : []).map((line, index) => ({
    sku: cleanText(line.sku, makeSku(line.description, index)).toUpperCase(),
    description: cleanText(line.description || line.name),
    quantity: Math.max(0, cleanNumber(line.quantity)),
    cost: cleanNumber(line.cost),
    sale: cleanNumber(line.sale),
    wholesalePrice: cleanNumber(line.wholesalePrice || line.wholesale_price),
    wholesalePrice2: cleanNumber(line.wholesalePrice2 || line.wholesale_price_2),
    wholesaleQty1: cleanInteger(line.wholesaleQty1 || line.wholesale_qty_1),
    wholesaleQty2: cleanInteger(line.wholesaleQty2 || line.wholesale_qty_2),
    packagePieces: Math.max(1, cleanInteger(line.packagePieces || line.package_pieces || 1)),
    requestedQuantity: Math.max(0, cleanNumber(line.requestedQuantity || line.requested_quantity || line.quantity)),
    costIncludesTax: line.costIncludesTax !== undefined ? Boolean(line.costIncludesTax) : Boolean(line.cost_includes_tax),
    ivaEnabled: line.ivaEnabled !== undefined ? Boolean(line.ivaEnabled) : Boolean(line.iva_enabled),
    iepsEnabled: line.iepsEnabled !== undefined ? Boolean(line.iepsEnabled) : Boolean(line.ieps_enabled),
    ivaRate: cleanNumber(line.ivaRate || line.iva_rate),
    iepsRate: cleanNumber(line.iepsRate || line.ieps_rate),
    finalCost: cleanNumber(line.finalCost || line.final_cost || line.cost),
    matchedProductSku: cleanText(line.matchedProductSku || line.sku).toUpperCase(),
    ocrAlias: cleanText(line.ocrAlias),
    priceEditFlags: getPurchaseLineEditFlags(line)
  })).filter(line => line.sku && line.quantity > 0);
}

function updatePurchaseOrder(input) {
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "Pedido requerido.");
  const existing = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Pedido no encontrado.");
  if(!["pedido_borrador", "pedido", "order", "pedido_revision"].includes(existing.mode)) throw httpError(409, "Este pedido ya no se puede modificar.");
  const supplierName = cleanText(input.supplier, existing.supplier);
  if(!supplierName) throw httpError(400, "Selecciona proveedor.");
  const lines = normalizePurchaseLines(input.lines);
  if(!lines.length) throw httpError(400, "El pedido necesita productos.");
  const total = cleanNumber(input.total, lines.reduce((sum, line) => sum + line.quantity * line.cost, 0));
  const nextMode = ["pedido_borrador", "pedido", "order", "pedido_revision"].includes(cleanText(input.mode)) ? cleanText(input.mode) : existing.mode;
  const nextStatus = nextMode === "pedido_borrador" ? "draft" : (nextMode === "pedido_revision" ? "receiving" : "ordered");
  db.prepare(`
    UPDATE purchase_notes
    SET mode = ?, status = ?, supplier = ?, line_count = ?, total = ?, notes = ?, lines_json = ?, ordered_lines_json = ?
    WHERE id = ?
  `).run(nextMode, nextStatus, supplierName, lines.length, total, cleanText(input.notes), jsonString(lines), jsonString(lines), id);
}

function receivePurchaseOrder(input, sessionContext = null) {
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "Pedido requerido.");
  const existing = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Pedido no encontrado.");
  if(existing.mode === "pedido_recibido" || getPurchaseDocumentStatus(existing) === "received") throw httpError(409, "Este pedido ya fue recibido.");
  if(!["pedido_borrador", "pedido", "order", "pedido_revision"].includes(existing.mode)) throw httpError(409, "Este pedido no esta listo para recibirse.");
  const lines = normalizePurchaseLines(input.lines);
  if(!lines.length) throw httpError(400, "El pedido necesita productos.");
  createPurchaseNote({
    mode: "manual",
    supplier: cleanText(input.supplier, existing.supplier),
    folio: cleanText(input.folio) || (existing.folio ? `${existing.folio}-REC` : ""),
    date: cleanText(input.date, new Date().toLocaleDateString("es-MX")),
    notes: cleanText(input.notes),
    lines,
    total: cleanNumber(input.total, lines.reduce((sum, line) => sum + line.quantity * line.cost, 0)),
    sourceOrderId: id,
    clientRequestId: cleanText(input.clientRequestId || input.client_request_id),
    quickReceive: input.quickReceive === true || input.quick_receive === true,
    receiveMode: input.quickReceive || input.quick_receive ? "quick" : "manual"
  }, sessionContext);
}

function deletePurchaseOrder(input, sessionContext = null) {
  const id = cleanInteger(input.id);
  if(!id) throw httpError(400, "Pedido requerido.");
  const existing = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Pedido no encontrado.");
  if(!["pedido_borrador", "pedido", "order", "pedido_revision", "pedido_recibido"].includes(existing.mode)) {
    throw httpError(409, "Solo se pueden eliminar pedidos desde esta opcion.");
  }
  db.prepare("DELETE FROM purchase_notes WHERE id = ?").run(id);
  logAuditEvent({
    type: "Pedido eliminado",
    severity: "Media",
    reference: existing.folio || String(id),
    amount: cleanNumber(existing.total),
    details: `Se eliminó pedido ${existing.folio || id} de ${existing.supplier || "sin proveedor"}.`,
    payload: {
      action: "deletePurchaseOrder",
      purchaseOrderId: id,
      supplier: existing.supplier,
      folio: existing.folio,
      mode: existing.mode
    }
  }, sessionContext);
}

function markPurchaseNoteAdminReview(input, sessionContext = null) {
  const id = cleanInteger(input.id || input.noteId || input.purchaseNoteId);
  if(!id) throw httpError(400, "Nota no valida.");
  const existing = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Nota no encontrada.");
  const reviewedAt = nowIso();
  const reviewedBy = sessionContext?.user?.name || sessionContext?.user?.username || "Administrador";
  const correctedLines = getReceiptLinesFromReviewInput(input);
  const currentInventoryStatus = cleanText(existing.inventory_status);
  if(correctedLines.length) {
    if(currentInventoryStatus === "provisional" || currentInventoryStatus === "applied") {
      applyProvisionalPurchaseReceiptCorrection(existing, correctedLines, sessionContext);
    } else {
      const total = correctedLines.reduce((sum, line) => sum + cleanNumber(line.quantity) * cleanNumber(line.finalCost || line.final_cost || line.cost), 0);
      db.prepare(`
        UPDATE purchase_notes SET receipt_lines_json = ?, lines_json = ?, line_count = ?, receipt_total = ?, total = CASE WHEN source IN ('order_receipt','direct_receipt') OR mode IN ('manual','pedido_recibido') THEN ? ELSE total END
        WHERE id = ?
      `).run(jsonString(correctedLines), jsonString(correctedLines), correctedLines.length, total, total, id);
    }
  }
  const refreshed = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(shouldApplyDeferredPurchaseInventoryStatus(refreshed.inventory_status) && (jsonParse(refreshed.receipt_lines_json, []).length || jsonParse(refreshed.lines_json, []).length)) {
    applyDeferredPurchaseInventory(refreshed, sessionContext);
  }
  db.prepare("UPDATE purchase_notes SET reviewed_by_admin_at = ?, reviewed_by_admin_user = ?, review_status = ?, inventory_status = ? WHERE id = ?")
    .run(reviewedAt, reviewedBy, "reviewed", "applied", id);
  return rowToPurchaseNote(db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id));
}

function unlockPurchaseNoteAdminReview(input, sessionContext = null) {
  const id = cleanInteger(input.id || input.noteId || input.purchaseNoteId);
  if(!id) throw httpError(400, "Nota no valida.");
  const existing = db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "Nota no encontrada.");
  const previousReviewStatus = cleanText(existing.review_status);
  const previousReviewedAt = cleanText(existing.reviewed_by_admin_at);
  const previousReviewedBy = cleanText(existing.reviewed_by_admin_user);
  if(!previousReviewStatus && !previousReviewedAt && !previousReviewedBy) {
    return rowToPurchaseNote(existing);
  }
  db.prepare("UPDATE purchase_notes SET review_status = '', reviewed_by_admin_at = '', reviewed_by_admin_user = '' WHERE id = ?").run(id);
  logAuditEvent({
    type: "Desbloqueo de revision de compra",
    severity: "Media",
    reference: existing.folio || String(id),
    amount: cleanNumber(existing.total),
    details: `${sessionContext?.user?.name || "Administrador"} desbloqueo la revision de ${existing.supplier || "proveedor"} para permitir correccion administrativa.`,
    payload: {
      action: "unlockPurchaseReview",
      purchaseNoteId: id,
      supplier: existing.supplier,
      folio: existing.folio,
      previousReviewStatus,
      previousReviewedAt,
      previousReviewedBy
    }
  }, sessionContext);
  return rowToPurchaseNote(db.prepare("SELECT * FROM purchase_notes WHERE id = ?").get(id));
}

function upsertCashierUser(input) {
  const name = cleanText(input.name);
  if(!name) throw httpError(400, "Nombre de usuario requerido.");
  const originalName = cleanText(input.originalName || input.previousName || input.oldName);
  const requestedPin = cleanText(input.pin);
  const role = cleanText(input.role, "cashier");
  const status = cleanText(input.status, "Activo");
  const permissionsJson = jsonString(Array.isArray(input.permissions) ? input.permissions : []);
  const existingByOriginal = originalName ? db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?)").get(originalName) : null;
  const existingByName = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?)").get(name);
  if(originalName && !existingByOriginal) throw httpError(404, "No encontre el cajero a editar.");
  if(originalName && existingByName && existingByName.name.toLowerCase() !== existingByOriginal.name.toLowerCase()) {
    throw httpError(409, "Ya existe un cajero con ese nombre.");
  }
  const sourceUser = existingByOriginal || existingByName;
  const pin = requestedPin ? hashPin(validateSecurePin(requestedPin)) : (sourceUser?.pin || "");
  if(!pin) throw httpError(400, "Clave/PIN requerido.");
  if(existingByOriginal) {
    db.prepare("UPDATE cashier_users SET name = ?, pin = ?, role = ?, status = ?, permissions_json = ? WHERE name = ?")
      .run(name, pin, role, status, permissionsJson, existingByOriginal.name);
    if(existingByOriginal.name !== name) {
      db.prepare("UPDATE cashier_tasks SET cashier = ? WHERE cashier = ?").run(name, existingByOriginal.name);
    }
    return;
  }
  if(existingByName) {
    db.prepare("UPDATE cashier_users SET pin = ?, role = ?, status = ?, permissions_json = ? WHERE name = ?")
      .run(pin, role, status, permissionsJson, existingByName.name);
    return;
  }
  db.prepare("INSERT INTO cashier_users(name, pin, role, status, permissions_json) VALUES (?, ?, ?, ?, ?)")
    .run(name, pin, role, status, permissionsJson);
}

function deleteCashierUser(input, authUser, sessionContext = null) {
  const name = cleanText(input.name);
  if(!name) throw httpError(400, "Nombre de usuario requerido.");
  const existing = db.prepare("SELECT * FROM cashier_users WHERE LOWER(name) = LOWER(?)").get(name);
  if(!existing) throw httpError(404, "No encontre el cajero seleccionado.");
  if(authUser?.name && authUser.name.toLowerCase() === existing.name.toLowerCase()) {
    throw httpError(409, "No puedes eliminar el usuario con la sesion abierta.");
  }
  if(existing.role === "admin" && existing.status === "Activo") {
    const remainingAdmins = db.prepare("SELECT COUNT(*) AS total FROM cashier_users WHERE role = 'admin' AND status = 'Activo' AND LOWER(name) <> LOWER(?)").get(existing.name).total;
    if(remainingAdmins < 1) throw httpError(409, "Debe quedar al menos un administrador activo.");
  }
  db.prepare("DELETE FROM cashier_users WHERE name = ?").run(existing.name);
  db.prepare("UPDATE cashier_tasks SET cashier = 'Todos' WHERE cashier = ?").run(existing.name);
  logAuditEvent({
    type: "Cajero eliminado",
    severity: "Alta",
    reference: existing.name,
    details: `Usuario ${existing.name} eliminado. Rol anterior: ${existing.role}.`,
    payload: { name: existing.name, role: existing.role, status: existing.status }
  }, sessionContext);
}

function upsertCashierTask(input) {
  const title = cleanText(input.title);
  if(!title) throw httpError(400, "Titulo de tarea requerido.");
  const createdAt = cleanText(input.createdAt || input.created_at, new Date().toLocaleDateString("es-MX"));
  const completedAt = cleanText(input.completedAt || input.completed_at);
  const requestedStatus = cleanText(input.status, "Activa");
  const status = ["Activa", "Pausada", "Archivada", "Eliminada"].includes(requestedStatus) ? requestedStatus : "Activa";
  const requestedResetPeriod = cleanText(input.resetPeriod || input.reset_period, "weekly");
  const resetPeriod = ["none", "daily", "weekly", "monthly"].includes(requestedResetPeriod) ? requestedResetPeriod : "weekly";
  if(input.id) {
    db.prepare(`
      UPDATE cashier_tasks SET title = ?, day = ?, shift = ?, cashier = ?, priority = ?, category = ?, description = ?, subtasks_json = ?, done = ?, status = ?, reset_period = ?, created_at = ?, completed_at = ?
      WHERE id = ?
    `).run(title, cleanText(input.day, "Diario"), cleanText(input.shift, "Todos"), cleanText(input.cashier, "Todos"), cleanText(input.priority, "Normal"), cleanText(input.category, "General"), cleanText(input.description), jsonString(input.subtasks || []), input.done ? 1 : 0, status, resetPeriod, createdAt, completedAt, cleanInteger(input.id));
    syncTaskAttachments(cleanInteger(input.id), input.photos || []);
    return cleanInteger(input.id);
  } else {
    const result = db.prepare(`
      INSERT INTO cashier_tasks(title, day, shift, cashier, priority, category, description, subtasks_json, photos_json, done, status, reset_period, created_at, completed_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(title, cleanText(input.day, "Diario"), cleanText(input.shift, "Todos"), cleanText(input.cashier, "Todos"), cleanText(input.priority, "Normal"), cleanText(input.category, "General"), cleanText(input.description), jsonString(input.subtasks || []), "[]", input.done ? 1 : 0, status, resetPeriod, createdAt, completedAt);
    const taskId = Number(result.lastInsertRowid);
    syncTaskAttachments(taskId, input.photos || []);
    return taskId;
  }
}

function deleteCashierTask(input, sessionContext = null) {
  const id = cleanInteger(input.id || input.taskId);
  if(!id) throw httpError(400, "Tarea requerida.");
  const existing = db.prepare("SELECT * FROM cashier_tasks WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "No encontre la tarea.");
  db.prepare("UPDATE cashier_tasks SET status = 'Eliminada', completed_at = ? WHERE id = ?").run(new Date().toLocaleDateString("es-MX"), id);
  logAuditEvent({
    type: "Tarea eliminada",
    severity: "Media",
    reference: String(id),
    details: `Tarea quitada del calendario: ${existing.title}.`,
    payload: { id, title: existing.title, day: existing.day, shift: existing.shift, cashier: existing.cashier }
  }, sessionContext);
}

function archiveCashierTask(input, sessionContext = null) {
  const id = cleanInteger(input.id || input.taskId);
  if(!id) throw httpError(400, "Tarea requerida.");
  const existing = db.prepare("SELECT * FROM cashier_tasks WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "No encontre la tarea.");
  const mediaPolicy = cleanText(input.mediaPolicy || input.media_policy) === "delete" ? "delete" : "keep";
  const completedAt = new Date().toLocaleDateString("es-MX");
  const subtasks = jsonParse(existing.subtasks_json, []).map((subtask, index) => ({
    ...subtask,
    id: subtask.id || index + 1,
    done: true
  }));
  db.prepare("UPDATE cashier_tasks SET subtasks_json = ?, done = 1, status = 'Archivada', completed_at = ? WHERE id = ?")
    .run(jsonString(subtasks), completedAt, id);
  if(mediaPolicy === "delete") {
    const attachmentIds = db.prepare("SELECT id FROM task_attachments WHERE task_id = ?").all(id).map(row => row.id);
    attachmentIds.forEach(removeTaskAttachmentFiles);
    db.prepare("UPDATE cashier_tasks SET photos_json = '[]' WHERE id = ?").run(id);
  } else {
    db.prepare("UPDATE task_attachments SET storage_tier = 'archived', archived_at = ? WHERE task_id = ?").run(nowIso(), id);
  }
  logAuditEvent({
    type: "Tarea cerrada y archivada",
    severity: "Media",
    reference: String(id),
    details: `${existing.title}. Imágenes: ${mediaPolicy === "delete" ? "eliminadas" : "conservadas en archivo"}.`,
    payload: { id, title: existing.title, mediaPolicy }
  }, sessionContext);
  return id;
}

function updateCashierTaskProgress(input, sessionContext = null) {
  const payload = input.task || input;
  const id = cleanInteger(payload.id || payload.taskId);
  if(!id) throw httpError(400, "Tarea requerida.");
  const existing = db.prepare("SELECT * FROM cashier_tasks WHERE id = ?").get(id);
  if(!existing) throw httpError(404, "No encontre la tarea.");
  if(cleanText(existing.status, "Activa") === "Eliminada") throw httpError(409, "Esta tarea fue eliminada.");
  if(cleanText(existing.status) === "Archivada") throw httpError(409, "Esta tarea ya fue cerrada y archivada.");
  if(!taskVisibleForUser(existing, sessionContext?.user)) {
    throw httpError(403, "Esta tarea no esta asignada a tu usuario.");
  }
  const currentSubtasks = jsonParse(existing.subtasks_json, []);
  const incomingSubtasks = Array.isArray(payload.subtasks) ? payload.subtasks : currentSubtasks;
  const incomingPhotos = Array.isArray(payload.photos) ? payload.photos : [];
  const subtasks = currentSubtasks.map((subtask, index) => {
    const incoming = incomingSubtasks.find(item => String(item.id || index + 1) === String(subtask.id || index + 1))
      || incomingSubtasks.find(item => cleanText(item.title) === cleanText(subtask.title))
      || {};
    return {
      id: subtask.id || index + 1,
      title: cleanText(subtask.title || incoming.title, `Subtarea ${index + 1}`),
      done: Boolean(incoming.done)
    };
  });
  const done = subtasks.length ? subtasks.every(subtask => subtask.done) : Boolean(payload.done);
  const completedAt = done ? cleanText(payload.completedAt || payload.completed_at, new Date().toLocaleDateString("es-MX")) : "";
  const requestedResetPeriod = cleanText(payload.resetPeriod || payload.reset_period || existing.reset_period || "weekly");
  const resetPeriod = ["none", "daily", "weekly", "monthly"].includes(requestedResetPeriod) ? requestedResetPeriod : "weekly";
  db.prepare(`
    UPDATE cashier_tasks
    SET subtasks_json = ?, done = ?, completed_at = ?, reset_period = ?
    WHERE id = ?
  `).run(jsonString(subtasks), done ? 1 : 0, completedAt, resetPeriod, id);
  syncTaskAttachments(id, incomingPhotos);
  return id;
}

function saveSettings(input) {
  const entries = Object.entries(input || {});
  const statement = db.prepare("INSERT OR REPLACE INTO settings(key, value_json) VALUES (?, ?)");
  db.exec("BEGIN IMMEDIATE");
  try {
    entries.forEach(([key, value]) => {
      statement.run(key, jsonString(value));
    });
    db.exec("COMMIT");
  } catch(error) {
    try { db.exec("ROLLBACK"); } catch(rollbackError) {}
    throw error;
  }
}

function createSettingsSnapshot(input = {}, sessionContext = null) {
  const settings = input.settings && typeof input.settings === "object" ? input.settings : getSettings();
  const reason = cleanText(input.reason || input.motivo, "Cambio de configuracion");
  const userName = cleanText(sessionContext?.user?.name || input.user || input.usuario, "Sistema");
  db.prepare(`
    INSERT INTO settings_snapshots(created_at, user_name, reason, settings_json)
    VALUES (?, ?, ?, ?)
  `).run(nowIso(), userName, reason, jsonString(settings));
  return db.prepare("SELECT * FROM settings_snapshots WHERE id = last_insert_rowid()").get();
}

function ensureInitialSettingsSnapshot() {
  const snapshotCount = scalar("SELECT COUNT(*) AS count FROM settings_snapshots").count;
  const settingsCount = scalar("SELECT COUNT(*) AS count FROM settings").count;
  if(snapshotCount > 0 || settingsCount <= 0) return;
  createSettingsSnapshot({
    reason: "Punto inicial automatico de configuracion",
    user: "Sistema"
  });
}

function rowToSettingsSnapshot(row, includeSettings = false) {
  const snapshot = {
    id: row.id,
    createdAt: row.created_at,
    user: row.user_name,
    reason: row.reason
  };
  if(includeSettings) snapshot.settings = jsonParse(row.settings_json, {});
  return snapshot;
}

function listSettingsSnapshots(limit = 30) {
  return db.prepare("SELECT * FROM settings_snapshots ORDER BY id DESC LIMIT ?").all(Math.max(1, Math.min(100, cleanInteger(limit, 30)))).map(rowToSettingsSnapshot);
}

function restoreSettingsSnapshot(input = {}, sessionContext = null) {
  const id = cleanInteger(input.id || input.snapshotId || input.snapshot_id);
  if(!id) throw httpError(400, "Selecciona una configuracion para restaurar.");
  const row = db.prepare("SELECT * FROM settings_snapshots WHERE id = ?").get(id);
  if(!row) throw httpError(404, "No encontre el historial de configuracion seleccionado.");
  createSettingsSnapshot({ reason: `Antes de restaurar configuracion #${id}` }, sessionContext);
  const settings = jsonParse(row.settings_json, {});
  saveSettings(settings);
  logAuditEvent({
    type: "Configuracion restaurada",
    severity: "Media",
    reference: `settings-snapshot-${id}`,
    details: `Se restauro la configuracion guardada el ${row.created_at}.`,
    payload: { snapshotId: id, restoredBy: sessionContext?.user?.name || "" }
  }, sessionContext);
  return rowToSettingsSnapshot(row);
}

function importSettingsConfiguration(input = {}, sessionContext = null) {
  const settings = input.settings && typeof input.settings === "object" ? input.settings : input;
  if(!settings || typeof settings !== "object" || Array.isArray(settings)) throw httpError(400, "Archivo de configuracion invalido.");
  createSettingsSnapshot({ reason: "Antes de importar configuracion" }, sessionContext);
  saveSettings(settings);
  const profiles = Array.isArray(input.printProfiles) ? input.printProfiles : [];
  profiles.forEach(profile => {
    if(profile && profile.name && profile.settings && typeof profile.settings === "object") savePrintProfile(profile, sessionContext);
  });
  logAuditEvent({
    type: "Configuracion importada",
    severity: "Media",
    reference: "settings-import",
    details: "Se importo una configuracion desde archivo JSON.",
    payload: { keys: Object.keys(settings).length, printProfiles: profiles.length, importedBy: sessionContext?.user?.name || "" }
  }, sessionContext);
  return { ok: true, keys: Object.keys(settings).length, printProfiles: profiles.length };
}

function rowToPrintProfile(row) {
  return {
    id: row.id,
    name: row.name,
    type: row.type,
    terminalId: row.terminal_id,
    settings: jsonParse(row.settings_json, {}),
    isDefault: Boolean(row.is_default),
    updatedAt: row.updated_at
  };
}

function listPrintProfiles() {
  return db.prepare("SELECT * FROM print_profiles ORDER BY is_default DESC, type, name").all().map(rowToPrintProfile);
}

function savePrintProfile(input = {}, sessionContext = null) {
  const name = cleanText(input.name || input.profileName || input.nombre);
  if(!name) throw httpError(400, "Nombre de perfil requerido.");
  const type = cleanText(input.type || input.tipo, "general");
  const terminalId = cleanText(input.terminalId || input.terminal_id || input.deviceId);
  const settings = input.settings && typeof input.settings === "object" ? input.settings : getSettings();
  const isDefault = Boolean(input.isDefault || input.default);
  db.exec("BEGIN IMMEDIATE");
  try {
    if(isDefault) db.prepare("UPDATE print_profiles SET is_default = 0 WHERE type = ? AND terminal_id = ?").run(type, terminalId);
    db.prepare(`
      INSERT INTO print_profiles(name, type, terminal_id, settings_json, is_default, updated_at)
      VALUES (?, ?, ?, ?, ?, ?)
      ON CONFLICT(name) DO UPDATE SET type = excluded.type, terminal_id = excluded.terminal_id, settings_json = excluded.settings_json, is_default = excluded.is_default, updated_at = excluded.updated_at
    `).run(name, type, terminalId, jsonString(settings), isDefault ? 1 : 0, nowIso());
    db.exec("COMMIT");
  } catch(error) {
    try { db.exec("ROLLBACK"); } catch(rollbackError) {}
    throw error;
  }
  const row = db.prepare("SELECT * FROM print_profiles WHERE name = ?").get(name);
  logAuditEvent({
    type: "Perfil de impresion guardado",
    severity: "Baja",
    reference: name,
    details: `Perfil ${name} actualizado.`,
    payload: { profileId: row?.id || 0, type, terminalId, user: sessionContext?.user?.name || "" }
  }, sessionContext);
  return rowToPrintProfile(row);
}

function applyPrintProfile(input = {}, sessionContext = null) {
  const id = cleanInteger(input.id || input.profileId || input.profile_id);
  const name = cleanText(input.name || input.profileName);
  const row = id ? db.prepare("SELECT * FROM print_profiles WHERE id = ?").get(id) : db.prepare("SELECT * FROM print_profiles WHERE name = ?").get(name);
  if(!row) throw httpError(404, "No encontre el perfil de impresion.");
  createSettingsSnapshot({ reason: `Antes de aplicar perfil ${row.name}` }, sessionContext);
  const profile = rowToPrintProfile(row);
  saveSettings(profile.settings);
  logAuditEvent({
    type: "Perfil de impresion aplicado",
    severity: "Media",
    reference: row.name,
    details: `Se aplico el perfil de impresion ${row.name}.`,
    payload: { profileId: row.id, type: row.type, terminalId: row.terminal_id }
  }, sessionContext);
  return profile;
}

function getActivePromotionForProduct(product) {
  const settings = getSettings();
  const promotions = Array.isArray(settings.promotions) ? settings.promotions : [];
  const dayLabels = ["Domingo", "Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado"];
  const today = dayLabels[new Date().getDay()];
  return promotions.find(promo => {
    if(!promo) return false;
    if(promo.endDate && new Date(`${promo.endDate}T23:59:59`).getTime() < Date.now()) return false;
    if(promo.day && promo.day !== "Todos" && promo.day !== today) return false;
    const target = cleanText(promo.target).toLowerCase();
    if(!target) return false;
    if(promo.scope === "department") return target === cleanText(product.category).toLowerCase();
    if(promo.scope === "supplier") return target === cleanText(product.supplier).toLowerCase();
    return target === cleanText(product.sku).toLowerCase();
  });
}

function applyPromotionPrice(product, basePrice) {
  const promo = getActivePromotionForProduct(product);
  if(!promo) return basePrice;
  if(["bogo2x1", "bogo3x2", "points2x", "points3x"].includes(promo.discountType)) return basePrice;
  const value = cleanNumber(promo.value);
  if(value <= 0) return basePrice;
  if(promo.discountType === "price") return Math.max(0, value);
  if(promo.discountType === "amount") return Math.max(0, basePrice - value);
  return Math.max(0, basePrice * (1 - value / 100));
}

function getPromotionLineTotal(product, basePrice, quantity = 1) {
  const qty = Math.max(0, cleanNumber(quantity));
  const promo = getActivePromotionForProduct(product);
  if(!promo || qty <= 0) return cleanMoney(basePrice * qty);
  if(promo.discountType === "bogo2x1") return cleanMoney(Math.ceil(qty / 2) * basePrice);
  if(promo.discountType === "bogo3x2") return cleanMoney((qty - Math.floor(qty / 3)) * basePrice);
  if(promo.discountType === "points2x" || promo.discountType === "points3x") return cleanMoney(basePrice * qty);
  return cleanMoney(applyPromotionPrice(product, basePrice) * qty);
}

function getPromotionEffectiveUnitPrice(product, basePrice, quantity = 1) {
  const qty = Math.max(0.001, cleanNumber(quantity, 1));
  return cleanMoney(getPromotionLineTotal(product, basePrice, qty) / qty);
}

function normalizeBundleItems(items) {
  if(!Array.isArray(items)) return [];
  const merged = new Map();
  items.forEach(item => {
    const sku = cleanText(item.sku).toUpperCase();
    const quantity = Math.max(1, cleanInteger(item.quantity, 1));
    if(!sku) return;
    merged.set(sku, (merged.get(sku) || 0) + quantity);
  });
  return [...merged.entries()].map(([sku, quantity]) => ({ sku, quantity }));
}

function createMobileInventoryAdjustment(input, sessionContext) {
  const type = cleanText(input.type, "add");
  const isNegative = ["remove", "waste", "set"].includes(type);
  let adminUser = sessionContext?.user?.role === "admin" ? sessionContext.user : null;
  if(isNegative) {
    adminUser = adminUser || validateAdminPin(input.adminPin || input.authorizationCode);
    if(!adminUser) throw httpError(403, "Codigo de autorizacion de administrador invalido.");
  }
  createInventoryAdjustment(input, sessionContext);
}

function getSalesSummaryForSession(sessionId) {
  const cleanSessionId = cleanText(sessionId);
  const empty = { count: 0, items: 0, total: 0, cash: 0, cashMxnNet: 0, cashNet: 0, card: 0, transfer: 0, usd: 0, usdMxn: 0, usdMxnNet: 0, credit: 0, pointsRedeemed: 0, change: 0, pointsAwarded: 0, grossProfit: 0 };
  if(!cleanSessionId) return empty;
  const rows = db.prepare(`
    SELECT s.id, s.total, s.change_amount, s.payments_json, s.loyalty_points,
      COALESCE(SUM(si.quantity), 0) AS items,
      COALESCE(SUM(si.line_total - (si.cost_price * si.quantity)), 0) AS gross_profit
    FROM sales s
    LEFT JOIN sale_items si ON si.sale_id = s.id
    WHERE s.session_id = ? AND s.status = 'Completada'
    GROUP BY s.id
  `).all(cleanSessionId);
  return rows.reduce((summary, row) => {
    const payments = jsonParse(row.payments_json, {});
    const exchangeRate = cleanNumber(payments.exchangeRate);
    const usd = cleanNumber(payments.usd);
    const usdMxn = usd * exchangeRate;
    const cash = cleanNumber(payments.cash);
    const change = cleanNumber(row.change_amount);
    const changeAppliedToMxn = Math.min(change, cash);
    const remainingChange = Math.max(change - changeAppliedToMxn, 0);
    const cashMxnNet = Math.max(cash - changeAppliedToMxn, 0);
    const usdMxnNet = Math.max(usdMxn - remainingChange, 0);
    summary.count += 1;
    summary.items += cleanNumber(row.items);
    summary.total += cleanNumber(row.total);
    summary.cash += cash;
    summary.cashMxnNet += cashMxnNet;
    summary.usd += usd;
    summary.usdMxn += usdMxn;
    summary.usdMxnNet += usdMxnNet;
    summary.cashNet += cashMxnNet + usdMxnNet;
    summary.card += cleanNumber(payments.card);
    summary.transfer += cleanNumber(payments.transfer);
    summary.credit += cleanNumber(payments.credit);
    const redeemedPoints = cleanNumber(payments.points);
    summary.pointsRedeemed += redeemedPoints;
    summary.change += change;
    summary.pointsAwarded += cleanNumber(row.loyalty_points);
    summary.grossProfit += cleanMoney(cleanNumber(row.gross_profit) - redeemedPoints);
    return summary;
  }, empty);
}

function isCashLikeMovementMethod(method = "") {
  const clean = cleanText(method || "Efectivo").toLowerCase();
  return ["efectivo", "cash", "mxn", "dolares", "dolares", "dólares", "usd"].includes(clean);
}

function getCashMovementSummaryForSession(sessionId) {
  const cleanSessionId = cleanText(sessionId);
  const empty = { incomeCash: 0, incomeNonCash: 0, paymentCash: 0, paymentNonCash: 0, incomeTotal: 0, paymentTotal: 0 };
  if(!cleanSessionId) return empty;
  const rows = db.prepare("SELECT type, amount, method FROM cash_movements WHERE session_id = ?").all(cleanSessionId);
  return rows.reduce((summary, row) => {
    const amount = cleanNumber(row.amount);
    const cashLike = isCashLikeMovementMethod(row.method);
    if(row.type === "income") {
      summary.incomeTotal += amount;
      if(cashLike) summary.incomeCash += amount;
      else summary.incomeNonCash += amount;
    } else if(row.type === "payment") {
      summary.paymentTotal += amount;
      if(cashLike) summary.paymentCash += amount;
      else summary.paymentNonCash += amount;
    }
    return summary;
  }, empty);
}

function recalculateCutReportPayload(row, payload = {}) {
  const sessionId = cleanText(row?.session_id || payload.sessionId);
  if(!sessionId) return { payload, difference: cleanNumber(row?.difference ?? payload.difEfectivo) };
  const salesSummary = getSalesSummaryForSession(sessionId);
  const movementSummary = getCashMovementSummaryForSession(sessionId);
  const previousCalc = payload.cashCalculation && typeof payload.cashCalculation === "object" ? payload.cashCalculation : {};
  const physicalCount = cleanNumber(previousCalc.fisicoContado ?? payload.fisico);
  const internalWithdrawals = cleanNumber(previousCalc.retirosCapturadosCorte ?? previousCalc.retirosInternosNoGasto ?? payload.totalRetiros);
  const terminalAfirme = cleanNumber(payload.terminalAfirme);
  const terminalMercadoPago = cleanNumber(payload.terminalMercadoPago);
  const terminalTotal = terminalAfirme + terminalMercadoPago;
  const expectedBeforeWithdrawals = Math.max(salesSummary.cashNet + movementSummary.incomeCash - movementSummary.paymentCash, 0);
  const expectedInDrawer = expectedBeforeWithdrawals - internalWithdrawals;
  const totalArqueo = physicalCount + internalWithdrawals;
  const difference = cleanMoney(physicalCount - expectedInDrawer);
  const cashCalculation = {
    ...previousCalc,
    ventasTotal: salesSummary.cashNet + salesSummary.card + salesSummary.transfer + salesSummary.credit,
    ventasEfectivo: salesSummary.cashNet,
    ventasEfectivoMxn: salesSummary.cashMxnNet,
    ventasTarjeta: salesSummary.card,
    ventasTransferencia: salesSummary.transfer,
    ventasCredito: salesSummary.credit,
    dolaresFisicosRecibidos: salesSummary.usd,
    dolaresConvertidosMxn: salesSummary.usdMxn,
    dolaresConvertidosMxnNetos: salesSummary.usdMxnNet,
    cambioEntregado: salesSummary.change,
    puntosUsados: salesSummary.pointsRedeemed,
    puntosOtorgados: salesSummary.pointsAwarded,
    utilidadBrutaDespuesPuntos: salesSummary.grossProfit,
    entradasEfectivo: movementSummary.incomeCash,
    salidasEfectivo: movementSummary.paymentCash,
    salidasNoEfectivo: movementSummary.paymentNonCash,
    efectivoEsperado: expectedBeforeWithdrawals,
    efectivoEsperadoAntesRetiros: expectedBeforeWithdrawals,
    retirosInternosNoGasto: internalWithdrawals,
    efectivoDebeQuedarCajon: expectedInDrawer,
    gastosProveedoresSaleTienda: movementSummary.paymentCash,
    fisicoContado: physicalCount,
    retirosCapturadosCorte: internalWithdrawals,
    totalArqueo,
    diferenciaCaja: difference,
    ventasCantidad: salesSummary.count
  };
  const nextPayload = {
    ...payload,
    salesSummary,
    grossProfit: salesSummary.grossProfit,
    sistema: expectedBeforeWithdrawals,
    sistemaEfectivo: expectedBeforeWithdrawals,
    sistemaTarjeta: salesSummary.card,
    gastosInformativos: movementSummary.paymentCash,
    totalRetiros: internalWithdrawals,
    totalArqueo,
    difEfectivo: difference,
    desfaseTarjetas: cleanMoney(terminalTotal - salesSummary.card),
    cashCalculation
  };
  return { payload: nextPayload, difference };
}

function buildCutPreview(input = {}, sessionContext = null) {
  const sessionId = cleanText(input.sessionId || input.session_id || sessionContext?.cashSession?.id);
  if(!sessionId) throw httpError(400, "Sesion de caja requerida para calcular el corte.");
  const payload = {
    sessionId,
    fisico: cleanNumber(input.fisico),
    totalRetiros: cleanNumber(input.totalRetiros),
    terminalAfirme: cleanNumber(input.terminalAfirme),
    terminalMercadoPago: cleanNumber(input.terminalMercadoPago),
    tipoCambio: cleanNumber(input.tipoCambio)
  };
  const recalculated = recalculateCutReportPayload({ session_id: sessionId, difference: 0 }, payload);
  const cashCalculation = recalculated.payload.cashCalculation || {};
  return {
    ok: true,
    sessionId,
    serverTime: nowIso(),
    difference: recalculated.difference,
    salesSummary: recalculated.payload.salesSummary || {},
    cashCalculation,
    systemCash: cleanNumber(recalculated.payload.sistemaEfectivo ?? recalculated.payload.sistema),
    systemCard: cleanNumber(recalculated.payload.sistemaTarjeta),
    cashExpenses: cleanNumber(cashCalculation.salidasEfectivo),
    nonCashExpenses: cleanNumber(cashCalculation.salidasNoEfectivo),
    cashIncome: cleanNumber(cashCalculation.entradasEfectivo),
    terminalDifference: cleanNumber(recalculated.payload.desfaseTarjetas)
  };
}

function findExistingCutReport({ clientRequestId = "", sessionId = "", cashier = "", date = "", shift = "" } = {}) {
  const cleanClientRequestId = cleanText(clientRequestId);
  if(cleanClientRequestId) {
    const byClientId = db.prepare("SELECT * FROM cut_reports WHERE client_request_id = ? LIMIT 1").get(cleanClientRequestId);
    if(byClientId) return byClientId;
  }
  const cleanSessionId = cleanText(sessionId);
  if(cleanSessionId) {
    const bySession = db.prepare("SELECT * FROM cut_reports WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(cleanSessionId);
    if(bySession) return bySession;
    return null;
  }
  const cleanCashier = cleanText(cashier);
  const cleanDate = cleanText(date);
  const cleanShift = cleanText(shift);
  if(cleanCashier && cleanDate && cleanShift) {
    return db.prepare(`
      SELECT * FROM cut_reports
      WHERE LOWER(cashier) = LOWER(?) AND date = ? AND shift = ?
      ORDER BY id DESC LIMIT 1
    `).get(cleanCashier, cleanDate, cleanShift);
  }
  return null;
}

function saveCutReport(input, sessionContext) {
  const cashier = sessionContext?.user?.name || cleanText(input.cashier || input.cajero);
  const sessionId = sessionContext?.cashSession?.id || cleanText(input.sessionId);
  const closeMode = cleanText(input.closeMode || input.cierreModo || "normal");
  const closeRisk = cleanText(input.closeRisk || input.riesgoCierre || "");
  const authorizedBy = cleanText(input.authorizedBy || input.autorizadoPor || "");
  const clientRequestId = cleanText(input.clientRequestId || input.client_request_id || input.provisionalId || "");
  const date = cleanText(input.date || input.fecha, new Date().toLocaleDateString("es-MX"));
  const shift = cleanText(input.shift || input.turno);
  let payload = { ...input, cashier, cajero: cashier, sessionId, closeMode, closeRisk, authorizedBy, clientRequestId, date, fecha: date, shift, turno: shift };
  let difference = cleanNumber(input.difference ?? input.difEfectivo);
  if(sessionId) {
    const authoritativeCut = recalculateCutReportPayload({ session_id: sessionId, difference }, payload);
    payload = authoritativeCut.payload;
    difference = authoritativeCut.difference;
  }
  const notes = cleanText(input.notes || input.notas);
  const existing = findExistingCutReport({ clientRequestId, sessionId, cashier, date, shift });
  if(existing) {
    const previousPayload = jsonParse(existing.payload_json, {});
    let mergedPayload = { ...previousPayload, ...payload, duplicatePrevented: true, duplicateUpdatedAt: new Date().toISOString() };
    if(sessionId || existing.session_id) {
      const authoritativeCut = recalculateCutReportPayload({ session_id: sessionId || existing.session_id, difference }, mergedPayload);
      mergedPayload = authoritativeCut.payload;
      difference = authoritativeCut.difference;
    }
    db.prepare(`
      UPDATE cut_reports
      SET session_id = ?, client_request_id = COALESCE(NULLIF(?, ''), client_request_id),
          date = ?, cashier = ?, shift = ?, difference = ?, notes = ?, payload_json = ?
      WHERE id = ?
    `).run(
      sessionId || existing.session_id || "",
      clientRequestId,
      date,
      cashier,
      shift,
      difference,
      notes,
      jsonString(mergedPayload),
      existing.id
    );
    logAuditEvent({
      type: "Corte actualizado sin duplicar",
      severity: "Media",
      reference: sessionId || clientRequestId || `${date} ${shift}`,
      amount: difference,
      details: `Se evito duplicar un corte de ${cashier || "S/N"}; se actualizo el registro existente #${existing.id}.`,
      payload: { cutId: existing.id, sessionId, clientRequestId, cashier, date, shift, closeMode }
    }, sessionContext);
    return { id: existing.id, duplicatePrevented: true };
  }

  const result = db.prepare(`
    INSERT INTO cut_reports(session_id, client_request_id, date, cashier, shift, difference, notes, payload_json, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    sessionId,
    clientRequestId,
    date,
    cashier,
    shift,
    difference,
    notes,
    jsonString(payload),
    new Date().toISOString()
  );
  if(closeMode !== "normal") {
    logAuditEvent({
      type: closeMode === "forced" ? "Cierre de caja forzado" : "Cierre provisional confirmado",
      severity: closeMode === "forced" ? "Alta" : "Media",
      reference: sessionId || date,
      amount: difference,
      details: `${cashier || "S/N"} guardo corte en modo ${closeMode}. ${closeRisk || "Sin detalle."}`.trim(),
      payload: { closeMode, closeRisk, authorizedBy, cashier, sessionId, clientRequestId, fecha: date, turno: shift }
    }, sessionContext);
  }
  return { id: result.lastInsertRowid, duplicatePrevented: false };
}

function adjustCutReport(input = {}, sessionContext = null) {
  requirePermission(sessionContext?.user, "admin");
  const cutId = cleanInteger(input.cutId || input.id);
  const sessionIdInput = cleanText(input.sessionId);
  let row = cutId ? db.prepare("SELECT * FROM cut_reports WHERE id = ?").get(cutId) : null;
  if(!row && sessionIdInput) row = db.prepare("SELECT * FROM cut_reports WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(sessionIdInput);
  if(!row) throw httpError(404, "Corte no encontrado.");

  const payload = jsonParse(row.payload_json, {});
  const action = cleanText(input.action || "note").toLowerCase();
  const type = cleanText(input.type || action).toLowerCase();
  const amount = cleanNumber(input.amount);
  const method = cleanText(input.method, "Efectivo");
  const concept = cleanText(input.concept);
  const note = cleanText(input.note || input.notes || input.reason);
  const sessionId = cleanText(row.session_id || payload.sessionId || sessionIdInput);
  const adminName = cleanText(sessionContext?.user?.name, "Administrador");
  const now = new Date().toLocaleString("es-MX");
  const cutReference = cleanText(input.reference || `Ajuste corte #${row.id}`);
  const validMovementType = type === "income" || type === "payment";

  if(validMovementType) {
    if(amount <= 0) throw httpError(400, "Monto mayor a cero requerido.");
    if(!concept) throw httpError(400, "Concepto requerido para ajustar el corte.");
    const clientRequestId = cleanText(input.clientRequestId || `cut-adjust-${row.id}-${crypto.randomBytes(4).toString("hex")}`);
    const existing = db.prepare("SELECT id FROM cash_movements WHERE client_request_id = ?").get(clientRequestId);
    if(!existing) {
      db.prepare(`
        INSERT INTO cash_movements(session_id, type, amount, method, concept, reference, cashier, date, client_request_id)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        sessionId,
        type,
        amount,
        method,
        concept,
        cutReference,
        cleanText(row.cashier || payload.cajero || adminName),
        now,
        clientRequestId
      );
    }
  } else if(!note) {
    throw httpError(400, "Captura una nota para el corte.");
  }

  const adjustment = {
    date: now,
    action: validMovementType ? type : "note",
    amount: validMovementType ? amount : 0,
    method: validMovementType ? method : "",
    concept: validMovementType ? concept : "",
    note,
    reference: cutReference,
    admin: adminName
  };
  const adjustments = Array.isArray(payload.adminAdjustments) ? payload.adminAdjustments : [];
  adjustments.push(adjustment);
  payload.adminAdjustments = adjustments;
  payload.adminAdjustmentUpdatedAt = new Date().toISOString();
  payload.adminAdjustmentUpdatedBy = adminName;

  const previousNotes = cleanText(row.notes || payload.notas);
  const adjustmentNote = `[${now}] ${adminName}: ${validMovementType ? `${type === "income" ? "Entrada" : "Salida"} ${inventorySafeMoney(amount)} ${method} - ${concept}` : "Nota"}${note ? ` | ${note}` : ""}`;
  const nextNotes = [previousNotes, adjustmentNote].filter(Boolean).join("\n");
  payload.notas = nextNotes;
  payload.notes = nextNotes;

  const recalculated = recalculateCutReportPayload(row, payload);
  db.prepare("UPDATE cut_reports SET difference = ?, notes = ?, payload_json = ? WHERE id = ?").run(
    recalculated.difference,
    nextNotes,
    jsonString(recalculated.payload),
    row.id
  );
  logAuditEvent({
    type: validMovementType ? "Ajuste administrativo de corte" : "Nota administrativa de corte",
    severity: validMovementType ? "Alta" : "Media",
    reference: `Corte #${row.id}`,
    amount: validMovementType ? amount : 0,
    details: `${adminName} ${validMovementType ? "agrego movimiento" : "agrego nota"} al corte de ${row.cashier || payload.cajero || "S/N"}. ${note || concept || ""}`.trim(),
    payload: { cutId: row.id, sessionId, adjustment }
  }, sessionContext);
  return { ok: true, cutId: row.id, adjustment, difference: recalculated.difference };
}

function inventorySafeMoney(value) {
  return `$${cleanNumber(value).toLocaleString("es-MX", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}

function backupTimestamp() {
  const now = new Date();
  const pad = value => String(value).padStart(2, "0");
  const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
  return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}-${milliseconds}`;
}

function uniqueBackupBaseName(prefix, suffix = "", destinationDir = BACKUP_DIR) {
  const safeSuffix = cleanText(suffix).replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
  for(let attempt = 0; attempt < 20; attempt += 1) {
    const random = crypto.randomBytes(2).toString("hex");
    const baseName = `${prefix}-${backupTimestamp()}${safeSuffix ? `-${safeSuffix}` : ""}-${random}`;
    if(!fs.existsSync(path.join(destinationDir, `${baseName}.sqlite`)) && !fs.existsSync(path.join(destinationDir, `${baseName}.json`))) return baseName;
  }
  throw httpError(500, "No se pudo reservar un nombre unico para el respaldo.");
}

function directorySizeBytes(directory) {
  if(!fs.existsSync(directory)) return 0;
  return fs.readdirSync(directory, { withFileTypes: true }).reduce((total, entry) => {
    const fullPath = path.join(directory, entry.name);
    return total + (entry.isDirectory() ? directorySizeBytes(fullPath) : fs.statSync(fullPath).size);
  }, 0);
}

function backupTaskMedia(baseName, destinationDir) {
  if(!fs.existsSync(TASK_MEDIA_DIR)) return { mediaPath: "", mediaSizeBytes: 0 };
  const mediaPath = path.join(destinationDir, `${baseName}-task-media`);
  fs.rmSync(mediaPath, { recursive: true, force: true });
  fs.cpSync(TASK_MEDIA_DIR, mediaPath, { recursive: true, force: true });
  return { mediaPath, mediaSizeBytes: directorySizeBytes(mediaPath) };
}

function validateDatabaseBackupFile(filePath) {
  const resolved = path.resolve(filePath);
  if(!fs.existsSync(resolved)) throw httpError(404, "No encontre el archivo de respaldo.");
  const stat = fs.statSync(resolved);
  if(!stat.isFile() || stat.size < 100) throw httpError(400, "El respaldo esta vacio o incompleto.");
  let validationDb = null;
  try {
    validationDb = new DatabaseSync(resolved, { readOnly: true });
    const integrityRows = validationDb.prepare("PRAGMA integrity_check").all();
    const integrityMessages = integrityRows.map(row => cleanText(row.integrity_check || Object.values(row)[0])).filter(Boolean);
    if(integrityMessages.length !== 1 || integrityMessages[0].toLowerCase() !== "ok") {
      throw new Error(`integrity_check: ${integrityMessages.join(" | ") || "sin respuesta"}`);
    }
    const requiredTables = ["products", "sales", "cash_movements", "cash_sessions", "settings", "cashier_users"];
    const tableRows = validationDb.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
    const tables = new Set(tableRows.map(row => cleanText(row.name).toLowerCase()));
    const missingTables = requiredTables.filter(table => !tables.has(table));
    if(missingTables.length) throw new Error(`faltan tablas obligatorias: ${missingTables.join(", ")}`);
    validationDb.prepare("SELECT COUNT(*) AS total FROM products").get();
    validationDb.prepare("SELECT COUNT(*) AS total FROM settings").get();
    return { ok: true, sizeBytes: stat.size, tables: requiredTables };
  } catch(error) {
    throw httpError(400, `El respaldo esta corrupto o no pertenece a T Market POS. ${cleanText(error.message)}`);
  } finally {
    try { validationDb?.close(); } catch {}
  }
}

function createDatabaseBackup(input = {}, sessionContext = null) {
  fs.mkdirSync(BACKUP_DIR, { recursive: true });
  const cashier = cleanText(sessionContext?.user?.name || input.cashier || input.cajero, "Sin usuario");
  const sessionId = cleanText(sessionContext?.cashSession?.id || input.sessionId);
  const baseName = uniqueBackupBaseName("tmarket-backup", cashier || "usuario");
  const fileName = `${baseName}.sqlite`;
  const backupPath = path.join(BACKUP_DIR, fileName);
  const sqlPath = backupPath.replace(/\\/g, "/").replace(/'/g, "''");
  db.exec(`VACUUM INTO '${sqlPath}'`);
  validateDatabaseBackupFile(backupPath);
  const mediaBackup = backupTaskMedia(baseName, BACKUP_DIR);
  const metadata = {
    fileName,
    path: backupPath,
    createdAt: new Date().toLocaleString("es-MX"),
    cashier,
    sessionId,
    reason: cleanText(input.reason || input.motivo, "Respaldo manual"),
    sourceDb: DB_PATH,
    sizeBytes: fs.statSync(backupPath).size,
    ...mediaBackup
  };
  fs.writeFileSync(path.join(BACKUP_DIR, `${baseName}.json`), JSON.stringify(metadata, null, 2), "utf8");
  logAuditEvent({
    type: "Respaldo generado",
    severity: "Baja",
    reference: fileName,
    amount: metadata.sizeBytes,
    details: metadata.reason,
    payload: metadata
  }, sessionContext);
  return metadata;
}

function listDatabaseBackups() {
  fs.mkdirSync(BACKUP_DIR, { recursive: true });
  const roots = [BACKUP_DIR, path.join(BACKUP_DIR, "daily")].filter(dir => fs.existsSync(dir));
  return roots.flatMap(dir => fs.readdirSync(dir)
    .filter(file => file.toLowerCase().endsWith(".sqlite"))
    .map(file => {
      const fullPath = path.join(dir, file);
      const stat = fs.statSync(fullPath);
      const metadataPath = fullPath.replace(/\.sqlite$/i, ".json");
      const metadata = fs.existsSync(metadataPath) ? jsonParse(fs.readFileSync(metadataPath, "utf8"), {}) : {};
      return {
        fileName: file,
        path: fullPath,
        createdAt: metadata.createdAt || stat.mtime.toLocaleString("es-MX"),
        cashier: metadata.cashier || "",
        reason: metadata.reason || "Respaldo",
        category: metadata.category || (dir.endsWith(`${path.sep}daily`) ? "daily" : "manual"),
        sizeBytes: stat.size,
        mtimeMs: stat.mtimeMs
      };
    }))
    .sort((a, b) => b.mtimeMs - a.mtimeMs);
}

function isPathInside(parentDir, targetPath) {
  const parent = path.resolve(parentDir);
  const target = path.resolve(targetPath);
  return target === parent || target.startsWith(parent + path.sep);
}

function resolveDatabaseBackupPath(input = {}) {
  const rawValue = cleanText(input.path || input.fileName || input.backup);
  const fileName = path.basename(rawValue);
  if(!fileName || !fileName.toLowerCase().endsWith(".sqlite")) {
    throw httpError(400, "Selecciona un respaldo valido.");
  }

  const requestedCategory = cleanText(input.category || input.restoreCategory || input.originalCategory || input.original_category);
  const listedMatches = listDatabaseBackups().filter(item => {
    const sameName = item.fileName === fileName || path.basename(item.path || "") === fileName;
    const sameCategory = !requestedCategory || item.category === requestedCategory;
    return sameName && sameCategory;
  });
  const candidates = [
    ...listedMatches.map(item => item.path),
    path.join(BACKUP_DIR, fileName),
    path.join(BACKUP_DIR, "daily", fileName)
  ];

  for(const candidate of candidates) {
    if(!candidate) continue;
    const resolved = path.resolve(candidate);
    if(!isPathInside(BACKUP_DIR, resolved)) continue;
    if(fs.existsSync(resolved)) return resolved;
  }
  throw httpError(404, "No encontre el respaldo seleccionado.");
}

function ensureDailyDatabaseBackup() {
  try {
    fs.mkdirSync(BACKUP_DIR, { recursive: true });
    const today = new Date().toLocaleDateString("en-CA");
    const markerPath = path.join(BACKUP_DIR, `.daily-${today}.ok`);
    if(fs.existsSync(markerPath)) return;

    const dailyDir = path.join(BACKUP_DIR, "daily");
    fs.mkdirSync(dailyDir, { recursive: true });
    const baseName = uniqueBackupBaseName("tmarket-daily", "", dailyDir);
    const fileName = `${baseName}.sqlite`;
    const backupPath = path.join(dailyDir, fileName);
    const sqlPath = backupPath.replace(/\\/g, "/").replace(/'/g, "''");
    db.exec(`VACUUM INTO '${sqlPath}'`);
    const mediaBackup = backupTaskMedia(baseName, dailyDir);

    const metadata = {
      fileName,
      path: backupPath,
      createdAt: new Date().toLocaleString("es-MX"),
      cashier: "Sistema",
      sessionId: "",
      reason: "Respaldo diario automatico al iniciar servidor",
      sourceDb: DB_PATH,
      sizeBytes: fs.statSync(backupPath).size,
      category: "daily",
      ...mediaBackup
    };
    fs.writeFileSync(path.join(dailyDir, fileName.replace(/\.sqlite$/i, ".json")), JSON.stringify(metadata, null, 2), "utf8");
    fs.writeFileSync(markerPath, JSON.stringify({ ok: true, fileName, createdAt: metadata.createdAt }, null, 2), "utf8");

    logAuditEvent({
      type: "Respaldo diario creado",
      severity: "Baja",
      reference: fileName,
      amount: metadata.sizeBytes,
      details: "Respaldo automatico creado al iniciar el servidor.",
      payload: metadata
    }, null);
    pruneDailyBackups(30);
  } catch(error) {
    try {
      logAuditEvent({
        type: "Respaldo diario fallido",
        severity: "Alta",
        reference: "daily-backup",
        details: error.message || "No se pudo crear respaldo diario.",
        payload: { error: error.message || String(error) }
      }, null);
    } catch (_) {}
    console.warn("No se pudo crear respaldo diario:", error.message || error);
  }
}

function pruneDailyBackups(keep = 30) {
  const dailyDir = path.join(BACKUP_DIR, "daily");
  if(!fs.existsSync(dailyDir)) return;
  const rows = fs.readdirSync(dailyDir)
    .filter(file => /^tmarket-daily-\d{8}(?:-\d{6}-\d{3}-[a-f0-9]{4})?\.sqlite$/i.test(file))
    .map(file => {
      const fullPath = path.join(dailyDir, file);
      return { file, fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs };
    })
    .sort((a, b) => b.mtimeMs - a.mtimeMs);
  rows.slice(keep).forEach(row => {
    try {
      fs.unlinkSync(row.fullPath);
      const meta = row.fullPath.replace(/\.sqlite$/i, ".json");
      if(fs.existsSync(meta)) fs.unlinkSync(meta);
      const mediaDir = row.fullPath.replace(/\.sqlite$/i, "-task-media");
      if(fs.existsSync(mediaDir)) fs.rmSync(mediaDir, { recursive: true, force: true });
    } catch (_) {}
  });
}


function resetDatabase(input = {}, sessionContext = null) {
  const backup = createDatabaseBackup({ ...input, reason: cleanText(input.reason, "Respaldo automatico antes de reiniciar base") }, sessionContext);
  db.exec("BEGIN IMMEDIATE");
  try {
    [
      "sale_items", "sales", "cash_movements", "cash_sessions", "inventory_adjustments", "purchase_notes",
      "shopping_list", "shopping_supplier_notes", "department_labels", "department_inventory", "quotations", "cut_reports",
      "utility_expenses", "cashier_tasks", "cashier_users", "products", "suppliers", "departments", "settings"
    ].forEach(table => db.prepare(`DELETE FROM ${table}`).run());
    db.prepare("DELETE FROM sqlite_sequence WHERE name NOT IN ('audit_events')").run();
    db.exec("COMMIT");
    fs.rmSync(TASK_MEDIA_DIR, { recursive: true, force: true });
    fs.mkdirSync(TASK_MEDIA_DIR, { recursive: true });
    seedDatabase();
    hardenDefaultDemoCashier();
    logAuditEvent({
      type: "Base de datos reiniciada",
      severity: "Critica",
      reference: backup.fileName,
      details: "Se borro la informacion operativa y se cargaron datos iniciales. Se genero respaldo previo.",
      payload: { backup }
    }, sessionContext);
    return { backup };
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function restoreDatabaseBackup(input = {}, sessionContext = null) {
  const backupPath = resolveDatabaseBackupPath(input);
  const fileName = path.basename(backupPath);
  const validation = validateDatabaseBackupFile(backupPath);
  const stagedRestorePath = path.join(path.dirname(DB_PATH), `.tmarket-restore-${process.pid}-${Date.now()}-${crypto.randomBytes(2).toString("hex")}.sqlite`);
  const backupBaseName = fileName.replace(/\.sqlite$/i, "");
  const backupMediaPath = path.join(path.dirname(backupPath), `${backupBaseName}-task-media`);
  const stagedMediaPath = path.join(path.dirname(DB_PATH), `.tmarket-media-restore-${process.pid}-${Date.now()}-${crypto.randomBytes(2).toString("hex")}`);
  fs.copyFileSync(backupPath, stagedRestorePath);
  if(fs.existsSync(backupMediaPath)) fs.cpSync(backupMediaPath, stagedMediaPath, { recursive: true, force: true });
  try {
    validateDatabaseBackupFile(stagedRestorePath);
  } catch(error) {
    try { fs.rmSync(stagedRestorePath, { force: true }); } catch {}
    throw error;
  }
  const beforeRestore = createDatabaseBackup({ reason: `Respaldo automatico antes de restaurar ${fileName}` }, sessionContext);
  logAuditEvent({
    type: "Restauracion de base solicitada",
    severity: "Critica",
    reference: fileName,
    details: "La base se reemplazara por el respaldo seleccionado. El servidor se reiniciara.",
    payload: { restoreFrom: backupPath, beforeRestore, validation }
  }, sessionContext);
  const restoreTarget = DB_PATH;
  setTimeout(() => {
    try {
      db.close();
      const wal = `${restoreTarget}-wal`;
      const shm = `${restoreTarget}-shm`;
      fs.copyFileSync(stagedRestorePath, restoreTarget);
      validateDatabaseBackupFile(restoreTarget);
      if(fs.existsSync(stagedMediaPath)) {
        fs.rmSync(TASK_MEDIA_DIR, { recursive: true, force: true });
        fs.renameSync(stagedMediaPath, TASK_MEDIA_DIR);
      }
      if(fs.existsSync(wal)) fs.rmSync(wal, { force: true });
      if(fs.existsSync(shm)) fs.rmSync(shm, { force: true });
    } finally {
      try { fs.rmSync(stagedRestorePath, { force: true }); } catch {}
      try { fs.rmSync(stagedMediaPath, { recursive: true, force: true }); } catch {}
      process.exit(0);
    }
  }, 300);
  return { restoredFrom: backupPath, beforeRestore, restartRequired: true };
}

function restoreUploadedDatabaseBackup(input = {}, sessionContext = null) {
  const originalName = path.basename(cleanText(input.fileName || input.name || "respaldo-manual.sqlite"));
  if(!originalName || !originalName.toLowerCase().endsWith(".sqlite")) {
    throw httpError(400, "Selecciona un archivo .sqlite generado como respaldo de T Market POS.");
  }
  const dataBase64 = cleanText(input.dataBase64 || input.base64);
  if(!dataBase64) throw httpError(400, "El archivo de respaldo llego vacio.");

  let buffer;
  try {
    buffer = Buffer.from(dataBase64.replace(/^data:.*?;base64,/, ""), "base64");
  } catch {
    throw httpError(400, "No pude leer el archivo de respaldo.");
  }
  if(buffer.length < 32) throw httpError(400, "El archivo de respaldo no parece valido.");
  if(buffer.subarray(0, 16).toString("utf8") !== "SQLite format 3\0") {
    throw httpError(400, "El archivo seleccionado no parece ser una base SQLite valida.");
  }

  fs.mkdirSync(BACKUP_DIR, { recursive: true });
  const safeName = originalName.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/\.sqlite$/i, "");
  const importedBaseName = uniqueBackupBaseName("tmarket-importado", safeName || "respaldo");
  const importedName = `${importedBaseName}.sqlite`;
  const importedPath = path.join(BACKUP_DIR, importedName);
  fs.writeFileSync(importedPath, buffer);
  try {
    validateDatabaseBackupFile(importedPath);
  } catch(error) {
    try { fs.rmSync(importedPath, { force: true }); } catch {}
    throw error;
  }
  const metadata = {
    fileName: importedName,
    path: importedPath,
    createdAt: new Date().toLocaleString("es-MX"),
    cashier: cleanText(sessionContext?.user?.name, "Administrador"),
    sessionId: cleanText(sessionContext?.cashSession?.id),
    reason: `Respaldo importado manualmente desde archivo: ${originalName}`,
    sourceFile: originalName,
    sizeBytes: buffer.length
  };
  fs.writeFileSync(importedPath.replace(/\.sqlite$/i, ".json"), JSON.stringify(metadata, null, 2), "utf8");
  logAuditEvent({
    type: "Respaldo importado manualmente",
    severity: "Critica",
    reference: importedName,
    amount: metadata.sizeBytes,
    details: metadata.reason,
    payload: metadata
  }, sessionContext);
  return restoreDatabaseBackup({ fileName: importedName }, sessionContext);
}

function replaceShoppingList(items, notes = {}, sessionContext = null, options = {}) {
  const cleanItems = Array.isArray(items) ? items : [];
  const previousItems = db.prepare(`
    SELECT sl.sku, sl.quantity, sl.supplier AS selected_supplier, p.name, p.supplier
    FROM shopping_list sl
    LEFT JOIN products p ON p.sku = sl.sku
  `).all();
  const nextSkus = new Set(cleanItems.map(item => cleanText(item.sku).toUpperCase()).filter(Boolean));
  const removedItems = previousItems.filter(item => !nextSkus.has(item.sku));
  db.exec("BEGIN IMMEDIATE");
  try {
    db.prepare("DELETE FROM shopping_list").run();
    db.prepare("DELETE FROM shopping_supplier_notes").run();
    const insert = db.prepare("INSERT OR REPLACE INTO shopping_list(sku, quantity, supplier, updated_at) VALUES (?, ?, ?, ?)");
    cleanItems.forEach(item => {
      const sku = cleanText(item.sku).toUpperCase();
      if(!sku) return;
      insert.run(sku, Math.max(1, cleanInteger(item.quantity, 1)), cleanText(item.supplier || item.selectedSupplier), nowIso());
    });
    const insertNote = db.prepare("INSERT OR REPLACE INTO shopping_supplier_notes(supplier, note, updated_at) VALUES (?, ?, ?)");
    if(notes && typeof notes === "object") {
      Object.entries(notes).forEach(([supplier, note]) => {
        const cleanSupplier = cleanText(supplier);
        const cleanNote = cleanText(note);
        if(cleanSupplier && cleanNote) insertNote.run(cleanSupplier, cleanNote, nowIso());
      });
    }
    db.exec("COMMIT");
    if(!options.suppressRemovedAudit) {
      removedItems.forEach(item => logAuditEvent({
        type: "Producto borrado de lista de compra",
        severity: "Media",
        reference: item.sku,
        amount: cleanNumber(item.quantity),
        details: `${item.name || item.sku} fue quitado de la lista de compra. Proveedor: ${item.supplier || "S/N"}.`,
        payload: item
      }, sessionContext));
    }
  } catch (error) {
    db.exec("ROLLBACK");
    throw error;
  }
}

function httpError(status, message) {
  const error = new Error(message);
  error.status = status;
  return error;
}

function send(res, status, data, headers = {}) {
  const body = Buffer.isBuffer(data) ? data : String(data ?? "");
  res.writeHead(status, {
    "Access-Control-Allow-Origin": ALLOWED_CORS_ORIGIN,
    "Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization, X-TMarket-Token, X-TMarket-Client",
    ...headers
  });
  res.end(body);
}

function sendJson(res, status, data) {
  send(res, status, JSON.stringify(data), { "Content-Type": "application/json; charset=utf-8" });
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    let size = 0;
    req.on("data", chunk => {
      size += chunk.length;
      if(size > 120 * 1024 * 1024) {
        reject(httpError(413, "El cuerpo de la peticion es demasiado grande."));
        req.destroy();
        return;
      }
      chunks.push(chunk);
    });
    req.on("end", () => {
      const raw = Buffer.concat(chunks).toString("utf8");
      if(!raw) return resolve({});
      try {
        resolve(JSON.parse(raw));
      } catch {
        reject(httpError(400, "JSON invalido."));
      }
    });
    req.on("error", reject);
  });
}

function contentTypeFor(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  if(ext === ".html") return "text/html; charset=utf-8";
  if(ext === ".js") return "application/javascript; charset=utf-8";
  if(ext === ".webmanifest") return "application/manifest+json; charset=utf-8";
  if(ext === ".json") return "application/json; charset=utf-8";
  if(ext === ".css") return "text/css; charset=utf-8";
  if(ext === ".png") return "image/png";
  if(ext === ".jpg" || ext === ".jpeg" || ext === ".jfif") return "image/jpeg";
  if(ext === ".ico") return "image/x-icon";
  if(ext === ".svg") return "image/svg+xml";
  return "application/octet-stream";
}

async function handleApi(req, res, url) {
  refreshDatabaseConnectionIfNeeded();
  const route = url.pathname.replace(/^\/api/, "") || "/";
  if(req.method === "GET" && route === "/health") {
    return sendJson(res, 200, {
      ok: true,
      app: "TMarketPOS",
      name: "T Market POS Server",
      build: APP_BUILD,
      serverBootId: SERVER_BOOT_ID,
      serverTime: nowIso()
    });
  }
  if(req.method === "GET" && route === "/network-info") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, getConnectionInfo());
  }
  if(req.method === "GET" && route === "/session") {
    const sessionContext = getSessionContext(req);
    return sendJson(res, 200, { user: sessionContext.user, cashSession: sessionContext.cashSession });
  }
  if(req.method === "GET" && route === "/license") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, getLicenseStatus());
  }
  if(req.method === "GET" && route.startsWith("/state/")) {
    const sessionContext = getSessionContext(req);
    const moduleName = route.slice("/state/".length).toLowerCase();
    if(moduleName === "audit" || moduleName === "reports") requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, buildStateModuleResponse(sessionContext, moduleName, url.searchParams));
  }
  if(req.method === "GET" && route === "/state") {
    const sessionContext = getSessionContext(req);
    const light = url.searchParams.get("light") === "1";
    return sendJson(res, 200, getState(sessionContext, light ? {
      saleLimit: 80,
      movementLimit: 120,
      adjustmentLimit: 120,
      departmentLabelLimit: 80,
      auditLimit: 80
    } : {}));
  }
  if(req.method === "GET" && route === "/backups") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, { ok: true, backups: listDatabaseBackups() });
  }

  if(req.method === "GET" && route === "/settings/export") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, { ok: true, exportedAt: nowIso(), appBuild: APP_BUILD, settings: getSettings(), printProfiles: listPrintProfiles() });
  }

  if(req.method === "GET" && route === "/print-profiles") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, { ok: true, profiles: listPrintProfiles() });
  }

  if(req.method === "GET" && route === "/session/current") {
    const sessionContext = getSessionContext(req);
    const activity = getCashSessionActivitySummary(sessionContext.cashSession?.id || "");
    return sendJson(res, 200, { ok: true, user: sessionContext.user, cashSession: sessionContext.cashSession, activity });
  }

  if(req.method === "GET" && route === "/tasks/detail") {
    const sessionContext = getSessionContext(req);
    return sendJson(res, 200, { ok: true, task: getCashierTaskDetail(url.searchParams.get("id"), sessionContext) });
  }

  if(req.method === "GET" && route === "/internal-withdrawals") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "withdrawals");
    return sendJson(res, 200, { ok: true, withdrawals: listInternalWithdrawals(sessionContext, url.searchParams.get("sessionId")) });
  }

  if(req.method === "GET" && route === "/cuts/preview") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, permissionForRoute(route));
    return sendJson(res, 200, buildCutPreview(Object.fromEntries(url.searchParams.entries()), sessionContext));
  }

  if(req.method === "POST" && route === "/session/confirm-pin") {
    const sessionContext = getSessionContext(req);
    const sessionPinBody = await readBody(req);
    const confirmedUser = validateCurrentSessionPin(sessionContext, sessionPinBody.pin || sessionPinBody.password);
    const activity = getCashSessionActivitySummary(sessionContext.cashSession?.id || "");
    return sendJson(res, 200, { ok: true, user: confirmedUser, cashSession: sessionContext.cashSession, activity });
  }

  if(req.method === "GET" && route === "/diagnostics") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, getServerDiagnostics(sessionContext));
  }

  if(req.method === "GET" && route === "/support-package") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, getSupportPackage(sessionContext));
  }

  if(req.method === "GET" && route === "/cash-sessions/active") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, { ok: true, sessions: listCashSessionsForMonitor() });
  }

  if(req.method === "GET" && route === "/audit-events/query") {
    const sessionContext = getSessionContext(req);
    requirePermission(sessionContext.user, "admin");
    return sendJson(res, 200, queryAuditEvents({
      limit: url.searchParams.get("limit"),
      offset: url.searchParams.get("offset"),
      status: url.searchParams.get("status"),
      exact: url.searchParams.get("exact"),
      from: url.searchParams.get("from"),
      to: url.searchParams.get("to"),
      q: url.searchParams.get("q"),
      sort: url.searchParams.get("sort"),
      direction: url.searchParams.get("direction")
    }));
  }

  const body = ["POST", "PUT"].includes(req.method) ? await readBody(req) : {};
  if(req.method === "POST" && route === "/login") {
    return sendJson(res, 200, loginUser(body));
  }
  if(req.method === "POST" && route === "/setup-admin") {
    return sendJson(res, 200, setupInitialAdmin(body));
  }
  if(req.method === "POST" && route === "/logout") {
    const header = req.headers.authorization || "";
    const token = header.startsWith("Bearer ") ? header.slice(7).trim() : req.headers["x-tmarket-token"];
    if(token && sessions.has(token)) {
      const sessionContext = sessions.get(token);
      const activity = getCashSessionActivitySummary(sessionContext.cashSession?.id || "");
      if(activity.hasActivity && !activity.hasCut && body.forceClose !== true) {
        const error = httpError(409, "Esta sesion tiene ventas o movimientos de dinero y no tiene corte final. Haz el corte antes de salir o autoriza cierre forzado.");
        error.details = { requiresCut: true, canForceClose: userHasPermission(sessionContext.user, "forceCloseSessions"), activity };
        throw error;
      }
      if(activity.hasActivity && !activity.hasCut && body.forceClose === true && !canForceCloseSessions(sessionContext.user, body.adminPin || body.authorizationPin || body.pin || "")) {
        throw httpError(403, "Solo administrador o usuario autorizado puede forzar cierre sin corte.");
      }
      closeCashSession(sessionContext, {
        status: activity.hasActivity && !activity.hasCut && body.forceClose === true ? "Cerrada sin corte" : "Cerrada",
        reason: cleanText(body.reason || (body.forceClose ? "Cierre forzado sin corte" : "")),
        closedBy: sessionContext.user?.name || ""
      });
      sessions.delete(token);
      return sendJson(res, 200, { ok: true, closed: true, activity });
    }
    return sendJson(res, 200, { ok: true, closed: false });
  }

  const authSession = ["POST", "PUT"].includes(req.method) ? getSessionContext(req) : null;
  const authUser = authSession?.user || null;
  requirePermission(authUser, permissionForRoute(route));

  if(req.method === "POST" && route === "/cash-sessions/force-close") {
    requirePermission(authUser, "forceCloseSessions");
    const closedSession = closeCashSessionById(body.sessionId || body.id, {
      status: cleanText(body.status || "Cerrada sin corte"),
      reason: cleanText(body.reason || "Cierre forzado desde monitoreo de sesiones"),
      closedBy: authUser?.name || "Administrador"
    });
    return sendJson(res, 200, { ok: true, closedSession, sessions: listCashSessionsForMonitor() });
  }

  if(req.method === "POST" && route === "/products") {
    upsertProduct(body.product || body, authSession);
    scanSkuAmbiguitiesAndAudit(authSession);
    return sendJson(res, 200, getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80, auditLimit: 80 }));
  }
  if(req.method === "POST" && route === "/products/status") {
    updateProductStatus(body.product || body, authSession);
    return sendJson(res, 200, getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80, auditLimit: 80 }));
  }
  if(req.method === "POST" && route === "/products/delete") {
    deleteProduct(body.product || body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/products/bulk") {
    const products = Array.isArray(body.products) ? body.products : [];
    const priceRisk = analyzeBulkPriceResetRisk(products);
    if(priceRisk.sale >= 5 || priceRisk.cost >= 20 || priceRisk.sale > products.length * 0.1) {
      throw httpError(400, `Importacion bloqueada: ${priceRisk.sale} precios de venta y ${priceRisk.cost} costos intentan ponerse en cero. Revisa columnas del Excel. Ejemplos: ${priceRisk.skus.join(", ")}`);
    }
    db.exec("BEGIN IMMEDIATE");
    try {
      products.forEach(upsertProduct);
      db.exec("COMMIT");
    } catch (error) {
      db.exec("ROLLBACK");
      throw error;
    }
    scanSkuAmbiguitiesAndAudit(authSession);
    return sendJson(res, 200, getState(authSession));
  }

  if(req.method === "POST" && route === "/catalog/sku-ambiguities/scan") {
    const result = scanSkuAmbiguitiesAndAudit(authSession);
    return sendJson(res, 200, { ok: true, partialState: true, skuAmbiguityGroups: result.groups, auditEvents: result.auditEvents });
  }
  if(req.method === "POST" && route === "/catalog/departments") {
    upsertDepartment(body.name);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/catalog/departments/update") {
    updateDepartment(body.department || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/catalog/departments/delete") {
    deleteDepartment(body.department || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/catalog/suppliers") {
    upsertSupplier(body.supplier || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/inventory-adjustments") {
    if(["remove", "waste", "set"].includes(cleanText(body.type))) requirePermission(authUser, "negativeAdjustments");
    const adjustmentId = createInventoryAdjustment(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80, auditLimit: 80 }), adjustmentId });
  }
  if(req.method === "POST" && route === "/department-inventory") {
    requireActiveLicenseForModule("internalLabels", "etiquetas internas");
    upsertDepartmentInventory(body.product || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/department-labels") {
    requireActiveLicenseForModule("internalLabels", "etiquetas internas");
    const departmentLabel = createDepartmentLabel(body.label || body);
    return sendJson(res, 200, { ...getState(authSession), departmentLabel });
  }
  if(req.method === "POST" && route === "/cash-movements") {
    const movementId = createCashMovement(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), movementId });
  }
  if(req.method === "POST" && route === "/customers") {
    const customerId = upsertCustomer(body.customer || body);
    return sendJson(res, 200, { ...getState(authSession), customerId });
  }
  if(req.method === "POST" && route === "/customers/delete") {
    deleteCustomer(body.customer || body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/customers/payment") {
    const customerMovementId = registerCustomerPayment(body.payment || body, authSession);
    return sendJson(res, 200, { ...getState(authSession), customerMovementId });
  }
  if(req.method === "POST" && route === "/sales") {
    requireActiveLicenseForModule("posSales", "punto de venta");
    const sale = createSale(body, authSession);
    return sendJson(res, 200, {
      ok: true,
      partialState: true,
      sale,
      productUpdates: sale.updatedProducts || [],
      saleReports: loadSaleReportsPage(1, 0),
      cashSession: authSession.cashSession,
      offlineStockAlerts: sale.offlineStockAlerts || []
    });
  }
  if(req.method === "POST" && route === "/sales/delivery-status") {
    const delivery = updateSaleDeliveryStatus(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), delivery });
  }
  if(req.method === "POST" && route === "/quotations") {
    const quotation = createQuotation(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), quotation });
  }
  if(req.method === "POST" && route === "/quotations/delete") {
    const quotation = deleteQuotation(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), quotation });
  }
  if(req.method === "POST" && route === "/quotations/status") {
    const quotation = updateQuotationStatus(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), quotation });
  }
  if(req.method === "POST" && route === "/sales/cancel") {
    const sale = cancelSale(body, authSession);
    return sendJson(res, 200, { ...getState(authSession, { saleLimit: 80, movementLimit: 120, adjustmentLimit: 120, departmentLabelLimit: 80 }), sale });
  }
  if(req.method === "POST" && route === "/mobile/inventory-adjustment") {
    requireActiveLicenseForModule("appInventory", "app Android de inventario");
    createMobileInventoryAdjustment(body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/purchase-notes") {
    requirePurchaseDocumentAction(body.sourceOrderId || body.source_order_id ? "receive" : "create", authSession, body);
    const purchaseNoteId = createPurchaseNote(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), purchaseNoteId });
  }
  if(req.method === "POST" && route === "/purchase-notes/review") {
    requirePermission(authUser, "admin");
    const purchaseNote = markPurchaseNoteAdminReview(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), purchaseNote });
  }
  if(req.method === "POST" && route === "/purchase-notes/unreview") {
    requirePermission(authUser, "admin");
    const purchaseNote = unlockPurchaseNoteAdminReview(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), purchaseNote });
  }
  if(req.method === "POST" && route === "/purchase-orders/update") {
    updatePurchaseOrder(body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/purchase-orders/receive") {
    requirePurchaseDocumentAction("receive", authSession, body);
    receivePurchaseOrder(body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/purchase-orders/delete") {
    deletePurchaseOrder(body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/shopping-list") {
    replaceShoppingList(body.items || body.shoppingList, body.notes || body.shoppingListNotes, authSession, {
      suppressRemovedAudit: Boolean(body.suppressRemovedAudit || body.reason === "purchaseOrder" || body.reason === "pedido")
    });
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/cashiers") {
    upsertCashierUser(body.user || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/cashiers/delete") {
    deleteCashierUser(body.user || body, authUser, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/tasks") {
    const taskId = upsertCashierTask(body.task || body);
    return sendJson(res, 200, { ok: true, taskId, cashierTasks: listCashierTaskSummaries(authSession) });
  }
  if(req.method === "POST" && route === "/tasks/progress") {
    const taskId = updateCashierTaskProgress(body.task || body, authSession);
    return sendJson(res, 200, { ok: true, taskId, cashierTasks: listCashierTaskSummaries(authSession) });
  }
  if(req.method === "POST" && route === "/tasks/delete") {
    deleteCashierTask(body.task || body, authSession);
    return sendJson(res, 200, { ok: true, cashierTasks: listCashierTaskSummaries(authSession) });
  }
  if(req.method === "POST" && route === "/tasks/archive") {
    const taskId = archiveCashierTask(body.task || body, authSession);
    return sendJson(res, 200, { ok: true, taskId, cashierTasks: listCashierTaskSummaries(authSession) });
  }
  if(req.method === "POST" && route === "/utility-expenses") {
    upsertUtilityExpense(body.expense || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/utility-expenses/delete") {
    deleteUtilityExpense(body.expense || body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/audit-expense-reconciliation/run") {
    const result = reconcileDailyExpenseOutputs(body.date || body.dateKey || getPreviousLocalDateKey(), { logAlert: body.logAlert !== false });
    return sendJson(res, 200, { ok: true, reconciliation: result });
  }
  if(req.method === "POST" && route === "/settings") {
    createSettingsSnapshot({ reason: cleanText(body.reason || body.motivo, "Antes de guardar configuracion") }, authSession);
    saveSettings(body.settings || body);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/internal-withdrawals") {
    const withdrawalId = createInternalWithdrawal(body.withdrawal || body, authSession);
    return sendJson(res, 200, { ok: true, withdrawalId, withdrawals: listInternalWithdrawals(authSession) });
  }
  if(req.method === "POST" && route === "/internal-withdrawals/delete") {
    deleteInternalWithdrawal(body, authSession);
    return sendJson(res, 200, { ok: true, withdrawals: listInternalWithdrawals(authSession) });
  }
  if(req.method === "POST" && route === "/settings/import") {
    const result = importSettingsConfiguration(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), settingsImport: result });
  }
  if(req.method === "POST" && route === "/settings/snapshot") {
    const snapshot = createSettingsSnapshot({ reason: cleanText(body.reason || body.motivo, "Snapshot manual de configuracion") }, authSession);
    return sendJson(res, 200, { ...getState(authSession), settingsSnapshot: rowToSettingsSnapshot(snapshot) });
  }
  if(req.method === "POST" && route === "/settings/restore") {
    const restored = restoreSettingsSnapshot(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), settingsRestored: restored });
  }
  if(req.method === "POST" && route === "/print-profiles") {
    const profile = savePrintProfile(body.profile || body, authSession);
    return sendJson(res, 200, { ...getState(authSession), printProfile: profile });
  }
  if(req.method === "POST" && route === "/print-profiles/apply") {
    const profile = applyPrintProfile(body.profile || body, authSession);
    return sendJson(res, 200, { ...getState(authSession), printProfile: profile });
  }
  if(req.method === "POST" && route === "/admin/validate-pin") {
    const adminUser = validateAdminPin(body.pin || body.adminPin || body.authorizationCode);
    if(!adminUser) throw httpError(403, "PIN de administrador invalido.");
    return sendJson(res, 200, { ok: true, admin: { name: adminUser.name, role: adminUser.role } });
  }
  if(req.method === "POST" && route === "/authorization/permission") {
    const requested = Array.isArray(body.permissions) ? body.permissions : [body.permission];
    const authorizedUser = validateUserPinForAnyPermission(body.user || body.name || body.authorizationUser, body.pin || body.password || body.authorizationPin, requested);
    if(!authorizedUser) throw httpError(403, "Usuario o PIN sin permiso para esta accion.");
    const grantedPermissions = requested.map(cleanText).filter(permission => userHasPermission(authorizedUser, permission));
    if(!grantedPermissions.length) throw httpError(403, "Usuario o PIN sin permiso para esta accion.");
    return sendJson(res, 200, {
      ok: true,
      user: { name: authorizedUser.name, role: authorizedUser.role },
      permissions: grantedPermissions,
      expiresInSeconds: 300
    });
  }
  if(req.method === "POST" && route === "/cuts") {
    const cutResult = saveCutReport(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), cutResult });
  }
  if(req.method === "POST" && route === "/cuts/adjust") {
    const cutAdjustment = adjustCutReport(body, authSession);
    return sendJson(res, 200, { ...getState(authSession), cutAdjustment });
  }
  if(req.method === "POST" && route === "/backup") {
    const backup = createDatabaseBackup(body, authSession);
    return sendJson(res, 200, { ok: true, backup });
  }
  if(req.method === "POST" && route === "/license") {
    const license = saveLicense(body, authSession);
    return sendJson(res, 200, license);
  }
  if(req.method === "POST" && route === "/license/validate-online") {
    const online = await validateLicenseOnlineNow("manual");
    return sendJson(res, 200, { ok: true, online, license: getLicenseStatus() });
  }
  if(req.method === "POST" && route === "/audit-events") {
    logAuditEvent(body.event || body, authSession);
    return sendJson(res, 200, getState(authSession));
  }
  if(req.method === "POST" && route === "/audit-events/review") {
    const auditEvent = updateAuditEventReviewStatus(body, authSession);
    return sendJson(res, 200, { ok: true, partialState: true, auditEvent, auditEvents: [auditEvent] });
  }
  if(req.method === "POST" && route === "/database/reset") {
    const result = resetDatabase(body, authSession);
    return sendJson(res, 200, { ok: true, ...result, state: getState(authSession) });
  }
  if(req.method === "POST" && route === "/database/restore") {
    const result = restoreDatabaseBackup(body, authSession);
    return sendJson(res, 200, { ok: true, ...result });
  }
  if(req.method === "POST" && route === "/database/restore-upload") {
    const result = restoreUploadedDatabaseBackup(body, authSession);
    return sendJson(res, 200, { ok: true, ...result });
  }

  throw httpError(404, "Ruta API no encontrada.");
}

function serveFile(res, filePath) {
  if(!fs.existsSync(filePath)) throw httpError(404, `Archivo no encontrado: ${filePath}`);
  send(res, 200, fs.readFileSync(filePath), { "Content-Type": contentTypeFor(filePath) });
}

function serveProductPhoto(res, encodedSku) {
  const sku = cleanText(decodeURIComponent(encodedSku || "")).toUpperCase();
  const row = sku ? db.prepare("SELECT photo FROM products WHERE sku = ?").get(sku) : null;
  const photo = String(row?.photo || "");
  if(!photo) throw httpError(404, "Imagen de producto no encontrada.");
  const dataMatch = photo.match(/^data:([^;,]+);base64,(.+)$/s);
  if(dataMatch) return send(res, 200, Buffer.from(dataMatch[2], "base64"), { "Content-Type": dataMatch[1], "Cache-Control": "public, max-age=86400" });
  if(/^https?:\/\//i.test(photo) || photo.startsWith("/")) return send(res, 302, "", { Location: photo, "Cache-Control": "no-store" });
  throw httpError(404, "Formato de imagen no disponible.");
}

async function handleRequest(req, res) {
  try {
    const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
    if(req.method === "OPTIONS") return send(res, 204, "");
    if(url.pathname.startsWith("/api")) return await handleApi(req, res, url);
    if(req.method === "GET" && url.pathname.startsWith("/product-photo/")) return serveProductPhoto(res, url.pathname.slice("/product-photo/".length));
    if(url.pathname === "/favicon.ico") return send(res, 204, "");
    if(url.pathname === "/" || url.pathname === "/index.html") return serveFile(res, APP_HTML);
    if(url.pathname === "/tmarket-logo.jfif") return serveFile(res, LOGO_PATH);

    const safePath = path.normalize(decodeURIComponent(url.pathname)).replace(/^(\.\.[/\\])+/, "");
    const localFile = path.join(path.dirname(APP_HTML), safePath);
    return serveFile(res, localFile);
  } catch (error) {
    const status = error.status || 500;
    const message = status >= 500 ? `Error del servidor: ${error.message}` : error.message;
    if(status >= 500) console.error(error);
    sendJson(res, status, { ok: false, error: message, ...(error.details ? error.details : {}) });
  }
}

initSchema();
migrateLegacyTaskMedia();
seedDatabase();
ensureInitialSettingsSnapshot();
hardenDefaultDemoCashier();
dbSignature = getDatabaseSignature();
scheduleOnlineLicenseValidation();

const server = http.createServer(handleRequest);
server.listen(PORT, HOST, () => {
  console.log(`T Market TPV servidor listo`);
  console.log(`URL local:    http://localhost:${PORT}`);
  console.log(`Red local:    http://IP-DE-ESTA-COMPUTADORA:${PORT}`);
  console.log(`Base SQLite:  ${DB_PATH}`);
  console.log(`HTML servido: ${APP_HTML}`);
  setTimeout(() => ensureDailyDatabaseBackup(), 3000);
  setTimeout(() => {
    try { reconcileDailyExpenseOutputs(getPreviousLocalDateKey(), { logAlert: true }); }
    catch(error) { console.warn("No se pudo revisar la conciliacion de gastos anterior:", error.message || error); }
  }, 30000).unref?.();
  scheduleDailyExpenseReconciliation();
  scheduleTaskMediaMaintenance();
});





