// OpenTelemetry SDK — must be imported first to instrument http, express, pg, etc.
// Skipped in test to avoid gRPC connection attempts to a non-existent collector.
if (process.env.NODE_ENV !== 'test') {
  await import('./src/tracing.js');
}

import express from 'express';
import redisClient from './redis-client.js';
import * as appMetrics from './src/metrics.js';
import cors from 'cors';
import sqlite3 from 'sqlite3';
import { promisify } from 'util';
import path from 'path';
import { fileURLToPath } from 'url';
import session from 'express-session';
import connectSqlite3 from 'connect-sqlite3';
import connectPg from 'connect-pg-simple';
import pg from 'pg';
import bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit';
import webpush from 'web-push';
import pino from 'pino';
import pinoHttp from 'pino-http';
import multer from 'multer';
import { searchMedicines, getMedicineDetails, searchOTC } from './cima-service.js';
import { searchBabyProducts, getBabyProductDetails } from './off-service.js';
import { runFarmaciaWebhookImport, DEFAULT_FARMACIAS_WEBHOOK, importPharmaciesFromRows } from './farmacias-webhook-import.js';
import { fetchPharmaciesExternal } from '../API/index.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3001;

// Structured JSON logger. The Pino OTel instrumentation attaches trace_id /
// span_id to every log line so they can be correlated in Grafana.
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  base: { service: process.env.OTEL_SERVICE_NAME || 'farmaclic-backend' },
});

// Trust the reverse proxy in front of us (Docker/Traefik/nginx/Cloudflare) so
// req.ip and X-Forwarded-For are honored. Configurable via TRUST_PROXY:
//   - unset/"1" → trust exactly one hop (typical single reverse proxy)
//   - "loopback" → only trust 127.0.0.1
//   - "true"     → trust any proxy (only behind a trusted network)
//   - any other value is passed through verbatim (number of hops or IP list)
const trustProxyEnv = process.env.TRUST_PROXY ?? '1';
if (trustProxyEnv === 'true') app.set('trust proxy', true);
else if (trustProxyEnv === 'false') app.set('trust proxy', false);
else if (/^\d+$/.test(trustProxyEnv)) app.set('trust proxy', Number(trustProxyEnv));
else app.set('trust proxy', trustProxyEnv);

