import express from 'express';
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 { searchMedicines, getMedicineDetails } from './cima-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;

// 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());

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-finder-secret-key-change-in-production',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    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));

const searchLimiter = rateLimit({ windowMs: 60_000, max: 30, standardHeaders: true, legacyHeaders: false });
const loginLimiter = rateLimit({ windowMs: 60_000, max: 5, standardHeaders: true, legacyHeaders: false });
const registerLimiter = rateLimit({ windowMs: 60 * 60_000, max: 10, standardHeaders: true, legacyHeaders: false });
const geocodeLimiter = rateLimit({ windowMs: 60_000, max: 10, standardHeaders: true, legacyHeaders: false });

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 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 — push notifications 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 = []) {
  if (pgPool) {
    const res = await pgPool.query(toPositional(sql), params);
    return res.rows[0] ?? null;
  }
  return dbGet(sql, params);
}

async function userDbRun(sql, params = []) {
  if (pgPool) {
    const res = await pgPool.query(toPositional(sql), params);
    return { lastID: res.rows[0]?.id, changes: res.rowCount };
  }
  return dbRun(sql.replace(/\s+RETURNING\s+\w+\s*$/i, ''), params);
}

// 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
    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
    // Ahora usa nregistro (número de registro de CIMA) en lugar de medicine_id local
    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 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
        )
      `);
    }

    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)`);

    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 user_id to push tables (SQLite — no cross-DB FK)
    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 {}

    console.log('Database initialized successfully');
  } catch (error) {
    console.error('Error initializing database:', error);
  }
}

// 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

    const pharmacies = await dbAll(`
      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]);

    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' });
  }
});

// Get all pharmacies (for admin/debugging)
app.get('/api/pharmacies', async (req, res) => {
  try {
    const pharmacies = await dbAll(`
      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' });
  }
});

// ========== 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' });
};