// Configure CORS with credentials
app.use(cors({
  origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
  credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(pinoHttp({ logger }));

// Explicit HTTP request metrics (count by route + status class, duration).
// Auto-instrumentation gives duration histograms but no discrete error count.
app.use((req, res, next) => {
  const start = performance.now();
  res.on('finish', () => {
    const route = req.route?.path || req.path;
    const statusClass = `${Math.floor(res.statusCode / 100)}xx`;
    appMetrics.httpRequestsTotal.add(1, { route, status_class: statusClass });
    appMetrics.httpRequestDuration.record(performance.now() - start, { route });
  });
  next();
});

const PG_URL = process.env.PG_URL;
let pgPool = null;
if (PG_URL) {
  const { Pool } = pg;
  pgPool = new Pool({ connectionString: PG_URL });
}

const sessionConfig = {
  secret: process.env.SESSION_SECRET || 'farma-clic-secret-key-change-in-production',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.COOKIE_SECURE === 'true',
    sameSite: 'lax',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
};

if (process.env.NODE_ENV !== 'test') {
  if (pgPool) {
    const PgStore = connectPg(session);
    sessionConfig.store = new PgStore({ pool: pgPool, createTableIfMissing: true });
  } else {
    const SQLiteStore = connectSqlite3(session);
    sessionConfig.store = new SQLiteStore({ db: 'sessions.sqlite', dir: __dirname });
  }
}

// Configure session
app.use(session(sessionConfig));

// Shared handler so every limiter emits a counter (route label) on rejection.
const limitHandler = (route) => (req, res, next) => {
  appMetrics.rateLimitRejectedTotal.add(1, { route });
  res.status(429).json({ error: 'Too many requests, please try again later.' });
};

const searchLimiter = rateLimit({ windowMs: 60_000, max: 30, standardHeaders: true, legacyHeaders: false, handler: limitHandler('search') });
const loginLimiter = rateLimit({ windowMs: 60_000, max: 5, standardHeaders: true, legacyHeaders: false, handler: limitHandler('login') });
const registerLimiter = rateLimit({ windowMs: 60 * 60_000, max: 10, standardHeaders: true, legacyHeaders: false, handler: limitHandler('register') });
const geocodeLimiter = rateLimit({ windowMs: 60_000, max: 10, standardHeaders: true, legacyHeaders: false, handler: limitHandler('geocode') });

const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY || '';
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY || '';
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || 'mailto:admin@example.com';
const EXPO_ACCESS_TOKEN = process.env.EXPO_ACCESS_TOKEN || '';
const PUSH_ENABLED = Boolean(VAPID_PUBLIC_KEY && VAPID_PRIVATE_KEY);
if (PUSH_ENABLED) {
  webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY);
} else if (process.env.NODE_ENV !== 'test') {
  console.warn('[push] VAPID keys not configured — web push disabled');
}

// Database setup
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, 'database.sqlite');
const db = new sqlite3.Database(dbPath);

// Custom wrapper to get lastID from db.run
function dbRun(sql, params = []) {
  return new Promise((resolve, reject) => {
    db.run(sql, params, function(err) {
      if (err) reject(err);
      else resolve({ lastID: this.lastID, changes: this.changes });
    });
  });
}

const dbAll = promisify(db.all.bind(db));
const dbGet = promisify(db.get.bind(db));

// User DB helpers — route to PG when available, fallback to SQLite for tests
function toPositional(sql) {
  let i = 0;
  return sql.replace(/\?/g, () => `$${++i}`);
}

async function userDbGet(sql, params = []) {
  const engine = pgPool ? 'pg' : 'sqlite';
  const start = performance.now();
  try {
    if (pgPool) {
      const res = await pgPool.query(toPositional(sql), params);
      return res.rows[0] ?? null;
    }
    return await dbGet(sql, params);
  } catch (err) {
    appMetrics.dbErrorsTotal.add(1, { engine });
    throw err;
  } finally {
    appMetrics.dbQueryDuration.record(performance.now() - start, { engine });
  }
}

async function userDbRun(sql, params = []) {
  const engine = pgPool ? 'pg' : 'sqlite';
  const start = performance.now();
  try {
    if (pgPool) {
      const res = await pgPool.query(toPositional(sql), params);
      return { lastID: res.rows[0]?.id, changes: res.rowCount };
    }
    return await dbRun(sql.replace(/\s+RETURNING\s+\w+\s*$/i, ''), params);
  } catch (err) {
    appMetrics.dbErrorsTotal.add(1, { engine });
    throw err;
  } finally {
    appMetrics.dbQueryDuration.record(performance.now() - start, { engine });
  }
}

async function userDbAll(sql, params = []) {
  const engine = pgPool ? 'pg' : 'sqlite';
  const start = performance.now();
  try {
    if (pgPool) {
      const res = await pgPool.query(toPositional(sql), params);
      return res.rows;
    }
    return await dbAll(sql, params);
  } catch (err) {
    appMetrics.dbErrorsTotal.add(1, { engine });
    throw err;
  } finally {
    appMetrics.dbQueryDuration.record(performance.now() - start, { engine });
  }
}

// Accept opening_hours as either an object or a JSON string from the client
// and return a TEXT value (or null) safe to persist.
function serializeOpeningHours(value) {
  if (value == null || value === '') return null;
  if (typeof value === 'string') {
    try {
      JSON.parse(value);
      return value;
    } catch {
      return null;
    }
  }
  if (typeof value === 'object') {
    try {
      return JSON.stringify(value);
    } catch {
      return null;
    }
  }
  return null;
}

// Initialize database tables
async function initDatabase() {
  try {
    // Create pharmacies table — PG when available, SQLite fallback
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS pharmacies (
          id SERIAL PRIMARY KEY,
          name TEXT NOT NULL,
          address TEXT NOT NULL,
          phone TEXT,
          latitude DOUBLE PRECISION,
          longitude DOUBLE PRECISION,
          opening_hours TEXT
        )
      `);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS pharmacies (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          address TEXT NOT NULL,
          phone TEXT,
          latitude REAL,
          longitude REAL,
          opening_hours TEXT
        )
      `);
      // Add opening_hours to pre-existing pharmacies tables (no-op if already present)
      try {
        await dbRun(`ALTER TABLE pharmacies ADD COLUMN opening_hours TEXT`);
      } catch (e) {
        if (!/duplicate column/i.test(e.message)) throw e;
      }
    }

    // Create junction table for pharmacy-medicine relationships
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS pharmacy_medicines (
          id SERIAL PRIMARY KEY,
          pharmacy_id INTEGER NOT NULL REFERENCES pharmacies(id),
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          price REAL,
          stock INTEGER DEFAULT 0,
          UNIQUE(pharmacy_id, medicine_nregistro)
        )
      `);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS pharmacy_medicines (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          pharmacy_id INTEGER NOT NULL,
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          price REAL,
          stock INTEGER DEFAULT 0,
          FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
          UNIQUE(pharmacy_id, medicine_nregistro)
        )
      `);
      await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_medicine ON pharmacy_medicines(medicine_nregistro)`);
    }

    // Create junction table for pharmacy-product relationships (CIMA / Open Food Facts)
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS pharmacy_products (
          id SERIAL PRIMARY KEY,
          pharmacy_id INTEGER NOT NULL REFERENCES pharmacies(id),
          product_source TEXT NOT NULL,
          product_off_id TEXT NOT NULL,
          product_name TEXT,
          price REAL,
          stock INTEGER DEFAULT 0,
          UNIQUE(pharmacy_id, product_source, product_off_id)
        )
      `);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_pharmacy_product ON pharmacy_products(product_source, product_off_id)`);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS pharmacy_products (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          pharmacy_id INTEGER NOT NULL,
          product_source TEXT NOT NULL,
          product_off_id TEXT NOT NULL,
          product_name TEXT,
          price REAL,
          stock INTEGER DEFAULT 0,
          FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
          UNIQUE(pharmacy_id, product_source, product_off_id)
        )
      `);
      await dbRun(`CREATE INDEX IF NOT EXISTS idx_pharmacy_product ON pharmacy_products(product_source, product_off_id)`);
    }

    // Create users table — PG when available, SQLite fallback
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS users (
          id SERIAL PRIMARY KEY,
          username TEXT UNIQUE NOT NULL,
          password_hash TEXT NOT NULL,
          is_admin INTEGER NOT NULL DEFAULT 0,
          address TEXT,
          latitude DOUBLE PRECISION,
          longitude DOUBLE PRECISION,
          created_at TIMESTAMPTZ DEFAULT NOW()
        )
      `);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          username TEXT UNIQUE NOT NULL,
          password_hash TEXT NOT NULL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
      `);
    }

    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS user_alerts (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id),
          type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')),
          medicine_nregistro TEXT,
          title TEXT,
          detail TEXT,
          schedule TEXT,
          created_at TIMESTAMPTZ DEFAULT NOW(),
          updated_at TIMESTAMPTZ DEFAULT NOW()
        )
      `);
    }

    // Push subscription tables — PG when available, SQLite fallback
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS push_subscriptions (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          endpoint TEXT NOT NULL,
          p256dh TEXT NOT NULL,
          auth TEXT NOT NULL,
          created_at TIMESTAMPTZ DEFAULT NOW(),
          UNIQUE(user_id, medicine_nregistro, endpoint)
        )
      `);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_push_subscriptions_med ON push_subscriptions(medicine_nregistro)`);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user ON push_subscriptions(user_id)`);

      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS push_subscriptions_pharmacy (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          pharmacy_id INTEGER NOT NULL,
          endpoint TEXT NOT NULL,
          p256dh TEXT NOT NULL,
          auth TEXT NOT NULL,
          created_at TIMESTAMPTZ DEFAULT NOW(),
          UNIQUE(user_id, medicine_nregistro, pharmacy_id, endpoint)
        )
      `);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm ON push_subscriptions_pharmacy(medicine_nregistro, pharmacy_id)`);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm_user ON push_subscriptions_pharmacy(user_id)`);

      // Expo Push tokens (mobile)
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS expo_push_tokens (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
          expo_token TEXT NOT NULL,
          created_at TIMESTAMPTZ DEFAULT NOW(),
          UNIQUE(user_id, expo_token)
        )
      `);
      await pgPool.query(`CREATE INDEX IF NOT EXISTS idx_expo_push_tokens_user ON expo_push_tokens(user_id)`);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS push_subscriptions (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          endpoint TEXT NOT NULL,
          p256dh TEXT NOT NULL,
          auth TEXT NOT NULL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          UNIQUE(medicine_nregistro, endpoint)
        )
      `);
      await dbRun(`CREATE INDEX IF NOT EXISTS idx_push_subscriptions_med ON push_subscriptions(medicine_nregistro)`);

      await dbRun(`
        CREATE TABLE IF NOT EXISTS push_subscriptions_pharmacy (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          medicine_nregistro TEXT NOT NULL,
          medicine_name TEXT,
          pharmacy_id INTEGER NOT NULL,
          endpoint TEXT NOT NULL,
          p256dh TEXT NOT NULL,
          auth TEXT NOT NULL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          UNIQUE(medicine_nregistro, pharmacy_id, endpoint)
        )
      `);
      await dbRun(`CREATE INDEX IF NOT EXISTS idx_push_subs_pharm ON push_subscriptions_pharmacy(medicine_nregistro, pharmacy_id)`);

      // Expo Push tokens (mobile)
      await dbRun(`
        CREATE TABLE IF NOT EXISTS expo_push_tokens (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          expo_token TEXT NOT NULL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          UNIQUE(user_id, expo_token)
        )
      `);
      await dbRun(`CREATE INDEX IF NOT EXISTS idx_expo_push_tokens_user ON expo_push_tokens(user_id)`);
    }

if (!pgPool) {
      // SQLite-only migrations for existing deployments
      try { await dbRun('ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN address TEXT'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN latitude REAL'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN longitude REAL'); } catch {}
    }

    // Add new fields for profile redesign
    if (pgPool) {
      await pgPool.query(`
        ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name TEXT;
        ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name TEXT;
        ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
        ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT;
        ALTER TABLE users ADD COLUMN IF NOT EXISTS city TEXT;
      `);
    } else {
      try { await dbRun('ALTER TABLE users ADD COLUMN first_name TEXT'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN last_name TEXT'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN avatar_url TEXT'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN email TEXT'); } catch {}
      try { await dbRun('ALTER TABLE users ADD COLUMN city TEXT'); } catch {}
    }

// Add user_id to push tables (SQLite only — PG tables already have it)
    if (!pgPool) {
      try { await dbRun('ALTER TABLE push_subscriptions ADD COLUMN user_id INTEGER'); } catch {}
      try { await dbRun('ALTER TABLE push_subscriptions_pharmacy ADD COLUMN user_id INTEGER'); } catch {}
    }

    // Create alerts table for persistent Avisos data
    try {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS user_alerts (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          type TEXT NOT NULL CHECK (type IN ('reminder', 'availability', 'schedule')),
          medicine_nregistro TEXT,
          title TEXT,
          detail TEXT,
          schedule TEXT,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          FOREIGN KEY (user_id) REFERENCES users(id)
        )
      `);
    } catch (e) {
      if (!/table already exists/i.test(e.message)) throw e;
    }

    // Search history for "find pharmacies near me"
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS search_history (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id),
          address TEXT NOT NULL,
          latitude DOUBLE PRECISION,
          longitude DOUBLE PRECISION,
          created_at TIMESTAMPTZ DEFAULT NOW()
        )
      `);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS search_history (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          address TEXT NOT NULL,
          latitude REAL,
          longitude REAL,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          FOREIGN KEY (user_id) REFERENCES users(id)
        )
      `);
    }

    // Recent searches table
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS recent_searches (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id),
          medicine_id TEXT NOT NULL,
          name TEXT NOT NULL,
          active_ingredient TEXT,
          dosage TEXT,
          form TEXT,
          created_at TIMESTAMPTZ DEFAULT NOW()
        )
      `);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS recent_searches (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          medicine_id TEXT NOT NULL,
          name TEXT NOT NULL,
          active_ingredient TEXT,
          dosage TEXT,
          form TEXT,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          FOREIGN KEY (user_id) REFERENCES users(id)
        )
      `);
    }

    // User addresses table
    if (pgPool) {
      await pgPool.query(`
        CREATE TABLE IF NOT EXISTS user_addresses (
          id SERIAL PRIMARY KEY,
          user_id INTEGER NOT NULL REFERENCES users(id),
          address TEXT NOT NULL,
          latitude DOUBLE PRECISION,
          longitude DOUBLE PRECISION,
          label TEXT DEFAULT '',
          is_default INTEGER NOT NULL DEFAULT 0,
          created_at TIMESTAMPTZ DEFAULT NOW()
        )
      `);
    } else {
      await dbRun(`
        CREATE TABLE IF NOT EXISTS user_addresses (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id INTEGER NOT NULL,
          address TEXT NOT NULL,
          latitude REAL,
          longitude REAL,
          label TEXT DEFAULT '',
          is_default INTEGER NOT NULL DEFAULT 0,
          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
          FOREIGN KEY (user_id) REFERENCES users(id)
        )
      `);
    }
  } catch (err) {
    console.error('initDatabase failed:', err);
    throw err;
  }
}

// API Routes

// Search medicines using CIMA API with Redis cache
app.get('/api/medicines/search', searchLimiter, async (req, res) => {
  try {
    const query = req.query.q || '';
    
    if (!query.trim()) {
      return res.json([]);
    }

    // Usar el servicio de CIMA con caché de Redis
    const medicines = await searchMedicines(query);

    res.json(medicines);
  } catch (error) {
    console.error('Error searching medicines:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get pharmacies that sell a specific medicine (usando nregistro de CIMA)
app.get('/api/medicines/:medicineId/pharmacies', async (req, res) => {
  try {
    const nregistro = req.params.medicineId; // Ahora es el nregistro de CIMA

    // First try to get pharmacies with specific medicine data
    let pharmacies = await userDbAll(`
      SELECT
        p.id,
        p.name,
        p.address,
        p.phone,
        p.latitude,
        p.longitude,
        p.opening_hours,
        pm.price,
        pm.stock
      FROM pharmacies p
      INNER JOIN pharmacy_medicines pm ON p.id = pm.pharmacy_id
      WHERE pm.medicine_nregistro = ?
      ORDER BY p.name
    `, [nregistro]);

    // If no specific links exist, return all pharmacies as potential sellers
    if (pharmacies.length === 0) {
      pharmacies = await userDbAll(`
        SELECT
          p.id,
          p.name,
          p.address,
          p.phone,
        p.latitude,
        p.longitude,
        p.opening_hours,
        NULL as price,
        0 as stock
      FROM pharmacies p
      ORDER BY p.name
      `);
    }

    res.json(pharmacies);
  } catch (error) {
    console.error('Error fetching pharmacies:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get medicine details from CIMA API (usando nregistro)
app.get('/api/medicines/:medicineId', async (req, res) => {
  try {
    const nregistro = req.params.medicineId; // Ahora es el nregistro de CIMA

    const medicine = await getMedicineDetails(nregistro);

    if (!medicine) {
      return res.status(404).json({ error: 'Medicine not found' });
    }

    res.json(medicine);
  } catch (error) {
    console.error('Error fetching medicine:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== UNIFIED PRODUCT SEARCH ==========

app.get('/api/products/search', searchLimiter, async (req, res) => {
  try {
    const { q } = req.query;
    if (!q || q.trim().length < 2) {
      return res.json({ results: [], total: 0 });
    }
    const searchTerm = q.trim();
    const [cimaResults, offResults] = await Promise.allSettled([
      searchOTC(searchTerm),
      searchBabyProducts(searchTerm)
    ]);
    const cimaProducts = cimaResults.status === 'fulfilled' ? cimaResults.value : [];
    const offProducts = offResults.status === 'fulfilled' ? offResults.value : [];
    const allProducts = [...cimaProducts, ...offProducts]
      .sort((a, b) => a.name.localeCompare(b.name));
    res.json({
      results: allProducts,
      total: allProducts.length,
      sources: { cima: cimaProducts.length, openfoodfacts: offProducts.length }
    });
  } catch (err) {
    console.error('[Products] Search error:', err);
    res.status(500).json({ error: 'Error searching products' });
  }
});

app.get('/api/products/:source/:id', async (req, res) => {
  try {
    const { source, id } = req.params;
    if (source === 'cima') {
      const product = await getMedicineDetails(id);
      if (!product) return res.status(404).json({ error: 'Product not found' });
      return res.json({ ...product, source: 'cima', category: 'otc' });
    }
    if (source === 'openfoodfacts') {
      const product = await getBabyProductDetails(id);
      if (!product) return res.status(404).json({ error: 'Product not found' });
      return res.json(product);
    }
    res.status(400).json({ error: 'Invalid source' });
  } catch (err) {
    console.error('[Products] Detail error:', err);
    res.status(500).json({ error: 'Error fetching product details' });
  }
});

// Get pharmacies that sell a specific product (by source and product ID)
app.get('/api/products/:source/:productId/pharmacies', async (req, res) => {
  try {
    const { source, productId } = req.params;

    if (source !== 'cima' && source !== 'openfoodfacts') {
      return res.status(400).json({ error: 'Invalid source' });
    }

    let pharmacies = await userDbAll(`
      SELECT
        p.id,
        p.name,
        p.address,
        p.phone,
        p.latitude,
        p.longitude,
        p.opening_hours,
        pp.price,
        pp.stock
      FROM pharmacies p
      INNER JOIN pharmacy_products pp ON p.id = pp.pharmacy_id
      WHERE pp.product_source = ? AND pp.product_off_id = ?
      ORDER BY p.name
    `, [source, productId]);

    if (pharmacies.length === 0) {
      pharmacies = await userDbAll(`
        SELECT
          p.id,
          p.name,
          p.address,
          p.phone,
          p.latitude,
          p.longitude,
          p.opening_hours,
          NULL as price,
          0 as stock
        FROM pharmacies p
        ORDER BY p.name
      `);
    }

    res.json(pharmacies);
  } catch (error) {
    console.error('Error fetching pharmacies for product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== AUTHENTICATION MIDDLEWARE ==========

// Middleware to check if user is authenticated
const requireAuth = (req, res, next) => {
  if (req.session && req.session.userId) {
    return next();
  }
  return res.status(401).json({ error: 'Authentication required' });
};

// Middleware to check if user is an admin
const requireAdmin = (req, res, next) => {
  if (req.session && req.session.userId && req.session.isAdmin) {
    return next();
  }
  if (req.session && req.session.userId) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  return res.status(401).json({ error: 'Authentication required' });
};

// ========== RECENT SEARCHES (database-based) ==========

const MAX_RECENT = 5;

// Get recent searches for the logged-in user
app.get('/api/search/recent', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const rows = await userDbAll(
      'SELECT id, medicine_id, name, active_ingredient, dosage, form, created_at FROM recent_searches WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
      [userId, MAX_RECENT]
    );
    res.json(rows.map(r => ({
      id: r.medicine_id,
      name: r.name,
      active_ingredient: r.active_ingredient,
      dosage: r.dosage,
      form: r.form,
      timestamp: r.created_at,
    })));
  } catch (error) {
    console.error('Error fetching recent searches:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Save a medicine to recent searches
app.post('/api/search/recent', requireAuth, async (req, res) => {
  try {
    const { medicine } = req.body;
    if (!medicine || !medicine.id) {
      return res.status(400).json({ error: 'Medicine data required' });
    }

    const userId = req.session.userId;

    // Remove duplicate if exists (by medicine_id)
    await userDbRun(
      'DELETE FROM recent_searches WHERE user_id = ? AND medicine_id = ?',
      [userId, medicine.id]
    );

    // Insert new entry
    await userDbRun(
      'INSERT INTO recent_searches (user_id, medicine_id, name, active_ingredient, dosage, form) VALUES (?, ?, ?, ?, ?, ?)',
      [userId, medicine.id, medicine.name, medicine.active_ingredient || null, medicine.dosage || null, medicine.form || null]
    );

    // Trim to MAX_RECENT — delete oldest beyond limit
    await userDbRun(
      `DELETE FROM recent_searches WHERE user_id = ? AND id NOT IN (
        SELECT id FROM recent_searches WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
      )`,
      [userId, userId, MAX_RECENT]
    );

    res.json({ ok: true });
  } catch (error) {
    console.error('Error saving recent search:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get all pharmacies (for admin/debugging)
app.get('/api/pharmacies', async (req, res) => {
  try {
    const pharmacies = await userDbAll(`
      SELECT * FROM pharmacies ORDER BY name
    `);
    res.json(pharmacies);
  } catch (error) {
    console.error('Error fetching pharmacies:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// TSI Card scan: return mock prescriptions for a given CIP
// In production this would integrate with a real health data source
app.get('/api/tsi/:cip/prescriptions', async (req, res) => {
  try {
    const cip = req.params.cip || '';

    // Deterministic prescription set based on CIP prefix — simulates per-patient data
    const cipUpper = cip.toUpperCase();
    let medicineNames;
    if (cipUpper.startsWith('ALPE')) {
      medicineNames = ['Paracetamol', 'Omeprazol', 'Ibuprofeno'];
    } else if (cipUpper.startsWith('MADS')) {
      medicineNames = ['Amoxicilina', 'Loratadina', 'Ibuprofeno'];
    } else if (cipUpper.startsWith('VALE')) {
      medicineNames = ['Atorvastatina', 'Metformina', 'Losartán'];
    } else {
      medicineNames = ['Paracetamol', 'Ibuprofeno'];
    }

    // Build a static prescription list (CIMA API mock)
    const prescriptions = medicineNames.map((name, idx) => {
      const dosages = ['500mg', '20mg', '600mg', '500mg', '850mg'];
      const forms   = ['Comprimidos', 'Cápsulas', 'Comprimidos recubiertos', 'Cápsulas', 'Comprimidos'];
      const actives = {
        'Paracetamol':   'Paracetamol',
        'Omeprazol':     'Omeprazol',
        'Ibuprofeno':    'Ibuprofeno',
        'Amoxicilina':   'Amoxicilina',
        'Loratadina':    'Loratadina',
        'Atorvastatina': 'Atorvastatina',
        'Metformina':    'Metformina',
        'Losartán':      'Losartán potásico',
      };
      return {
        id:               `RX_${cip}_${idx}`,
        name:             name,
        active_ingredient: actives[name] || name,
        dosage:           dosages[idx % dosages.length],
        form:             forms[idx % forms.length],
      };
    });

    res.json(prescriptions);
  } catch (error) {
    console.error('Error fetching TSI prescriptions:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== TSI OCR: extract CIP from uploaded photo ==========

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB max
  fileFilter: (_req, file, cb) => {
    if (file.mimetype.startsWith('image/')) cb(null, true);
    else cb(new Error('Only image files are accepted'));
  },
});

// Extract CIP from OCR text — looks for a long alphanumeric token (16 chars typical for CIP)
function extractCip(text) {
  const cleaned = text.replace(/\s+/g, '');

  // Common Spanish text on TSI cards that should NOT be treated as CIP
  const CARD_TEXT_PATTERNS = [
    /TARJETASANITARIA/i,
    /TARJETASANIT/i,
    /TARJETASEGURIDAD/i,
    /SISTEMASALUD/i,
    /SERVICIOANDALUZ/i,
    /SALUD/i,
    /NUMERO/i,
    /NUM/i,
    /CIP/i,
    /CODIGO/i,
    /IDENTIFICACION/i,
    /PERSONAL/i,
    /TITULAR/i,
    /FECHA/i,
    /NACIMIENTO/i,
    /CADUCIDAD/i,
    /VALIDA/i,
    /ESPANA/i,
    /ESPAÑA/i,
  ];

  // Check if OCR result is just common card text
  const isCardText = (candidate) => {
    const upper = candidate.toUpperCase();
    return CARD_TEXT_PATTERNS.some(pattern => pattern.test(upper));
  };

  // TSI barcodes are alphanumeric like "803401INURO0890417003211T11"
  // Prioritize full alphanumeric matches (8-30 chars) before pure numeric
  const exact = cleaned.match(/[A-Z0-9]{16}/i);
  if (exact) {
    const candidate = exact[0].toUpperCase();
    if (!isCardText(candidate)) {
      return candidate;
    }
  }

  const looseMatches = cleaned.match(/[A-Z0-9]{8,30}/gi) || [];
  for (const match of looseMatches) {
    const candidate = match.toUpperCase();
    if (!isCardText(candidate)) {
      return candidate;
    }
  }

  return null;
}

app.post('/api/tsi/ocr', upload.single('photo'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No image file provided' });
    }

    console.log('[OCR] Processing image, size:', req.file.size, 'bytes');

    const { createWorker } = await import('tesseract.js');
    const worker = await createWorker('spa+eng');
    const { data } = await worker.recognize(req.file.buffer);
    await worker.terminate();

    console.log('[OCR] Raw text:', data.text);
    console.log('[OCR] Words:', data.words?.length || 0, 'words detected');
    if (data.words && data.words.length > 0) {
      console.log('[OCR] First 10 words:', data.words.slice(0, 10).map(w => `${w.text}(${w.confidence?.toFixed(2)})`).join(', '));
    }
    const cip = extractCip(data.text);
    console.log('[OCR] Extracted CIP:', cip);

    if (!cip) {
      return res.status(422).json({
        error: 'No se pudo detectar un código CIP en la imagen',
        ocrText: data.text.trim().slice(0, 500),
      });
    }

    res.json({ cip });
  } catch (error) {
    console.error('TSI OCR error:', error);
    res.status(500).json({ error: 'Error procesando la imagen' });
  }
});

// ========== AUTHENTICATION ROUTES ==========

// Login endpoint
app.post('/api/auth/login', loginLimiter, async (req, res) => {
  try {
    const { username, password } = req.body;

    if (!username || !password) {
      return res.status(400).json({ error: 'Username and password are required' });
    }

    // Find user by username
    const user = await userDbGet(
      'SELECT * FROM users WHERE username = ?',
      [username.trim()]
    );

    if (!user) {
      appMetrics.loginFailureTotal.add(1);
      return res.status(401).json({ error: 'Invalid username or password' });
    }

    // Verify password
    const isValidPassword = await bcrypt.compare(password, user.password_hash);

    if (!isValidPassword) {
      appMetrics.loginFailureTotal.add(1);
      return res.status(401).json({ error: 'Invalid username or password' });
    }

    // Create session
    req.session.userId = user.id;
    req.session.username = user.username;
    req.session.isAdmin = Boolean(user.is_admin);

    appMetrics.loginSuccessTotal.add(1);
    res.json({
      message: 'Login successful',
      user: {
        id: user.id,
        username: user.username,
        first_name: user.first_name || null,
        last_name: user.last_name || null,
        avatar_url: user.avatar_url || null,
        is_admin: Boolean(user.is_admin),
        address: user.address || null,
        latitude: user.latitude,
        longitude: user.longitude,
      }
    });
  } catch (error) {
    console.error('Error during login:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Register a new user (non-admin) and start a session
app.post('/api/auth/register', registerLimiter, async (req, res) => {
  try {
    const { username, password } = req.body || {};
    const u = String(username || '').trim();
    const p = String(password || '');

    if (!/^[A-Za-z0-9_]{3,32}$/.test(u)) {
      return res.status(400).json({ error: 'Username must be 3–32 characters: letters, digits, or underscore' });
    }
    if (p.length < 8) {
      return res.status(400).json({ error: 'Password must be at least 8 characters' });
    }

    const existing = await userDbGet('SELECT id FROM users WHERE username = ?', [u]);
    if (existing) {
      return res.status(409).json({ error: 'Username already taken' });
    }

    const passwordHash = await bcrypt.hash(p, 10);
    const result = await userDbRun(
      'INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 0) RETURNING id',
      [u, passwordHash]
    );

    req.session.userId = result.lastID;
    req.session.username = u;
    req.session.isAdmin = false;

    res.status(201).json({
      message: 'Registered',
      user: {
        id: result.lastID,
        username: u,
        first_name: null,
        last_name: null,
        avatar_url: null,
        is_admin: false,
        address: null,
        latitude: null,
        longitude: null,
      }
    });
  } catch (error) {
    console.error('Error during registration:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Logout endpoint
app.post('/api/auth/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      console.error('Error destroying session:', err);
      return res.status(500).json({ error: 'Error logging out' });
    }
    res.json({ message: 'Logout successful' });
  });
});

// Check authentication status — reads fresh user record so profile fields stay current
app.get('/api/auth/check', async (req, res) => {
  try {
    if (req.session && req.session.userId) {
      const user = await userDbGet(
        'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?',
        [req.session.userId]
      );
      if (!user) {
        req.session.destroy(() => {});
        return res.json({ authenticated: false });
      }
      return res.json({
        authenticated: true,
        user: {
          id: user.id,
          username: user.username,
          first_name: user.first_name || null,
          last_name: user.last_name || null,
          avatar_url: user.avatar_url || null,
          email: user.email || null,
          city: user.city || null,
          is_admin: Boolean(user.is_admin),
          address: user.address || null,
          latitude: user.latitude,
          longitude: user.longitude,
        }
      });
    }
    res.json({ authenticated: false });
  } catch (error) {
    console.error('Error checking auth:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Current user's profile
app.get('/api/users/me', requireAuth, async (req, res) => {
  try {
    const user = await userDbGet(
      'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?',
      [req.session.userId]
    );
    if (!user) return res.status(404).json({ error: 'Not found' });
    res.json({
      id: user.id,
      username: user.username,
      first_name: user.first_name || null,
      last_name: user.last_name || null,
      avatar_url: user.avatar_url || null,
      email: user.email || null,
      city: user.city || null,
      is_admin: Boolean(user.is_admin),
      address: user.address || null,
      latitude: user.latitude,
      longitude: user.longitude,
    });
  } catch (error) {
    console.error('Error loading profile:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update the current user's address + coordinates
app.put('/api/users/me', requireAuth, async (req, res) => {
  try {
    const body = req.body || {};
    const sets = [];
    const vals = [];

    if ('first_name' in body) {
      const v = body.first_name == null ? null : String(body.first_name).trim().slice(0, 100);
      sets.push('first_name = ?');
      vals.push(v);
    }
    if ('last_name' in body) {
      const v = body.last_name == null ? null : String(body.last_name).trim().slice(0, 100);
      sets.push('last_name = ?');
      vals.push(v);
    }
    if ('avatar_url' in body) {
      const v = body.avatar_url == null ? null : String(body.avatar_url);
      sets.push('avatar_url = ?');
      vals.push(v);
    }
    if ('email' in body) {
      const v = body.email == null ? null : String(body.email).trim().slice(0, 200);
      sets.push('email = ?');
      vals.push(v);
    }
    if ('city' in body) {
      const v = body.city == null ? null : String(body.city).trim().slice(0, 200);
      sets.push('city = ?');
      vals.push(v);
    }
    if ('address' in body) {
      const v = body.address == null || String(body.address).trim() === ''
        ? null
        : String(body.address).trim().slice(0, 500);
      sets.push('address = ?');
      vals.push(v);
    }
    if ('latitude' in body) {
      if (body.latitude !== '' && body.latitude != null) {
        const lat = typeof body.latitude === 'number' ? body.latitude : parseFloat(body.latitude);
        if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
          return res.status(400).json({ error: 'Invalid latitude' });
        }
        sets.push('latitude = ?');
        vals.push(lat);
      } else {
        sets.push('latitude = ?');
        vals.push(null);
      }
    }
    if ('longitude' in body) {
      if (body.longitude !== '' && body.longitude != null) {
        const lon = typeof body.longitude === 'number' ? body.longitude : parseFloat(body.longitude);
        if (!Number.isFinite(lon) || lon < -180 || lon > 180) {
          return res.status(400).json({ error: 'Invalid longitude' });
        }
        sets.push('longitude = ?');
        vals.push(lon);
      } else {
        sets.push('longitude = ?');
        vals.push(null);
      }
    }

    if (sets.length > 0) {
      vals.push(req.session.userId);
      await userDbRun(
        'UPDATE users SET ' + sets.join(', ') + ' WHERE id = ?',
        vals
      );
    }

    const user = await userDbGet(
      'SELECT id, username, first_name, last_name, avatar_url, email, city, is_admin, address, latitude, longitude FROM users WHERE id = ?',
      [req.session.userId]
    );
    res.json({
      id: user.id,
      username: user.username,
      first_name: user.first_name || null,
      last_name: user.last_name || null,
      avatar_url: user.avatar_url || null,
      email: user.email || null,
      city: user.city || null,
      is_admin: Boolean(user.is_admin),
      address: user.address || null,
      latitude: user.latitude,
      longitude: user.longitude,
    });
  } catch (error) {
    console.error('Error updating profile:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== USER ALERTS API ==========

// Get all alerts for the current user
app.get('/api/alerts', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const rows = await userDbAll(
      'SELECT id, type, medicine_nregistro, title, detail, schedule, created_at, updated_at FROM user_alerts WHERE user_id = ? ORDER BY created_at DESC',
      [userId]
    );
    res.json(rows);
  } catch (error) {
    console.error('Error fetching alerts:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Create a new alert
app.post('/api/alerts', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const { type, medicine_nregistro, title, detail, schedule } = req.body || {};

    if (!type || !['reminder', 'availability', 'schedule'].includes(type)) {
      return res.status(400).json({ error: 'Valid type is required (reminder, availability, schedule)' });
    }

    const result = await userDbRun(
      'INSERT INTO user_alerts (user_id, type, medicine_nregistro, title, detail, schedule) VALUES (?, ?, ?, ?, ?, ?)',
      [userId, type, medicine_nregistro || null, title || null, detail || null, schedule || null]
    );

    const newAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [result.lastID]);
    res.status(201).json(newAlert);
  } catch (error) {
    console.error('Error creating alert:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update an existing alert
app.put('/api/alerts/:id', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const alertId = parseInt(req.params.id);
    const { medicine_nregistro, title, detail, schedule } = req.body || {};

    const result = await userDbRun(
      'UPDATE user_alerts SET medicine_nregistro = ?, title = ?, detail = ?, schedule = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
      [medicine_nregistro || null, title || null, detail || null, schedule || null, alertId, userId]
    );

    if (result.changes === 0) {
      return res.status(404).json({ error: 'Alert not found' });
    }

    const updatedAlert = await userDbGet('SELECT * FROM user_alerts WHERE id = ?', [alertId]);
    res.json(updatedAlert);
  } catch (error) {
    console.error('Error updating alert:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete an alert
app.delete('/api/alerts/:id', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const alertId = parseInt(req.params.id);

    const result = await userDbRun(
      'DELETE FROM user_alerts WHERE id = ? AND user_id = ?',
      [alertId, userId]
    );

    if (result.changes === 0) {
      return res.status(404).json({ error: 'Alert not found' });
    }

    res.status(204).end();
  } catch (error) {
    console.error('Error deleting alert:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== SEARCH HISTORY API ==========

// Get search history for current user
app.get('/api/search-history', requireAuth, async (req, res) => {
  try {
    const rows = await userDbAll(
      'SELECT id, address, latitude, longitude, created_at FROM search_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
      [req.session.userId]
    );
    res.json(rows);
  } catch (error) {
    console.error('Error fetching search history:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Add new search to history
app.post('/api/search-history', requireAuth, async (req, res) => {
  try {
    const { address, latitude, longitude } = req.body || {};
    if (!address) {
      return res.status(400).json({ error: 'Address is required' });
    }

    const result = await userDbRun(
      'INSERT INTO search_history (user_id, address, latitude, longitude) VALUES (?, ?, ?, ?)',
      [req.session.userId, address, latitude || null, longitude || null]
    );

    res.json({ id: result.lastID, address, latitude, longitude });
  } catch (error) {
    console.error('Error adding search history:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete search history item
app.delete('/api/search-history/:id', requireAuth, async (req, res) => {
  try {
    await userDbRun(
      'DELETE FROM search_history WHERE id = ? AND user_id = ?',
      [req.params.id, req.session.userId]
    );
    res.json({ ok: true });
  } catch (error) {
    console.error('Error deleting search history:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== USER ADDRESSES API ==========

// List all addresses for the current user
app.get('/api/addresses', requireAuth, async (req, res) => {
  try {
    const rows = await userDbAll(
      'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, created_at ASC',
      [req.session.userId]
    );
    res.json(rows);
  } catch (error) {
    console.error('Error fetching addresses:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Add a new address
app.post('/api/addresses', requireAuth, async (req, res) => {
  try {
    const { address, latitude, longitude, label, is_default } = req.body || {};
    if (!address) {
      return res.status(400).json({ error: 'Address is required' });
    }

    if (is_default) {
      await userDbRun(
        'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
        [req.session.userId]
      );
    }

    const result = await userDbRun(
      'INSERT INTO user_addresses (user_id, address, latitude, longitude, label, is_default) VALUES (?, ?, ?, ?, ?, ?)',
      [req.session.userId, address, latitude || null, longitude || null, label || '', is_default ? 1 : 0]
    );

    const newAddress = await userDbGet(
      'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE id = ?',
      [result.lastID]
    );
    res.status(201).json(newAddress);
  } catch (error) {
    console.error('Error adding address:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update an address
app.put('/api/addresses/:id', requireAuth, async (req, res) => {
  try {
    const addressId = parseInt(req.params.id);
    const { address, latitude, longitude, label, is_default } = req.body || {};

    if (!address) {
      return res.status(400).json({ error: 'Address is required' });
    }

    if (is_default) {
      await userDbRun(
        'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
        [req.session.userId]
      );
    }

    await userDbRun(
      'UPDATE user_addresses SET address = ?, latitude = ?, longitude = ?, label = ?, is_default = ? WHERE id = ? AND user_id = ?',
      [address, latitude || null, longitude || null, label || '', is_default ? 1 : 0, addressId, req.session.userId]
    );

    const updated = await userDbGet(
      'SELECT id, address, latitude, longitude, label, is_default, created_at FROM user_addresses WHERE id = ?',
      [addressId]
    );

    if (!updated) {
      return res.status(404).json({ error: 'Address not found' });
    }

    res.json(updated);
  } catch (error) {
    console.error('Error updating address:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Set address as default
app.put('/api/addresses/:id/default', requireAuth, async (req, res) => {
  try {
    const addressId = parseInt(req.params.id);

    const existing = await userDbGet(
      'SELECT id FROM user_addresses WHERE id = ? AND user_id = ?',
      [addressId, req.session.userId]
    );
    if (!existing) {
      return res.status(404).json({ error: 'Address not found' });
    }

    await userDbRun(
      'UPDATE user_addresses SET is_default = 0 WHERE user_id = ?',
      [req.session.userId]
    );
    await userDbRun(
      'UPDATE user_addresses SET is_default = 1 WHERE id = ?',
      [addressId]
    );

    res.json({ ok: true });
  } catch (error) {
    console.error('Error setting default address:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete an address
app.delete('/api/addresses/:id', requireAuth, async (req, res) => {
  try {
    const addressId = parseInt(req.params.id);

    const deleted = await userDbGet(
      'SELECT is_default FROM user_addresses WHERE id = ? AND user_id = ?',
      [addressId, req.session.userId]
    );

    await userDbRun(
      'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
      [addressId, req.session.userId]
    );

    // If the deleted address was default, set the oldest remaining as default
    if (deleted?.is_default) {
      const oldest = await userDbGet(
        'SELECT id FROM user_addresses WHERE user_id = ? ORDER BY created_at ASC LIMIT 1',
        [req.session.userId]
      );
      if (oldest) {
        await userDbRun('UPDATE user_addresses SET is_default = 1 WHERE id = ?', [oldest.id]);
      }
    }

    res.json({ ok: true });
  } catch (error) {
    console.error('Error deleting address:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Geocode for any authenticated user (rate-limited). Same impl as /api/admin/geocode.
app.get('/api/geocode', requireAuth, geocodeLimiter, async (req, res) => {
  const q = (req.query.q || '').trim();
  if (!q) return res.status(400).json({ error: 'Query parameter q is required' });
  try {
    const result = await nominatimSearchFirstHit(q);
    if (!result.ok) return res.status(result.status).json({ error: result.error });
    const lat = parseFloat(result.hit.lat);
    const lon = parseFloat(result.hit.lon);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
      return res.status(502).json({ error: 'Geocoder result missing coordinates' });
    }
    res.json({
      lat,
      lon,
      displayName: result.hit.display_name || q,
      matchedQuery: result.triedQuery,
    });
  } catch (err) {
    console.error('Geocode error:', err);
    res.status(500).json({ error: err.message || 'Geocode failed' });
  }
});

// ========== ADMIN API ROUTES ==========

/** Suggested search radius (m) from Nominatim bounding box [south, north, west, east] */
function radiusMetersFromBoundingBox(south, north, west, east) {
  const s = parseFloat(south);
  const n = parseFloat(north);
  const w = parseFloat(west);
  const e = parseFloat(east);
  if (![s, n, w, e].every(Number.isFinite)) return null;
  const latMid = (s + n) / 2;
  const latM = Math.abs(n - s) * 111320;
  const lonM = Math.abs(e - w) * 111320 * Math.cos((latMid * Math.PI) / 180);
  const half = (Math.max(latM, lonM) / 2) * 1.12;
  return Math.round(Math.min(Math.max(half, 1500), 50000));
}

async function nominatimSearchFirstHit(originalQuery) {
  const trimmed = originalQuery.trim();
  const variants = [
    trimmed,
    `${trimmed}, España`,
    `${trimmed}, Spain`,
    `${trimmed}, ES`,
  ];
  const seen = new Set();
  const uniqueVariants = variants.filter((v) => {
    const k = v.toLowerCase();
    if (seen.has(k)) return false;
    seen.add(k);
    return true;
  });

  const delay = (ms) => new Promise((r) => setTimeout(r, ms));

  for (let i = 0; i < uniqueVariants.length; i++) {
    const q = uniqueVariants[i];
    if (i > 0) await delay(1100);
    const params = new URLSearchParams({
      format: 'json',
      limit: '1',
      q,
      addressdetails: '0',
    });
    const url = `https://nominatim.openstreetmap.org/search?${params}`;
    const nomRes = await fetch(url, {
      headers: {
        Accept: 'application/json',
        'Accept-Language': 'es,en',
        'User-Agent':
          'FarmaClic/1.0 (pharmacy admin; geocoding; https://github.com/)',
      },
    });
    const text = await nomRes.text();
    let data;
    try {
      data = text ? JSON.parse(text) : [];
    } catch {
      return { ok: false, error: 'Geocoder returned invalid JSON', status: 502 };
    }
    if (!nomRes.ok) {
      return {
        ok: false,
        error: `Geocoder HTTP ${nomRes.status}`,
        status: 502,
      };
    }
    if (Array.isArray(data) && data.length > 0) {
      return { ok: true, hit: data[0], triedQuery: q };
    }
  }


  return {
    ok: false,
    error:
      'No place found. Try adding the region (e.g. "Rubí, Barcelona" or "Toledo, Spain").',
    status: 422,
  };
}

// Geocode city → lat, lon, radius (OpenStreetMap Nominatim; admin-only, low volume)
app.get('/api/admin/geocode', requireAdmin, async (req, res) => {
  const q = (req.query.q || '').trim();
  if (!q) {
    return res.status(400).json({ error: 'Query parameter q is required' });
  }
  try {
    const result = await nominatimSearchFirstHit(q);
    if (!result.ok) {
      return res.status(result.status).json({ error: result.error });
    }
    const hit = result.hit;
    const lat = parseFloat(hit.lat);
    const lon = parseFloat(hit.lon);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
      return res.status(502).json({ error: 'Geocoder result missing coordinates' });
    }
    let radius = 12000;
    if (hit.boundingbox && hit.boundingbox.length >= 4) {
      const r = radiusMetersFromBoundingBox(
        hit.boundingbox[0],
        hit.boundingbox[1],
        hit.boundingbox[2],
        hit.boundingbox[3]
      );
      if (r != null) radius = r;
    }
    res.json({
      lat,
      lon,
      radius,
      displayName: hit.display_name || q,
      matchedQuery: result.triedQuery,
    });
  } catch (err) {
    console.error('Geocode error:', err);
    res.status(500).json({ error: err.message || 'Geocode failed' });
  }
});

// Add a new pharmacy
app.post('/api/admin/pharmacies', requireAdmin, async (req, res) => {
  try {
    const { name, address, phone, latitude, longitude, opening_hours } = req.body;

    if (!name || !address) {
      return res.status(400).json({ error: 'Name and address are required' });
    }

    // Check for duplicate (same name and address)
    const existing = await userDbGet(
      'SELECT * FROM pharmacies WHERE name = ? AND address = ?',
      [name.trim(), address.trim()]
    );

    if (existing) {
      return res.status(400).json({ error: 'A pharmacy with this name and address already exists' });
    }

    const openingHoursValue = serializeOpeningHours(opening_hours);

    const result = await userDbRun(
      'INSERT INTO pharmacies (name, address, phone, latitude, longitude, opening_hours) VALUES (?, ?, ?, ?, ?, ?)',
      [name.trim(), address.trim(), phone ? phone.trim() : null, latitude || null, longitude || null, openingHoursValue]
    );

    if (!result || result.lastID === undefined) {
      throw new Error('Failed to get lastID from database insert');
    }

    const newPharmacy = await userDbGet(
      'SELECT * FROM pharmacies WHERE id = ?',
      [result.lastID]
    );

    if (!newPharmacy) {
      throw new Error('Failed to retrieve created pharmacy');
    }

    res.status(201).json(newPharmacy);
    appMetrics.pharmacyWriteTotal.add(1, { op: 'create' });
  } catch (error) {
    console.error('Error adding pharmacy:', error);
    if (error.message.includes('UNIQUE constraint')) {
      res.status(400).json({ error: 'A pharmacy with this information already exists' });
    } else {
      res.status(500).json({ error: error.message || 'Internal server error' });
    }
  }
});

// Update a pharmacy
app.put('/api/admin/pharmacies/:id', requireAdmin, async (req, res) => {
  try {
    const pharmacyId = parseInt(req.params.id);
    const { name, address, phone, latitude, longitude, opening_hours } = req.body;

    if (!name || !address) {
      return res.status(400).json({ error: 'Name and address are required' });
    }

    const openingHoursValue = serializeOpeningHours(opening_hours);

    await userDbRun(
      'UPDATE pharmacies SET name = ?, address = ?, phone = ?, latitude = ?, longitude = ?, opening_hours = ? WHERE id = ?',
      [name, address, phone || null, latitude || null, longitude || null, openingHoursValue, pharmacyId]
    );

    const updatedPharmacy = await userDbGet(
      'SELECT * FROM pharmacies WHERE id = ?',
      [pharmacyId]
    );

    if (!updatedPharmacy) {
      return res.status(404).json({ error: 'Pharmacy not found' });
    }

    res.json(updatedPharmacy);
    appMetrics.pharmacyWriteTotal.add(1, { op: 'update' });
  } catch (error) {
    console.error('Error updating pharmacy:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete a pharmacy
app.delete('/api/admin/pharmacies/:id', requireAdmin, async (req, res) => {
  try {
    const pharmacyId = parseInt(req.params.id);

    // Delete related pharmacy_medicines and pharmacy_products first
    await userDbRun('DELETE FROM pharmacy_medicines WHERE pharmacy_id = ?', [pharmacyId]);
    await userDbRun('DELETE FROM pharmacy_products WHERE pharmacy_id = ?', [pharmacyId]);
    await userDbRun('DELETE FROM pharmacies WHERE id = ?', [pharmacyId]);

    res.json({ message: 'Pharmacy deleted successfully' });
    appMetrics.pharmacyWriteTotal.add(1, { op: 'delete' });
  } catch (error) {
    console.error('Error deleting pharmacy:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Import pharmacies from webhook (e.g. n8n).
// Body: { "url"?: string, "lat"?, "lon"|"lng"?, "radio"? } — lat/lon/radio add ?lat=&lon=&radio= (metres)
app.post('/api/admin/pharmacies/import-webhook', requireAdmin, async (req, res) => {
  try {
    const body = req.body && typeof req.body === 'object' ? req.body : {};
    const url =
      (typeof body.url === 'string' && body.url.trim()) ||
      process.env.FARMACIAS_WEBHOOK_URL ||
      DEFAULT_FARMACIAS_WEBHOOK;
    const region = {};
    if (body.lat != null && String(body.lat).trim() !== '') region.lat = body.lat;
    const lonVal = body.lon ?? body.lng;
    if (lonVal != null && String(lonVal).trim() !== '') region.lon = lonVal;
    if (body.radio != null && String(body.radio).trim() !== '') region.radio = body.radio;
    const hasRegion =
      region.lat != null || region.lon != null || region.radio != null;
    const result = await runFarmaciaWebhookImport(
      userDbGet,
      userDbRun,
      url.trim(),
      hasRegion ? region : null
    );
    res.json(result);
  } catch (error) {
    console.error('Webhook pharmacy import:', error);
    const status = error.message?.includes('HTTP') ? 502 : 400;
    res.status(status).json({
      error: error.message || 'Webhook import failed',
      ...(error.details ? { details: error.details } : {}),
    });
  }
});

// Import from OpenStreetMap (Overpass), or a JSON open-data URL — see /API
app.post('/api/admin/pharmacies/import-external', requireAdmin, async (req, res) => {
  try {
    const body = req.body && typeof req.body === 'object' ? req.body : {};
    const source = body.source;
    if (!['osm', 'openData'].includes(source)) {
      return res
        .status(400)
        .json({ error: 'source must be "osm", or "openData"' });
    }

    const rows = await fetchPharmaciesExternal({
      source,
      lat: body.lat,
      lon: body.lon ?? body.lng,
      lng: body.lng,
      radio: body.radio,
      openDataUrl:
        typeof body.openDataUrl === 'string' ? body.openDataUrl.trim() : undefined,
    });

    if (!rows.length) {
      return res.json({
        inserted: 0,
        skipped: 0,
        invalid: 0,
        errors: [],
        totalReceived: 0,
        source,
        message: 'No pharmacies returned for this query',
      });
    }

    const stats = await importPharmaciesFromRows(userDbGet, userDbRun, rows);
    res.json({ ...stats, totalReceived: rows.length, source });
  } catch (error) {
    console.error('External pharmacy import:', error);
    res.status(400).json({ error: error.message || 'External import failed' });
  }
});

// Search medicines from CIMA API (para el admin)
app.get('/api/admin/medicines', requireAdmin, async (req, res) => {
  try {
    const query = req.query.q || '';
    
    if (!query.trim()) {
      // Si no hay query, retornar lista vacía o medicamentos populares
      return res.json([]);
    }

    // Usar el servicio de CIMA
    const medicines = await searchMedicines(query);
    res.json(medicines);
  } catch (error) {
    console.error('Error fetching medicines:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get medicines for a specific pharmacy (usando nregistro)
app.get('/api/admin/pharmacies/:pharmacyId/medicines', requireAdmin, async (req, res) => {
  try {
    const pharmacyId = parseInt(req.params.pharmacyId);

    const medicines = await userDbAll(`
      SELECT 
        pm.id,
        pm.pharmacy_id,
        pm.medicine_nregistro,
        pm.medicine_name,
        pm.price,
        pm.stock
      FROM pharmacy_medicines pm
      WHERE pm.pharmacy_id = ?
      ORDER BY pm.medicine_name
    `, [pharmacyId]);

    res.json(medicines);
  } catch (error) {
    console.error('Error fetching pharmacy medicines:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Add medicine to a pharmacy (or update if exists) usando nregistro de CIMA
app.post('/api/admin/pharmacy-medicines', requireAdmin, async (req, res) => {
  try {
    const { pharmacy_id, medicine_nregistro, medicine_name, price, stock } = req.body;

    if (!pharmacy_id || !medicine_nregistro) {
      return res.status(400).json({ error: 'Pharmacy ID and Medicine nregistro are required' });
    }

    // Check if relationship already exists
    const existing = await userDbGet(
      'SELECT * FROM pharmacy_medicines WHERE pharmacy_id = ? AND medicine_nregistro = ?',
      [pharmacy_id, medicine_nregistro]
    );

    if (existing) {
      // Update existing relationship
      await userDbRun(
        'UPDATE pharmacy_medicines SET medicine_name = ?, price = ?, stock = ? WHERE pharmacy_id = ? AND medicine_nregistro = ?',
        [medicine_name, price || null, stock || 0, pharmacy_id, medicine_nregistro]
      );
    } else {
      // Insert new relationship
      await userDbRun(
        'INSERT INTO pharmacy_medicines (pharmacy_id, medicine_nregistro, medicine_name, price, stock) VALUES (?, ?, ?, ?, ?)',
        [pharmacy_id, medicine_nregistro, medicine_name, price || null, stock || 0]
      );
    }

    const relationship = await userDbGet(
      `SELECT * FROM pharmacy_medicines
       WHERE pharmacy_id = ? AND medicine_nregistro = ?`,
      [pharmacy_id, medicine_nregistro]
    );

    if (!existing) {
      const pharmacy = await userDbGet('SELECT id, name FROM pharmacies WHERE id = ?', [pharmacy_id]);
      sendPushForMedicine({
        medicine_nregistro,
        medicine_name: medicine_name || relationship?.medicine_name || null,
        pharmacy,
      }).catch(err => console.error('[push] fanout error:', err));
    }

    res.status(201).json(relationship);
    appMetrics.pharmacyMedicineLinkTotal.add(1, { op: 'create' });
  } catch (error) {
    console.error('Error adding medicine to pharmacy:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update pharmacy-medicine relationship
app.put('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const { price, stock } = req.body;

    await userDbRun(
      'UPDATE pharmacy_medicines SET price = ?, stock = ? WHERE id = ?',
      [price || null, stock || 0, id]
    );

    const updated = await userDbGet(
      'SELECT * FROM pharmacy_medicines WHERE id = ?',
      [id]
    );

    if (!updated) {
      return res.status(404).json({ error: 'Relationship not found' });
    }

    res.json(updated);
    appMetrics.pharmacyMedicineLinkTotal.add(1, { op: 'update' });
  } catch (error) {
    console.error('Error updating pharmacy-medicine:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete pharmacy-medicine relationship
app.delete('/api/admin/pharmacy-medicines/:id', requireAdmin, async (req, res) => {
  try {
    const id = parseInt(req.params.id);
    await userDbRun('DELETE FROM pharmacy_medicines WHERE id = ?', [id]);
    res.json({ message: 'Medicine removed from pharmacy successfully' });
    appMetrics.pharmacyMedicineLinkTotal.add(1, { op: 'delete' });
  } catch (error) {
    console.error('Error deleting pharmacy-medicine:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// ========== PHARMACY-PRODUCT ADMIN ROUTES ==========

// List products for a specific pharmacy
app.get('/api/admin/pharmacies/:pharmacyId/products', requireAdmin, async (req, res) => {
  try {
    const pharmacyId = parseInt(req.params.pharmacyId);

    const products = await userDbAll(`
      SELECT
        pp.id,
        pp.pharmacy_id,
        pp.product_source,
        pp.product_off_id,
        pp.product_name,
        pp.price,
        pp.stock
      FROM pharmacy_products pp
      WHERE pp.pharmacy_id = ?
      ORDER BY pp.product_name
    `, [pharmacyId]);

    res.json(products);
  } catch (error) {
    console.error('Error fetching pharmacy products:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Add/upsert a product-pharmacy link
app.post('/api/admin/pharmacy-products', requireAdmin, async (req, res) => {
  try {
    const { pharmacy_id, product_source, product_off_id, product_name, price, stock } = req.body;

    if (!pharmacy_id || !product_source || !product_off_id) {
      return res.status(400).json({ error: 'pharmacy_id, product_source, and product_off_id are required' });
    }

    if (product_source !== 'cima' && product_source !== 'openfoodfacts') {
      return res.status(400).json({ error: 'product_source must be "cima" or "openfoodfacts"' });
    }

    const existing = await userDbGet(
      'SELECT * FROM pharmacy_products WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
      [pharmacy_id, product_source, product_off_id]
    );

    if (existing) {
      await userDbRun(
        'UPDATE pharmacy_products SET product_name = ?, price = ?, stock = ? WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
        [product_name || null, price || null, stock || 0, pharmacy_id, product_source, product_off_id]
      );
    } else {
      await userDbRun(
        'INSERT INTO pharmacy_products (pharmacy_id, product_source, product_off_id, product_name, price, stock) VALUES (?, ?, ?, ?, ?, ?)',
        [pharmacy_id, product_source, product_off_id, product_name || null, price || null, stock || 0]
      );
    }

    const relationship = await userDbGet(
      'SELECT * FROM pharmacy_products WHERE pharmacy_id = ? AND product_source = ? AND product_off_id = ?',
      [pharmacy_id, product_source, product_off_id]
    );

    res.status(201).json(relationship);
  } catch (error) {
    console.error('Error adding pharmacy product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Update price/stock for a pharmacy-product link
app.put('/api/admin/pharmacy-products/:id', requireAdmin, async (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const { price, stock } = req.body;

    await userDbRun(
      'UPDATE pharmacy_products SET price = ?, stock = ? WHERE id = ?',
      [price || null, stock || 0, id]
    );

    const updated = await userDbGet(
      'SELECT * FROM pharmacy_products WHERE id = ?',
      [id]
    );

    if (!updated) {
      return res.status(404).json({ error: 'Relationship not found' });
    }

    res.json(updated);
  } catch (error) {
    console.error('Error updating pharmacy product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete a pharmacy-product link
app.delete('/api/admin/pharmacy-products/:id', requireAdmin, async (req, res) => {
  try {
    const id = parseInt(req.params.id);
    await userDbRun('DELETE FROM pharmacy_products WHERE id = ?', [id]);
    res.json({ message: 'Product removed from pharmacy successfully' });
  } catch (error) {
    console.error('Error deleting pharmacy product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Push notifications

app.get('/api/notifications/vapid-public-key', (req, res) => {
  if (!PUSH_ENABLED) {
    return res.status(503).json({ error: 'Push notifications are not configured on this server' });
  }
  res.json({ publicKey: VAPID_PUBLIC_KEY });
});

app.post('/api/notifications/subscribe', requireAuth, async (req, res) => {
  try {
    if (!PUSH_ENABLED) {
      return res.status(503).json({ error: 'Push notifications are not configured on this server' });
    }
    const { medicine_nregistro, medicine_name, subscription, pharmacy_id } = req.body || {};
    const endpoint = subscription?.endpoint;
    const p256dh = subscription?.keys?.p256dh;
    const auth = subscription?.keys?.auth;
    const userId = req.session.userId;

    if (!medicine_nregistro || !endpoint || !p256dh || !auth) {
      return res.status(400).json({ error: 'medicine_nregistro and a full PushSubscription are required' });
    }

    if (pharmacy_id) {
      const existing = await userDbGet(
        'SELECT id FROM push_subscriptions_pharmacy WHERE user_id = ? AND medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
        [userId, medicine_nregistro, pharmacy_id, endpoint]
      );
      if (existing) {
        await userDbRun(
          'UPDATE push_subscriptions_pharmacy SET medicine_name = ?, p256dh = ?, auth = ? WHERE id = ? AND user_id = ?',
          [medicine_name || null, p256dh, auth, existing.id, userId]
        );
      } else {
        await userDbRun(
          'INSERT INTO push_subscriptions_pharmacy (user_id, medicine_nregistro, medicine_name, pharmacy_id, endpoint, p256dh, auth) VALUES (?, ?, ?, ?, ?, ?, ?)',
          [userId, medicine_nregistro, medicine_name || null, pharmacy_id, endpoint, p256dh, auth]
        );
      }
    } else {
      const existing = await userDbGet(
        'SELECT id FROM push_subscriptions WHERE user_id = ? AND medicine_nregistro = ? AND endpoint = ?',
        [userId, medicine_nregistro, endpoint]
      );
      if (existing) {
        await userDbRun(
          'UPDATE push_subscriptions SET medicine_name = ?, p256dh = ?, auth = ? WHERE id = ? AND user_id = ?',
          [medicine_name || null, p256dh, auth, existing.id, userId]
        );
      } else {
        await userDbRun(
          'INSERT INTO push_subscriptions (user_id, medicine_nregistro, medicine_name, endpoint, p256dh, auth) VALUES (?, ?, ?, ?, ?, ?)',
          [userId, medicine_nregistro, medicine_name || null, endpoint, p256dh, auth]
        );
      }
    }

    res.status(201).json({ ok: true });
  } catch (error) {
    console.error('Error subscribing to push:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.delete('/api/notifications/unsubscribe', requireAuth, async (req, res) => {
  try {
    const { medicine_nregistro, endpoint, pharmacy_id } = req.body || {};
    const userId = req.session.userId;
    if (!endpoint) {
      return res.status(400).json({ error: 'endpoint is required' });
    }
    if (pharmacy_id) {
      await userDbRun(
        'DELETE FROM push_subscriptions_pharmacy WHERE user_id = ? AND medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
        [userId, medicine_nregistro, pharmacy_id, endpoint]
      );
    } else if (medicine_nregistro) {
      await userDbRun(
        'DELETE FROM push_subscriptions WHERE user_id = ? AND medicine_nregistro = ? AND endpoint = ?',
        [userId, medicine_nregistro, endpoint]
      );
    } else {
      await userDbRun('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [userId, endpoint]);
    }
    res.status(204).end();
  } catch (error) {
    console.error('Error unsubscribing from push:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Expo Push tokens (mobile app)

app.post('/api/notifications/expo-register', requireAuth, async (req, res) => {
  try {
    const { token, medicine_nregistro, medicine_name, pharmacy_id } = req.body || {};
    const userId = req.session.userId;
    if (!token) {
      return res.status(400).json({ error: 'expo token is required' });
    }

    // Upsert the token for this user
    const existing = await userDbGet(
      'SELECT id FROM expo_push_tokens WHERE user_id = ? AND expo_token = ?',
      [userId, token]
    );
    if (!existing) {
      await userDbRun(
        'INSERT INTO expo_push_tokens (user_id, expo_token) VALUES (?, ?)',
        [userId, token]
      );
    }

    // Also store the medicine subscription (reuse existing tables with expo prefix for endpoint)
    if (medicine_nregistro) {
      const expoEndpoint = `expo://${token}`;
      if (pharmacy_id) {
        const existingSub = await userDbGet(
          'SELECT id FROM push_subscriptions_pharmacy WHERE user_id = ? AND medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
          [userId, medicine_nregistro, pharmacy_id, expoEndpoint]
        );
        if (!existingSub) {
          await userDbRun(
            'INSERT INTO push_subscriptions_pharmacy (user_id, medicine_nregistro, medicine_name, pharmacy_id, endpoint, p256dh, auth) VALUES (?, ?, ?, ?, ?, ?, ?)',
            [userId, medicine_nregistro, medicine_name || null, pharmacy_id, expoEndpoint, '', '']
          );
        }
      } else {
        const existingSub = await userDbGet(
          'SELECT id FROM push_subscriptions WHERE user_id = ? AND medicine_nregistro = ? AND endpoint = ?',
          [userId, medicine_nregistro, expoEndpoint]
        );
        if (!existingSub) {
          await userDbRun(
            'INSERT INTO push_subscriptions (user_id, medicine_nregistro, medicine_name, endpoint, p256dh, auth) VALUES (?, ?, ?, ?, ?, ?)',
            [userId, medicine_nregistro, medicine_name || null, expoEndpoint, '', '']
          );
        }
      }
    }

    res.status(201).json({ ok: true });
  } catch (error) {
    console.error('Error registering Expo push token:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.delete('/api/notifications/expo-unregister', requireAuth, async (req, res) => {
  try {
    const { medicine_nregistro, pharmacy_id } = req.body || {};
    const userId = req.session.userId;

    // Get all expo tokens for this user to find the endpoints
    const tokens = await userDbAll(
      'SELECT expo_token FROM expo_push_tokens WHERE user_id = ?',
      [userId]
    );

    for (const t of tokens || []) {
      const expoEndpoint = `expo://${t.expo_token}`;
      if (pharmacy_id && medicine_nregistro) {
        await userDbRun(
          'DELETE FROM push_subscriptions_pharmacy WHERE user_id = ? AND medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
          [userId, medicine_nregistro, pharmacy_id, expoEndpoint]
        );
      } else if (medicine_nregistro) {
        await userDbRun(
          'DELETE FROM push_subscriptions WHERE user_id = ? AND medicine_nregistro = ? AND endpoint = ?',
          [userId, medicine_nregistro, expoEndpoint]
        );
      }
    }

    // If no medicine specified, remove the token entirely
    if (!medicine_nregistro) {
      await userDbRun('DELETE FROM expo_push_tokens WHERE user_id = ?', [userId]);
    }

    res.status(204).end();
  } catch (error) {
    console.error('Error unregistering Expo push token:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// List the current user's saved notifications across all their devices
app.get('/api/notifications/mine', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const globalRows = await userDbAll(
      `SELECT id, medicine_nregistro, medicine_name, created_at
         FROM push_subscriptions
        WHERE user_id = ?
        ORDER BY created_at DESC`,
      [userId]
    );
    const pharmacyRows = await userDbAll(
      `SELECT ps.id, ps.medicine_nregistro, ps.medicine_name, ps.pharmacy_id,
              ps.created_at, p.name AS pharmacy_name, p.address AS pharmacy_address
         FROM push_subscriptions_pharmacy ps
         LEFT JOIN pharmacies p ON p.id = ps.pharmacy_id
        WHERE ps.user_id = ?
        ORDER BY ps.created_at DESC`,
      [userId]
    );
    res.json({
      global: globalRows.map(r => ({ ...r, scope: 'global' })),
      pharmacy: pharmacyRows.map(r => ({ ...r, scope: 'pharmacy' })),
    });
  } catch (error) {
    console.error('Error listing user notifications:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Delete one of the user's saved notifications by row id + scope
app.delete('/api/notifications/mine', requireAuth, async (req, res) => {
  try {
    const userId = req.session.userId;
    const { scope, id } = req.body || {};
    const rowId = Number(id);
    if (!Number.isInteger(rowId) || rowId <= 0) {
      return res.status(400).json({ error: 'id must be a positive integer' });
    }
    if (scope === 'pharmacy') {
      await userDbRun(
        'DELETE FROM push_subscriptions_pharmacy WHERE id = ? AND user_id = ?',
        [rowId, userId]
      );
    } else if (scope === 'global') {
      await userDbRun(
        'DELETE FROM push_subscriptions WHERE id = ? AND user_id = ?',
        [rowId, userId]
      );
    } else {
      return res.status(400).json({ error: "scope must be 'global' or 'pharmacy'" });
    }
    res.status(204).end();
  } catch (error) {
    console.error('Error deleting user notification:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function sendPushForMedicine({ medicine_nregistro, medicine_name, pharmacy }) {
  if (!PUSH_ENABLED && !EXPO_ACCESS_TOKEN) return { sent: 0, failed: 0, pruned: 0 };

  const globalSubs = await userDbAll(
    'SELECT id, endpoint, p256dh, auth FROM push_subscriptions WHERE medicine_nregistro = ?',
    [medicine_nregistro]
  );
  const pharmacySubs = pharmacy?.id ? await userDbAll(
    'SELECT id, endpoint, p256dh, auth FROM push_subscriptions_pharmacy WHERE medicine_nregistro = ? AND pharmacy_id = ?',
    [medicine_nregistro, pharmacy.id]
  ) : [];

  // Deduplicate by endpoint; pharmacy-specific overrides global so one notification per device
  const endpointMap = new Map();
  for (const sub of (globalSubs || [])) endpointMap.set(sub.endpoint, { ...sub, _pharmSub: false });
  for (const sub of (pharmacySubs || [])) endpointMap.set(sub.endpoint, { ...sub, _pharmSub: true });
  const subs = [...endpointMap.values()];

  if (subs.length === 0) return { sent: 0, failed: 0, pruned: 0 };

  // Split into web push (VAPID) and Expo push (mobile)
  const webSubs = [];
  const expoTokens = [];
  for (const sub of subs) {
    if (sub.endpoint?.startsWith('expo://')) {
      const token = sub.endpoint.replace('expo://', '');
      if (!expoTokens.includes(token)) expoTokens.push(token);
    } else {
      webSubs.push(sub);
    }
  }

  const title = 'Medicamento disponible';
  const body = `${medicine_name || medicine_nregistro} está disponible en ${pharmacy?.name || 'una farmacia cercana'}`;
  const url = `/?nregistro=${encodeURIComponent(medicine_nregistro)}`;

  let sent = 0;
  let failed = 0;
  let pruned = 0;

  // Web Push via VAPID
  if (PUSH_ENABLED && webSubs.length > 0) {
    const payload = JSON.stringify({
      title,
      body,
      url,
      medicine_nregistro,
      medicine_name: medicine_name || null,
      pharmacy_id: pharmacy?.id ?? null,
      pharmacy_name: pharmacy?.name ?? null,
    });

    await Promise.all(webSubs.map(async (sub) => {
      try {
        await webpush.sendNotification(
          { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
          payload
        );
        sent++;
        appMetrics.pushSentTotal.add(1, { channel: 'web' });
      } catch (err) {
        failed++;
        appMetrics.pushFailedTotal.add(1, { channel: 'web' });
        const status = err?.statusCode;
        if (status === 404 || status === 410) {
          try {
            const deleteQuery = sub._pharmSub
              ? 'DELETE FROM push_subscriptions_pharmacy WHERE id = ?'
              : 'DELETE FROM push_subscriptions WHERE id = ?';
            await userDbRun(deleteQuery, [sub.id]);
            pruned++;
          } catch {}
        } else {
          console.error('[push] web send failed:', status, err?.body || err?.message);
        }
      }
    }));
  }

  // Expo Push (mobile)
  if (EXPO_ACCESS_TOKEN && expoTokens.length > 0) {
    const chunks = [];
    for (let i = 0; i < expoTokens.length; i += 100) {
      chunks.push(expoTokens.slice(i, i + 100));
    }

    for (const chunk of chunks) {
      const messages = chunk.map((token) => ({
        to: token,
        sound: 'default',
        title,
        body,
        data: {
          url,
          medicine_nregistro,
          medicine_name: medicine_name || null,
          pharmacy_id: pharmacy?.id ?? null,
          pharmacy_name: pharmacy?.name ?? null,
        },
      }));

      try {
        const response = await fetch('https://exp.host/--/api/v2/push/send', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${EXPO_ACCESS_TOKEN}`,
          },
          body: JSON.stringify(messages),
        });

        if (response.ok) {
          const result = await response.json();
          // Handle individual ticket errors
          if (result.data) {
            for (const ticket of result.data) {
              if (ticket.status === 'ok') {
                sent++;
                appMetrics.pushSentTotal.add(1, { channel: 'expo' });
              } else {
                failed++;
                appMetrics.pushFailedTotal.add(1, { channel: 'expo' });
                // Prune invalid tokens
                if (ticket.message?.includes('InvalidCredentials') || ticket.message?.includes('DeviceNotRegistered')) {
                  const expoEndpoint = `expo://${ticket.id || ''}`;
                  try {
                    await userDbRun('DELETE FROM push_subscriptions WHERE endpoint = ?', [expoEndpoint]);
                    await userDbRun('DELETE FROM push_subscriptions_pharmacy WHERE endpoint = ?', [expoEndpoint]);
                    await userDbRun('DELETE FROM expo_push_tokens WHERE expo_token = ?', [ticket.id || '']);
                    pruned++;
                  } catch {}
                }
              }
            }
          } else {
            sent += chunk.length;
            appMetrics.pushSentTotal.add(chunk.length, { channel: 'expo' });
          }
        } else {
          failed += chunk.length;
          appMetrics.pushFailedTotal.add(chunk.length, { channel: 'expo' });
          console.error('[push] expo send failed:', response.status);
        }
      } catch (err) {
        failed += chunk.length;
        appMetrics.pushFailedTotal.add(chunk.length, { channel: 'expo' });
        console.error('[push] expo send error:', err.message);
      }
    }
  }

  return { sent, failed, pruned };
}

// --- Health & readiness probes --------------------------------------------
// /healthz: liveness (process is up).  /readyz: dependencies reachable.
app.get('/healthz', (_req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

app.get('/readyz', async (_req, res) => {
  const checks = { redis: 'ok', db: 'ok' };
  try {
    await redisClient.ping();
  } catch {
    checks.redis = 'fail';
  }
  try {
    await userDbGet('SELECT 1');
  } catch {
    checks.db = 'fail';
  }
  const ready = checks.redis === 'ok' && checks.db === 'ok';
  res.status(ready ? 200 : 503).json({ status: ready ? 'ok' : 'degraded', checks });
});

export { app, initDatabase, db };

if (process.env.NODE_ENV !== 'test') {
  initDatabase().then(() => {
    app.listen(PORT, () => {
      console.log(`Server running on http://localhost:${PORT}`);
    });
  });
}