// ========== 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) {
      return res.status(401).json({ error: 'Invalid username or password' });
    }

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

    if (!isValidPassword) {
      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);

    res.json({
      message: 'Login successful',
      user: {
        id: user.id,
        username: user.username,
        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,
        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, 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,
          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, 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,
      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 { address, latitude, longitude } = req.body || {};
    const addr =
      address == null || String(address).trim() === ''
        ? null
        : String(address).trim().slice(0, 500);

    let lat = null;
    let lon = null;
    if (latitude !== '' && latitude != null) {
      lat = typeof latitude === 'number' ? latitude : parseFloat(latitude);
      if (!Number.isFinite(lat) || lat < -90 || lat > 90) {
        return res.status(400).json({ error: 'Invalid latitude' });
      }
    }
    if (longitude !== '' && longitude != null) {
      lon = typeof longitude === 'number' ? longitude : parseFloat(longitude);
      if (!Number.isFinite(lon) || lon < -180 || lon > 180) {
        return res.status(400).json({ error: 'Invalid longitude' });
      }
    }

    await userDbRun(
      'UPDATE users SET address = ?, latitude = ?, longitude = ? WHERE id = ?',
      [addr, lat, lon, req.session.userId]
    );

    const user = await userDbGet(
      'SELECT id, username, is_admin, address, latitude, longitude FROM users WHERE id = ?',
      [req.session.userId]
    );
    res.json({
      id: user.id,
      username: user.username,
      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' });
  }
});

// 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':
          'FarmaFinder/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 dbGet(
      '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 dbRun(
      '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 dbGet(
      'SELECT * FROM pharmacies WHERE id = ?',
      [result.lastID]
    );

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

    res.status(201).json(newPharmacy);
  } 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 dbRun(
      '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 dbGet(
      'SELECT * FROM pharmacies WHERE id = ?',
      [pharmacyId]
    );

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

    res.json(updatedPharmacy);
  } 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 first
    await dbRun('DELETE FROM pharmacy_medicines WHERE pharmacy_id = ?', [pharmacyId]);
    
    // Delete the pharmacy
    await dbRun('DELETE FROM pharmacies WHERE id = ?', [pharmacyId]);

    res.json({ message: 'Pharmacy deleted successfully' });
  } 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(
      dbGet,
      dbRun,
      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(dbGet, dbRun, 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 dbAll(`
      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 dbGet(
      'SELECT * FROM pharmacy_medicines WHERE pharmacy_id = ? AND medicine_nregistro = ?',
      [pharmacy_id, medicine_nregistro]
    );

    if (existing) {
      // Update existing relationship
      await dbRun(
        '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 dbRun(
        '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 dbGet(
      `SELECT * FROM pharmacy_medicines
       WHERE pharmacy_id = ? AND medicine_nregistro = ?`,
      [pharmacy_id, medicine_nregistro]
    );

    if (!existing) {
      const pharmacy = await dbGet('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);
  } 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 dbRun(
      'UPDATE pharmacy_medicines SET price = ?, stock = ? WHERE id = ?',
      [price || null, stock || 0, id]
    );

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

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

    res.json(updated);
  } 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 dbRun('DELETE FROM pharmacy_medicines WHERE id = ?', [id]);
    res.json({ message: 'Medicine removed from pharmacy successfully' });
  } catch (error) {
    console.error('Error deleting pharmacy-medicine:', 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 dbGet(
        'SELECT id FROM push_subscriptions_pharmacy WHERE medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
        [medicine_nregistro, pharmacy_id, endpoint]
      );
      if (existing) {
        await dbRun(
          'UPDATE push_subscriptions_pharmacy SET medicine_name = ?, p256dh = ?, auth = ?, user_id = ? WHERE id = ?',
          [medicine_name || null, p256dh, auth, userId, existing.id]
        );
      } else {
        await dbRun(
          'INSERT INTO push_subscriptions_pharmacy (medicine_nregistro, medicine_name, pharmacy_id, endpoint, p256dh, auth, user_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
          [medicine_nregistro, medicine_name || null, pharmacy_id, endpoint, p256dh, auth, userId]
        );
      }
    } else {
      const existing = await dbGet(
        'SELECT id FROM push_subscriptions WHERE medicine_nregistro = ? AND endpoint = ?',
        [medicine_nregistro, endpoint]
      );
      if (existing) {
        await dbRun(
          'UPDATE push_subscriptions SET medicine_name = ?, p256dh = ?, auth = ?, user_id = ? WHERE id = ?',
          [medicine_name || null, p256dh, auth, userId, existing.id]
        );
      } else {
        await dbRun(
          'INSERT INTO push_subscriptions (medicine_nregistro, medicine_name, endpoint, p256dh, auth, user_id) VALUES (?, ?, ?, ?, ?, ?)',
          [medicine_nregistro, medicine_name || null, endpoint, p256dh, auth, userId]
        );
      }
    }

    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 || {};
    if (!endpoint) {
      return res.status(400).json({ error: 'endpoint is required' });
    }
    if (pharmacy_id) {
      await dbRun(
        'DELETE FROM push_subscriptions_pharmacy WHERE medicine_nregistro = ? AND pharmacy_id = ? AND endpoint = ?',
        [medicine_nregistro, pharmacy_id, endpoint]
      );
    } else if (medicine_nregistro) {
      await dbRun(
        'DELETE FROM push_subscriptions WHERE medicine_nregistro = ? AND endpoint = ?',
        [medicine_nregistro, endpoint]
      );
    } else {
      await dbRun('DELETE FROM push_subscriptions WHERE endpoint = ?', [endpoint]);
    }
    res.status(204).end();
  } catch (error) {
    console.error('Error unsubscribing from push:', 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 dbAll(
      `SELECT id, medicine_nregistro, medicine_name, created_at
         FROM push_subscriptions
        WHERE user_id = ?
        ORDER BY created_at DESC`,
      [userId]
    );
    const pharmacyRows = await dbAll(
      `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 dbRun(
        'DELETE FROM push_subscriptions_pharmacy WHERE id = ? AND user_id = ?',
        [rowId, userId]
      );
    } else if (scope === 'global') {
      await dbRun(
        '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) return { sent: 0, failed: 0, pruned: 0 };

  const globalSubs = await dbAll(
    'SELECT id, endpoint, p256dh, auth FROM push_subscriptions WHERE medicine_nregistro = ?',
    [medicine_nregistro]
  );
  const pharmacySubs = pharmacy?.id ? await dbAll(
    '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 browser
  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 };

  const payload = JSON.stringify({
    title: 'Medicine available',
    body: `${medicine_name || medicine_nregistro} is now available at ${pharmacy?.name || 'a pharmacy near you'}`,
    url: `/?nregistro=${encodeURIComponent(medicine_nregistro)}`,
    medicine_nregistro,
    medicine_name: medicine_name || null,
    pharmacy_id: pharmacy?.id ?? null,
    pharmacy_name: pharmacy?.name ?? null,
  });

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

  await Promise.all(subs.map(async (sub) => {
    try {
      await webpush.sendNotification(
        { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
        payload
      );
      sent++;
    } catch (err) {
      failed++;
      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 dbRun(deleteQuery, [sub.id]);
          pruned++;
        } catch {}
      } else {
        console.error('[push] send failed:', status, err?.body || err?.message);
      }
    }
  }));

  return { sent, failed, pruned };
}

export { app, initDatabase, db };

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

