import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import { DEFAULT_WEIGHTS, scoreProfile, safeStr as scoreSafeStr } from './matchScoring.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Always use ../data (relative to server/) so Docker and local dev use the same directory
const DATA_DIR = process.env.DATA_PATH || path.join(__dirname, '..', 'data');
const PORT = process.env.PORT || 3001;

const app = express();
app.use(cors());
app.use(express.json({ limit: '5mb' }));

// Ensure data directory exists
if (!fs.existsSync(DATA_DIR)) {
  fs.mkdirSync(DATA_DIR, { recursive: true });
}

// Auth credentials file
const AUTH_FILE = path.join(DATA_DIR, 'auth.json');

// Read auth credentials
const getAuthCredentials = () => {
  if (fs.existsSync(AUTH_FILE)) {
    try {
      return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'));
    } catch (e) {
      console.error('Failed to read auth file:', e);
    }
  }
  // Default credentials if file doesn't exist
  return { username: 'admin', password: 'feeld123' };
};

// Simple session store (in-memory, resets on restart)
const sessions = new Map();

// Helper to get user data file path
const getUserDataPath = (userId) => {
  // Sanitize userId to prevent path traversal
  const safeId = userId.replace(/[^a-zA-Z0-9-_]/g, '_');
  return path.join(DATA_DIR, `${safeId}.json`);
};

// Helper to read user data
const readUserData = (userId) => {
  const filePath = getUserDataPath(userId);
  if (fs.existsSync(filePath)) {
    try {
      return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read user data:', e);
      return {};
    }
  }
  return {};
};

// Helper to write user data
const writeUserData = (userId, data) => {
  const filePath = getUserDataPath(userId);
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
};

// GET /api/data/:userId - Get all user data
app.get('/api/data/:userId', (req, res) => {
  const { userId } = req.params;
  const data = readUserData(userId);
  res.json(data);
});

// PUT /api/data/:userId - Update all user data
app.put('/api/data/:userId', (req, res) => {
  const { userId } = req.params;
  const data = req.body;
  writeUserData(userId, data);
  res.json({ success: true, data });
});

// GET /api/data/:userId/:key - Get specific key
app.get('/api/data/:userId/:key', (req, res) => {
  const { userId, key } = req.params;
  const data = readUserData(userId);
  res.json({ [key]: data[key] || null });
});

// PUT /api/data/:userId/:key - Update specific key
app.put('/api/data/:userId/:key', (req, res) => {
  const { userId, key } = req.params;
  const { value } = req.body;
  const data = readUserData(userId);
  data[key] = value;
  data.updatedAt = new Date().toISOString();
  writeUserData(userId, data);
  res.json({ success: true, [key]: value });
});

// DELETE /api/data/:userId/:key - Delete specific key
app.delete('/api/data/:userId/:key', (req, res) => {
  const { userId, key } = req.params;
  const data = readUserData(userId);
  delete data[key];
  writeUserData(userId, data);
  res.json({ success: true });
});

// POST /api/data/:userId/liked-profiles - Add a liked profile
app.post('/api/data/:userId/liked-profiles', (req, res) => {
  const { userId } = req.params;
  const { id, name } = req.body;

  const data = readUserData(userId);
  if (!data.likedProfiles) {
    data.likedProfiles = [];
  }

  // Don't add duplicates
  if (!data.likedProfiles.some(p => p.id === id)) {
    data.likedProfiles.unshift({
      id,
      name,
      likedAt: Date.now(),
    });
    data.updatedAt = new Date().toISOString();
    writeUserData(userId, data);
  }

  res.json({ success: true, likedProfiles: data.likedProfiles });
});

// DELETE /api/data/:userId/liked-profiles/:profileId - Remove a liked profile
app.delete('/api/data/:userId/liked-profiles/:profileId', (req, res) => {
  const { userId, profileId } = req.params;

  const data = readUserData(userId);
  if (data.likedProfiles) {
    data.likedProfiles = data.likedProfiles.filter(p => p.id !== profileId);
    writeUserData(userId, data);
  }

  res.json({ success: true, likedProfiles: data.likedProfiles || [] });
});

// GET /api/who-liked-you - Get profiles who liked you (discovered via scanning)
app.get('/api/who-liked-you', (req, res) => {
  const filePath = path.join(DATA_DIR, 'whoLikedYou.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      res.json(data);
    } catch (e) {
      console.error('Failed to read whoLikedYou.json:', e);
      res.json({ profiles: [] });
    }
  } else {
    res.json({ profiles: [] });
  }
});

// POST /api/who-liked-you - Add or update a profile who liked you
app.post('/api/who-liked-you', (req, res) => {
  const { profile } = req.body;
  const filePath = path.join(DATA_DIR, 'whoLikedYou.json');

  let data = { profiles: [], updatedAt: null };
  if (fs.existsSync(filePath)) {
    try {
      data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read whoLikedYou.json:', e);
    }
  }

  // Find existing profile by ID
  const existingIndex = data.profiles.findIndex(p => p.id === profile.id);

  if (existingIndex >= 0) {
    // Update existing profile, preserving original discoveredAt
    const originalDiscoveredAt = data.profiles[existingIndex].discoveredAt;
    data.profiles[existingIndex] = {
      ...profile,
      discoveredAt: originalDiscoveredAt,
      updatedAt: new Date().toISOString(),
    };
    console.log('Updated existing profile:', profile.imaginaryName);
  } else {
    // Add new profile
    data.profiles.unshift({
      ...profile,
      discoveredAt: new Date().toISOString(),
    });
    console.log('Added new profile:', profile.imaginaryName);
  }

  data.updatedAt = new Date().toISOString();
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));

  res.json({ success: true, profile });
});

// DELETE /api/who-liked-you/:profileId - Remove a discovered profile
app.delete('/api/who-liked-you/:profileId', (req, res) => {
  const { profileId } = req.params;
  const filePath = path.join(DATA_DIR, 'whoLikedYou.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      data.profiles = data.profiles.filter(p => p.id !== profileId);
      data.updatedAt = new Date().toISOString();
      fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      res.json({ success: true, profiles: data.profiles });
    } catch (e) {
      console.error('Failed to update whoLikedYou.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: true, profiles: [] });
  }
});

// GET /api/sent-pings - Get all sent pings
app.get('/api/sent-pings', (req, res) => {
  const filePath = path.join(DATA_DIR, 'sentPings.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      res.json(data);
    } catch (e) {
      console.error('Failed to read sentPings.json:', e);
      res.json({ pings: [] });
    }
  } else {
    res.json({ pings: [] });
  }
});

// POST /api/sent-pings - Add a sent ping
app.post('/api/sent-pings', (req, res) => {
  const { targetProfileId, targetName, message } = req.body;
  const filePath = path.join(DATA_DIR, 'sentPings.json');

  let data = { pings: [], updatedAt: null };
  if (fs.existsSync(filePath)) {
    try {
      data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read sentPings.json:', e);
    }
  }

  // Don't add duplicates
  if (!data.pings.some(p => p.targetProfileId === targetProfileId)) {
    data.pings.unshift({
      targetProfileId,
      targetName,
      message,
      sentAt: Date.now(),
      status: 'SENT',
    });
    data.updatedAt = new Date().toISOString();
    fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
  }

  res.json({ success: true, pings: data.pings });
});

// PUT /api/sent-pings/:targetProfileId - Update a sent ping status
app.put('/api/sent-pings/:targetProfileId', (req, res) => {
  const { targetProfileId } = req.params;
  const { status } = req.body;
  const filePath = path.join(DATA_DIR, 'sentPings.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      const ping = data.pings.find(p => p.targetProfileId === targetProfileId);
      if (ping) {
        ping.status = status;
        data.updatedAt = new Date().toISOString();
        fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      }
      res.json({ success: true, pings: data.pings });
    } catch (e) {
      console.error('Failed to update sentPings.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: false, error: 'No sent pings found' });
  }
});

// DELETE /api/sent-pings/:targetProfileId - Remove a sent ping
app.delete('/api/sent-pings/:targetProfileId', (req, res) => {
  const { targetProfileId } = req.params;
  const filePath = path.join(DATA_DIR, 'sentPings.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      data.pings = data.pings.filter(p => p.targetProfileId !== targetProfileId);
      data.updatedAt = new Date().toISOString();
      fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      res.json({ success: true, pings: data.pings });
    } catch (e) {
      console.error('Failed to update sentPings.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: true, pings: [] });
  }
});

// GET /api/favorites - Get all saved/favorited profiles
app.get('/api/favorites', (req, res) => {
  const filePath = path.join(DATA_DIR, 'favorites.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      res.json(data);
    } catch (e) {
      console.error('Failed to read favorites.json:', e);
      res.json({ favorites: [] });
    }
  } else {
    res.json({ favorites: [] });
  }
});

// POST /api/favorites - Add a favorited profile (idempotent)
app.post('/api/favorites', (req, res) => {
  const { targetProfileId, profile } = req.body;
  if (!targetProfileId) {
    return res.status(400).json({ success: false, error: 'targetProfileId required' });
  }
  const filePath = path.join(DATA_DIR, 'favorites.json');

  let data = { favorites: [], updatedAt: null };
  if (fs.existsSync(filePath)) {
    try {
      data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read favorites.json:', e);
    }
  }

  if (!data.favorites.some(f => f.targetProfileId === targetProfileId)) {
    data.favorites.unshift({
      targetProfileId,
      savedAt: Date.now(),
      profile: profile || null,
    });
    data.updatedAt = new Date().toISOString();
    fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
  }

  res.json({ success: true, favorites: data.favorites });
});

// DELETE /api/favorites/:targetProfileId - Remove a favorite
app.delete('/api/favorites/:targetProfileId', (req, res) => {
  const { targetProfileId } = req.params;
  const filePath = path.join(DATA_DIR, 'favorites.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      data.favorites = data.favorites.filter(f => f.targetProfileId !== targetProfileId);
      data.updatedAt = new Date().toISOString();
      fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      res.json({ success: true, favorites: data.favorites });
    } catch (e) {
      console.error('Failed to update favorites.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: true, favorites: [] });
  }
});

// DELETE /api/favorites - Clear all favorites
app.delete('/api/favorites', (req, res) => {
  const filePath = path.join(DATA_DIR, 'favorites.json');
  const data = { favorites: [], updatedAt: new Date().toISOString() };
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
  res.json({ success: true, favorites: [] });
});

// GET /api/disliked-profiles - Get all disliked profiles
app.get('/api/disliked-profiles', (req, res) => {
  const filePath = path.join(DATA_DIR, 'dislikedProfiles.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      res.json(data);
    } catch (e) {
      console.error('Failed to read dislikedProfiles.json:', e);
      res.json({ profiles: [] });
    }
  } else {
    res.json({ profiles: [] });
  }
});

// POST /api/disliked-profiles - Add a disliked profile
app.post('/api/disliked-profiles', (req, res) => {
  const { profile } = req.body;
  const filePath = path.join(DATA_DIR, 'dislikedProfiles.json');

  let data = { profiles: [], updatedAt: null };
  if (fs.existsSync(filePath)) {
    try {
      data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read dislikedProfiles.json:', e);
    }
  }

  // Don't add duplicates - check by ID
  if (!data.profiles.some(p => p.id === profile.id)) {
    data.profiles.unshift({
      ...profile,
      dislikedAt: new Date().toISOString(),
    });
    data.updatedAt = new Date().toISOString();
    fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
    console.log('Added disliked profile:', profile.imaginaryName);
  }

  res.json({ success: true, profiles: data.profiles });
});

// DELETE /api/disliked-profiles/:profileId - Remove a disliked profile
app.delete('/api/disliked-profiles/:profileId', (req, res) => {
  const { profileId } = req.params;
  const filePath = path.join(DATA_DIR, 'dislikedProfiles.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      data.profiles = data.profiles.filter(p => p.id !== profileId);
      data.updatedAt = new Date().toISOString();
      fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      res.json({ success: true, profiles: data.profiles });
    } catch (e) {
      console.error('Failed to update dislikedProfiles.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: true, profiles: [] });
  }
});

// GET /api/discovered-profiles - Get all discovered profiles cache
app.get('/api/discovered-profiles', (req, res) => {
  const filePath = path.join(DATA_DIR, 'discoveredProfiles.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      res.json(data);
    } catch (e) {
      console.error('Failed to read discoveredProfiles.json:', e);
      res.json({ profiles: [] });
    }
  } else {
    res.json({ profiles: [] });
  }
});

// GET /api/discovered-profiles/lookup/:id - Get a single cached profile
app.get('/api/discovered-profiles/lookup/:id', (req, res) => {
  const filePath = path.join(DATA_DIR, 'discoveredProfiles.json');
  if (!fs.existsSync(filePath)) return res.json({ profile: null });
  try {
    const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    const target = (data.profiles || []).find(p => p.id === req.params.id);
    res.json({ profile: target || null });
  } catch (e) {
    res.json({ profile: null });
  }
});

// POST /api/discovered-profiles/batch - Batch upsert discovered profiles
app.post('/api/discovered-profiles/batch', (req, res) => {
  const { profiles: incoming } = req.body;
  if (!Array.isArray(incoming) || incoming.length === 0) {
    return res.status(400).json({ success: false, error: 'profiles array required' });
  }

  const filePath = path.join(DATA_DIR, 'discoveredProfiles.json');

  let data = { profiles: [], updatedAt: null };
  if (fs.existsSync(filePath)) {
    try {
      data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error('Failed to read discoveredProfiles.json:', e);
    }
  }

  const existingMap = new Map(data.profiles.map(p => [p.id, p]));
  let added = 0;
  let updated = 0;

  for (const profile of incoming) {
    if (!profile.id) continue;
    const existing = existingMap.get(profile.id);
    if (existing) {
      // Last-seen wins for discoveredLocation; fall back to prior value if
      // this batch didn't carry one.
      existingMap.set(profile.id, {
        ...existing,
        ...profile,
        discoveredLocation: profile.discoveredLocation ?? existing.discoveredLocation ?? null,
        discoveredAt: existing.discoveredAt,
        updatedAt: new Date().toISOString(),
      });
      updated++;
    } else {
      existingMap.set(profile.id, {
        ...profile,
        discoveredLocation: profile.discoveredLocation ?? null,
        discoveredAt: profile.discoveredAt || new Date().toISOString(),
      });
      added++;
    }
  }

  // Convert back to array, sort by discoveredAt descending (newest first)
  let allProfiles = Array.from(existingMap.values());
  allProfiles.sort((a, b) => {
    const dateA = a.discoveredAt ? new Date(a.discoveredAt).getTime() : 0;
    const dateB = b.discoveredAt ? new Date(b.discoveredAt).getTime() : 0;
    return dateB - dateA;
  });

  // Cap at 2000 profiles, evict oldest
  if (allProfiles.length > 2000) {
    allProfiles = allProfiles.slice(0, 2000);
  }

  data.profiles = allProfiles;
  data.updatedAt = new Date().toISOString();
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));

  console.log(`Discovered profiles batch: +${added} new, ${updated} updated, ${allProfiles.length} total`);
  res.json({ success: true, added, updated, total: allProfiles.length });
});

// DELETE /api/discovered-profiles/:profileId - Remove a discovered profile
app.delete('/api/discovered-profiles/:profileId', (req, res) => {
  const { profileId } = req.params;
  const filePath = path.join(DATA_DIR, 'discoveredProfiles.json');

  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      data.profiles = data.profiles.filter(p => p.id !== profileId);
      data.updatedAt = new Date().toISOString();
      fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
      res.json({ success: true, profiles: data.profiles });
    } catch (e) {
      console.error('Failed to update discoveredProfiles.json:', e);
      res.status(500).json({ success: false, error: e.message });
    }
  } else {
    res.json({ success: true, profiles: [] });
  }
});

// PUT /api/discovered-profiles/update-photos - Update photos for a cached profile
app.put('/api/discovered-profiles/update-photos', (req, res) => {
  const { profileId, photos } = req.body;
  if (!profileId || !Array.isArray(photos)) {
    return res.status(400).json({ success: false, error: 'profileId and photos array required' });
  }
  const filePath = path.join(DATA_DIR, 'discoveredProfiles.json');
  if (fs.existsSync(filePath)) {
    try {
      const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
      const idx = (data.profiles || []).findIndex(p => p.id === profileId);
      if (idx >= 0) {
        data.profiles[idx].photos = photos;
        data.profiles[idx].photosRefreshedAt = new Date().toISOString();
        data.updatedAt = new Date().toISOString();
        fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
        return res.json({ success: true, updated: true });
      }
      return res.json({ success: true, updated: false, reason: 'profile not found' });
    } catch (e) {
      console.error('Failed to update photos:', e);
      return res.status(500).json({ success: false, error: e.message });
    }
  }
  res.json({ success: true, updated: false, reason: 'no cache file' });
});

// ============================================================
// Smart Matches Endpoints
// ============================================================

const MATCH_WEIGHTS_FILE = path.join(DATA_DIR, 'matchWeights.json');

function readMatchWeights() {
  if (fs.existsSync(MATCH_WEIGHTS_FILE)) {
    try {
      return { ...DEFAULT_WEIGHTS, ...JSON.parse(fs.readFileSync(MATCH_WEIGHTS_FILE, 'utf8')) };
    } catch (e) {
      console.error('Failed to read matchWeights.json:', e);
    }
  }
  return { ...DEFAULT_WEIGHTS };
}

function readJsonFile(filename, fallback) {
  const filePath = path.join(DATA_DIR, filename);
  if (fs.existsSync(filePath)) {
    try {
      return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      console.error(`Failed to read ${filename}:`, e);
    }
  }
  return fallback;
}

// GET /api/matches — Score, filter, and rank discovered profiles
app.get('/api/matches', (req, res) => {
  try {
    const {
      minAge, maxAge, maxDistance, gender, sexuality, desires,
      verifiedOnly, search, theyLikedOnly, sort,
      limit = '50', offset = '0',
    } = req.query;

    const limitNum = Math.min(parseInt(limit) || 50, 200);
    const offsetNum = parseInt(offset) || 0;

    // 1. Read discovered profiles
    const discovered = readJsonFile('discoveredProfiles.json', { profiles: [] });
    let profiles = discovered.profiles || [];

    // 2. Build exclusion sets
    const disliked = readJsonFile('dislikedProfiles.json', { profiles: [] });
    const dislikedIds = new Set((disliked.profiles || []).map(p => p.id));

    const sentPings = readJsonFile('sentPings.json', { pings: [] });
    const sentPingIds = new Set((sentPings.pings || []).map(p => p.targetProfileId));

    // Read user.json to get liked profiles
    const userFiles = fs.readdirSync(DATA_DIR).filter(f => f.endsWith('.json') && !['discoveredProfiles', 'dislikedProfiles', 'sentPings', 'whoLikedYou', 'auth', 'auth-tokens', 'locationRotation', 'savedLocations', 'matchWeights'].some(n => f.startsWith(n)));
    let likedIds = new Set();
    for (const file of userFiles) {
      try {
        const data = JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8'));
        if (data.likedProfiles) {
          data.likedProfiles.forEach(p => likedIds.add(p.id));
        }
      } catch (e) { /* skip */ }
    }

    // 3. Filter out excluded profiles
    profiles = profiles.filter(p => !dislikedIds.has(p.id) && !sentPingIds.has(p.id) && !likedIds.has(p.id));

    // 4. Apply query param filters
    if (minAge) {
      const min = parseInt(minAge);
      profiles = profiles.filter(p => typeof p.age === 'number' && p.age >= min);
    }
    if (maxAge) {
      const max = parseInt(maxAge);
      profiles = profiles.filter(p => typeof p.age === 'number' && p.age <= max);
    }
    if (maxDistance) {
      const maxDist = parseInt(maxDistance);
      profiles = profiles.filter(p => {
        const mi = p.distance?.mi;
        return typeof mi === 'number' && mi <= maxDist;
      });
    }
    if (gender) {
      const genders = gender.split(',').map(g => g.trim().toUpperCase());
      profiles = profiles.filter(p => {
        const pg = (typeof p.gender === 'string' ? p.gender : '').toUpperCase();
        return genders.includes(pg);
      });
    }
    if (sexuality) {
      const sexualities = sexuality.split(',').map(s => s.trim().toUpperCase());
      profiles = profiles.filter(p => {
        const ps = (typeof p.sexuality === 'string' ? p.sexuality : '').toUpperCase();
        return sexualities.includes(ps);
      });
    }
    if (desires) {
      const desireList = desires.split(',').map(d => d.trim().toUpperCase());
      profiles = profiles.filter(p => {
        const pd = Array.isArray(p.desires) ? p.desires.map(d => (typeof d === 'string' ? d : '').toUpperCase()) : [];
        return desireList.some(d => pd.includes(d));
      });
    }
    if (verifiedOnly === 'true') {
      profiles = profiles.filter(p => {
        const vs = typeof p.verificationStatus === 'string' ? p.verificationStatus : '';
        return vs.toUpperCase() === 'VERIFIED';
      });
    }
    if (theyLikedOnly === 'true') {
      profiles = profiles.filter(p => p.interactionStatus?.theirs === 'LIKED');
    }

    // 5. Text search on name + bio
    if (search) {
      const searchLower = search.toLowerCase();
      profiles = profiles.filter(p => {
        const name = (typeof p.imaginaryName === 'string' ? p.imaginaryName : '').toLowerCase();
        const bio = (typeof p.bio === 'string' ? p.bio : '').toLowerCase();
        return name.includes(searchLower) || bio.includes(searchLower);
      });
    }

    // 6. Score each profile
    const weights = readMatchWeights();
    const scored = profiles.map(p => {
      const { total, breakdown } = scoreProfile(p, weights);
      return { ...p, _score: total, _scoreBreakdown: breakdown };
    });

    // 7. Sort
    if (sort === 'distance') {
      scored.sort((a, b) => (a.distance?.mi ?? 9999) - (b.distance?.mi ?? 9999));
    } else if (sort === 'recent') {
      scored.sort((a, b) => {
        const da = a.discoveredAt ? new Date(a.discoveredAt).getTime() : 0;
        const db = b.discoveredAt ? new Date(b.discoveredAt).getTime() : 0;
        return db - da;
      });
    } else {
      // Default: score descending
      scored.sort((a, b) => b._score - a._score);
    }

    // 8. Paginate
    const total = scored.length;
    const paginated = scored.slice(offsetNum, offsetNum + limitNum);

    res.json({
      matches: paginated,
      total,
      filters: { minAge, maxAge, maxDistance, gender, sexuality, desires, verifiedOnly, search, theyLikedOnly, sort },
      updatedAt: discovered.updatedAt,
    });
  } catch (e) {
    console.error('Failed to compute matches:', e);
    res.status(500).json({ error: e.message });
  }
});

// GET /api/matches/weights — Return current scoring weights
app.get('/api/matches/weights', (req, res) => {
  res.json({ weights: readMatchWeights(), updatedAt: new Date().toISOString() });
});

// PUT /api/matches/weights — Update scoring weights
app.put('/api/matches/weights', (req, res) => {
  const { weights } = req.body;
  if (!weights || typeof weights !== 'object') {
    return res.status(400).json({ success: false, error: 'weights object required' });
  }
  const merged = { ...DEFAULT_WEIGHTS, ...weights };
  fs.writeFileSync(MATCH_WEIGHTS_FILE, JSON.stringify(merged, null, 2));
  res.json({ success: true, weights: merged });
});

// GET /api/matches/summary — Discord-friendly text summary
app.get('/api/matches/summary', (req, res) => {
  try {
    const { limit = '5', ...filterParams } = req.query;

    // Reuse the matches logic by building the same pipeline
    const discovered = readJsonFile('discoveredProfiles.json', { profiles: [] });
    let profiles = discovered.profiles || [];

    // Exclusions
    const disliked = readJsonFile('dislikedProfiles.json', { profiles: [] });
    const dislikedIds = new Set((disliked.profiles || []).map(p => p.id));
    const sentPings = readJsonFile('sentPings.json', { pings: [] });
    const sentPingIds = new Set((sentPings.pings || []).map(p => p.targetProfileId));
    profiles = profiles.filter(p => !dislikedIds.has(p.id) && !sentPingIds.has(p.id));

    // Apply filters
    if (filterParams.theyLikedOnly === 'true') {
      profiles = profiles.filter(p => p.interactionStatus?.theirs === 'LIKED');
    }
    if (filterParams.verifiedOnly === 'true') {
      profiles = profiles.filter(p => (typeof p.verificationStatus === 'string' ? p.verificationStatus : '').toUpperCase() === 'VERIFIED');
    }
    if (filterParams.maxAge) {
      profiles = profiles.filter(p => typeof p.age === 'number' && p.age <= parseInt(filterParams.maxAge));
    }
    if (filterParams.minAge) {
      profiles = profiles.filter(p => typeof p.age === 'number' && p.age >= parseInt(filterParams.minAge));
    }
    if (filterParams.maxDistance) {
      profiles = profiles.filter(p => typeof p.distance?.mi === 'number' && p.distance.mi <= parseInt(filterParams.maxDistance));
    }

    // Score & sort
    const weights = readMatchWeights();
    const scored = profiles.map(p => ({
      ...p,
      _score: scoreProfile(p, weights).total,
    }));
    scored.sort((a, b) => b._score - a._score);

    const topN = scored.slice(0, parseInt(limit) || 5);
    const total = scored.length;

    // Build summary text
    const lines = topN.map((p, i) => {
      const name = typeof p.imaginaryName === 'string' ? p.imaginaryName : 'Unknown';
      const age = p.age || '?';
      const dist = p.distance?.mi != null ? `${Math.round(p.distance.mi)}mi` : '?mi';
      const verified = (typeof p.verificationStatus === 'string' && p.verificationStatus.toUpperCase() === 'VERIFIED') ? 'Verified' : '';
      const liked = p.interactionStatus?.theirs === 'LIKED' ? 'they liked you' : '';
      const tags = [verified, liked, dist].filter(Boolean).join(', ');
      return `${i + 1}. ${name}, ${age} (Score: ${p._score}) — ${tags}`;
    });

    const summary = topN.length > 0
      ? `Top ${topN.length} Matches:\n${lines.join('\n')}`
      : 'No matches found with current filters.';

    res.json({
      summary,
      matches: topN.map(p => ({
        id: p.id,
        imaginaryName: typeof p.imaginaryName === 'string' ? p.imaginaryName : '',
        age: p.age,
        score: p._score,
        distance: p.distance,
        verified: (typeof p.verificationStatus === 'string' && p.verificationStatus.toUpperCase() === 'VERIFIED'),
        theyLikedYou: p.interactionStatus?.theirs === 'LIKED',
      })),
      total,
    });
  } catch (e) {
    console.error('Failed to compute matches summary:', e);
    res.status(500).json({ error: e.message });
  }
});

// Health check
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

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

  if (username === creds.username && password === creds.password) {
    // Generate a simple session token
    const token = crypto.randomUUID();
    sessions.set(token, { username, createdAt: Date.now() });

    res.json({ success: true, token });
  } else {
    res.status(401).json({ success: false, error: 'Invalid credentials' });
  }
});

// Verify session endpoint
app.get('/api/auth/verify', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (token && sessions.has(token)) {
    res.json({ success: true, authenticated: true });
  } else {
    res.status(401).json({ success: false, authenticated: false });
  }
});

// Logout endpoint
app.post('/api/auth/logout', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (token) {
    sessions.delete(token);
  }

  res.json({ success: true });
});

// GET /api/auth/token — Single source of truth: backend manages the refresh token,
// frontend just gets a ready-to-use access token. No more race conditions.
app.get('/api/auth/token', async (req, res) => {
  try {
    // Ensure feeldAPI has credentials loaded
    if (!feeldAPI.refreshToken) {
      feeldAPI.loadCredentials();
    }
    if (!feeldAPI.refreshToken) {
      return res.status(503).json({ error: 'No refresh token available. Seed from mobile app first.' });
    }
    const accessToken = await feeldAPI.getToken();
    res.json({
      accessToken,
      profileId: feeldAPI.profileId,
      analyticsId: feeldAPI.analyticsId,
      expiresAt: feeldAPI.expiresAt,
    });
  } catch (e) {
    console.error('[Auth] Token request failed:', e.message);
    res.status(500).json({ error: e.message });
  }
});

// POST /api/auth/seed — Seed a refresh token (from mobile app Proxyman capture, etc.)
app.post('/api/auth/seed', (req, res) => {
  const { refreshToken, profileId, analyticsId } = req.body;
  if (!refreshToken) return res.status(400).json({ error: 'refreshToken required' });
  feeldAPI.saveCredentials(
    profileId || feeldAPI.profileId,
    refreshToken,
    analyticsId || feeldAPI.analyticsId
  );
  // Clear cached access token so next request gets a fresh one
  feeldAPI.accessToken = null;
  feeldAPI.expiresAt = 0;
  res.json({ success: true });
});

// ============================================================
// Emulate App Open — replicate the mobile app's launch sequence
// ============================================================

app.post('/api/emulate-open', async (req, res) => {
  const { latitude, longitude, locationName } = req.body;
  if (latitude == null || longitude == null) {
    return res.status(400).json({ error: 'latitude and longitude required' });
  }

  if (!feeldAPI.profileId) {
    feeldAPI.loadCredentials();
    if (!feeldAPI.profileId) {
      return res.status(503).json({ error: 'No Feeld credentials. Seed from browser first.' });
    }
  }

  const steps = [];

  try {
    // Step 1: Set device location (mimics GPS update on app open)
    await feeldAPI.updateLocation(latitude, longitude);
    steps.push({ step: 'DeviceLocationUpdate', status: 'ok', location: locationName || `${latitude},${longitude}` });

    // Step 2: Fetch search settings
    let filters = {
      ageRange: [22, 59],
      maxDistance: 100,
      lookingFor: ['WOMAN', 'MAN_WOMAN_COUPLE', 'WOMAN_WOMAN_COUPLE'],
      recentlyOnline: false,
      desiringFor: [],
    };

    try {
      const settings = await feeldAPI.getSearchSettings();
      const profile = settings?.profile;
      if (profile) {
        filters = {
          ageRange: profile.ageRange || filters.ageRange,
          maxDistance: profile.distanceMax || filters.maxDistance,
          lookingFor: profile.lookingFor || filters.lookingFor,
          recentlyOnline: profile.recentlyOnline || false,
          desiringFor: profile.desiringFor || [],
        };
      }
      steps.push({ step: 'SearchSettings', status: 'ok', filters });
    } catch (e) {
      steps.push({ step: 'SearchSettings', status: 'fallback', error: e.message });
    }

    // Step 3: Discover profiles at this location
    const discovery = await feeldAPI.discoverProfiles(filters);
    const profiles = discovery?.discovery?.nodes || [];
    steps.push({ step: 'DiscoverProfiles', status: 'ok', count: profiles.length, hasNextBatch: discovery?.discovery?.hasNextBatch });

    // Step 4: Cache discovered profiles and detect who liked us
    let newProfiles = 0;
    let likedMeFound = 0;
    if (profiles.length > 0) {
      const discoveredFile = path.join(DATA_DIR, 'discoveredProfiles.json');
      let existing = { profiles: [], updatedAt: null };
      try { existing = JSON.parse(fs.readFileSync(discoveredFile, 'utf8')); } catch {}
      const existingMap = new Map(existing.profiles.map(p => [p.id, p]));

      for (const p of profiles) {
        const safeStr = v => (typeof v === 'string' ? v : '');
        const cached = {
          id: p.id,
          imaginaryName: safeStr(p.imaginaryName),
          age: p.age,
          gender: safeStr(p.gender),
          sexuality: safeStr(p.sexuality),
          bio: safeStr(p.bio),
          desires: Array.isArray(p.desires) ? p.desires.filter(d => typeof d === 'string') : [],
          connectionGoals: Array.isArray(p.connectionGoals) ? p.connectionGoals.filter(g => typeof g === 'string') : [],
          verificationStatus: safeStr(p.verificationStatus),
          interactionStatus: p.interactionStatus,
          discoveredLocation: locationName || `${latitude},${longitude}`,
          discoveredAt: existingMap.has(p.id) ? existingMap.get(p.id).discoveredAt : new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        };
        if (!existingMap.has(p.id)) newProfiles++;
        existingMap.set(p.id, cached);

        // Detect who liked us
        if (p.interactionStatus?.theirs === 'LIKED') {
          likedMeFound++;
          // Save to whoLikedYou cache
          const whoFile = path.join(DATA_DIR, 'whoLikedYou.json');
          let whoData = { profiles: [], updatedAt: null };
          try { whoData = JSON.parse(fs.readFileSync(whoFile, 'utf8')); } catch {}
          if (!whoData.profiles.some(w => w.id === p.id)) {
            whoData.profiles.push({
              id: p.id,
              imaginaryName: safeStr(p.imaginaryName),
              age: p.age,
              gender: safeStr(p.gender),
              sexuality: safeStr(p.sexuality),
              photos: p.photos,
              discoveredAt: new Date().toISOString(),
            });
            whoData.updatedAt = new Date().toISOString();
            fs.writeFileSync(whoFile, JSON.stringify(whoData, null, 2));
          }
        }
      }

      existing.profiles = Array.from(existingMap.values());
      existing.updatedAt = new Date().toISOString();
      fs.writeFileSync(discoveredFile, JSON.stringify(existing, null, 2));
      console.log(`Discovered profiles batch: +${newProfiles} new, ${profiles.length} updated, ${existing.profiles.length} total`);
    }

    res.json({
      success: true,
      location: locationName || `${latitude},${longitude}`,
      steps,
      summary: {
        profilesFound: profiles.length,
        newProfiles,
        likedMeFound,
      },
    });
  } catch (e) {
    console.error('[EmulateOpen] Error:', e.message);
    res.status(500).json({ error: e.message, steps });
  }
});

// ============================================================
// OKCupid API Proxy — server-side fetch to bypass Cloudflare
// ============================================================

const OKC_TOKEN_FILE = path.join(DATA_DIR, 'okc-token.json');
const OKC_CREDS_FILE = path.join(DATA_DIR, 'okc-credentials.json');

const OKC_HEADERS = {
  'Content-Type': 'application/json',
  'User-Agent': 'OkCupid/111.1.0 iOS/26.2.1',
  'x-okcupid-locale': 'en',
  'x-okcupid-platform': 'ios',
  'x-okcupid-auth-v': '1',
  'x-okcupid-version': '111.1.0',
  'x-okcupid-device-id': '40022B89-7089-4969-85CC-94843116EEE9',
  'apollographql-client-name': 'com.okcupid.app-apollo-ios',
  'apollographql-client-version': '111.1.0-1625',
  'Accept': 'application/json',
};

// OKC token is valid for 45 min. We login ONCE and refresh proactively before expiry.
// NEVER login reactively in response to a failed request.
let _okcLoginInProgress = null;

async function okcLogin() {
  // Deduplicate: if a login is already in progress, wait for it
  if (_okcLoginInProgress) return _okcLoginInProgress;
  _okcLoginInProgress = _doOkcLogin();
  const result = await _okcLoginInProgress;
  _okcLoginInProgress = null;
  return result;
}

async function _doOkcLogin() {
  if (!fs.existsSync(OKC_CREDS_FILE)) return false;
  try {
    const creds = JSON.parse(fs.readFileSync(OKC_CREDS_FILE, 'utf8'));
    if (!creds.email || !creds.password) return false;

    // Step 1: Anonymous token
    const anonResp = await fetch('https://e2p-okapi.api.okcupid.com/graphql/AnonAuthToken', {
      method: 'POST',
      headers: OKC_HEADERS,
      body: JSON.stringify({
        operationName: 'AnonAuthToken',
        query: 'mutation AnonAuthToken($input: AuthAnonymousInput!) { authAnonymous(input: $input) { token } }',
        variables: { input: { deviceId: '40022B89-7089-4969-85CC-94843116EEE9', siteCode: 36 } },
        extensions: { clientLibrary: { name: 'apollo-ios', version: '1.23.0' } },
      }),
    });
    const anonData = await anonResp.json();
    let anonToken = anonData?.data?.authAnonymous?.token;
    if (!anonToken) {
      console.error('[OKC] Anon auth failed:', JSON.stringify(anonData).substring(0, 200));
      return false;
    }
    anonToken = anonToken.replace(/^Bearer\s+/i, '');

    // Step 2: Login with anon token
    const loginResp = await fetch('https://e2p-okapi.api.okcupid.com/graphql/AuthLogin', {
      method: 'POST',
      headers: { ...OKC_HEADERS, 'Authorization': 'Bearer ' + anonToken },
      body: JSON.stringify({
        operationName: 'AuthLogin',
        query: 'mutation AuthLogin($input: AuthEmailLoginInput!) { authEmailLogin(input: $input) { token encryptedUserId status } }',
        variables: { input: { email: creds.email, password: creds.password } },
        extensions: { clientLibrary: { name: 'apollo-ios', version: '1.23.0' } },
      }),
    });
    const loginData = await loginResp.json();
    const newToken = loginData?.data?.authEmailLogin?.token;
    if (newToken) {
      const clean = newToken.replace(/^Bearer\s+/i, '');
      fs.writeFileSync(OKC_TOKEN_FILE, JSON.stringify({ token: clean, updatedAt: new Date().toISOString() }, null, 2));
      console.log('[OKC] Token refreshed, valid for 45 min');
      return true;
    }
    console.error('[OKC] Login failed (status ' + loginData?.data?.authEmailLogin?.status + ')');
    return false;
  } catch (e) {
    console.error('[OKC] Login error:', e.message);
    return false;
  }
}

function getOkcToken() {
  if (fs.existsSync(OKC_TOKEN_FILE)) {
    try {
      const token = JSON.parse(fs.readFileSync(OKC_TOKEN_FILE, 'utf8')).token;
      return token ? token.replace(/^Bearer\s+/i, '') : null;
    } catch (e) {}
  }
  return null;
}

function getOkcTokenExpiry() {
  const token = getOkcToken();
  if (!token) return 0;
  try {
    const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
    return payload.exp * 1000;
  } catch (e) { return 0; }
}

// Proactive refresh: check every 5 min, login if < 10 min remaining
setInterval(async () => {
  const expiry = getOkcTokenExpiry();
  if (!expiry) return;
  const remaining = expiry - Date.now();
  if (remaining < 10 * 60 * 1000 && remaining > -5 * 60 * 1000) {
    // Between 10 min before expiry and 5 min after — refresh
    console.log('[OKC] Proactive refresh: token expires in', Math.round(remaining / 60000), 'min');
    await okcLogin();
  }
}, 5 * 60 * 1000);

// Login on startup if token is expired or missing
(async () => {
  const expiry = getOkcTokenExpiry();
  if (!expiry || Date.now() > expiry) {
    console.log('[OKC] Token expired or missing on startup, logging in...');
    await okcLogin();
  } else {
    console.log('[OKC] Token valid, expires in', Math.round((expiry - Date.now()) / 60000), 'min');
  }
})();

// POST /api/okcupid/graphql/:operation — Proxy GraphQL requests to OKCupid
// NO reactive login — just forward the request with current token
app.post('/api/okcupid/graphql/:operation', async (req, res) => {
  const token = getOkcToken();
  if (!token) {
    return res.status(401).json({ error: 'No OKCupid token. Login not configured.' });
  }

  const operation = req.params.operation;
  // Log vote operations for debugging
  if (operation === 'UserVote') {
    console.log('[OKC] UserVote request:', JSON.stringify(req.body?.variables || {}).substring(0, 500));
  }
  try {
    const response = await fetch(`https://e2p-okapi.api.okcupid.com/graphql/${operation}`, {
      method: 'POST',
      headers: {
        ...OKC_HEADERS,
        'Authorization': `Bearer ${token}`,
        'x-match-useragent': 'OkCupid/111.1.0 iOS/26.2.1',
        'X-APOLLO-OPERATION-TYPE': req.body?.query?.trim().startsWith('mutation') ? 'mutation' : 'query',
        'X-APOLLO-OPERATION-NAME': operation,
        'Accept-Language': 'en-US,en;q=0.9',
        'Connection': 'keep-alive',
      },
      body: JSON.stringify({
        ...req.body,
        extensions: { clientLibrary: { name: 'apollo-ios', version: '1.23.0' }, ...req.body.extensions },
      }),
    });

    const data = await response.text();
    if (operation === 'UserVote') {
      console.log('[OKC] UserVote response:', data.substring(0, 300));
    }
    res.status(response.status).type('application/json').send(data);
  } catch (e) {
    console.error('[OKC Proxy] Error:', e.message);
    res.status(500).json({ error: e.message });
  }
});

// GET /api/okcupid/token — Get stored OKC token
app.get('/api/okcupid/token', (req, res) => {
  const token = getOkcToken();
  res.json({ token: token ? token.substring(0, 20) + '...' : null, hasToken: !!token });
});

// PUT /api/okcupid/token — Save OKC token
app.put('/api/okcupid/token', (req, res) => {
  const { token } = req.body;
  if (!token) return res.status(400).json({ error: 'token required' });
  fs.writeFileSync(OKC_TOKEN_FILE, JSON.stringify({ token, updatedAt: new Date().toISOString() }, null, 2));
  res.json({ success: true });
});

// ============================================================
// FeeldAPI Client — makes direct GraphQL calls to Feeld backend
// ============================================================

const FIREBASE_API_KEY = 'AIzaSyD9o9mzulN50-hqOwF6ww9pxUNUxwVOCXA';
const FIREBASE_REFRESH_URL = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
const GRAPHQL_ENDPOINT = 'https://core.api.fldcore.com/graphql';
const APP_VERSION = '9.4.3';
const OS_VERSION = '26.2.1';
// iOS app identifiers (mirror of src/config/constants.ts)
const IOS_BUNDLE_ID = 'com.3nder.threender';
const IOS_DEVICE_MODEL = 'iPhone14_4';
const FIREBASE_SDK_VERSION = '11.5.0';
const FIREBASE_GMP_ID = '1:594152761603:ios:f52cf15efff827861b2136';
const FIREBASE_APPCHECK_FALLBACK = 'eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==';
const AUTH_TOKENS_FILE = path.join(DATA_DIR, 'auth-tokens.json');
const ROTATION_STATE_FILE = path.join(DATA_DIR, 'locationRotation.json');
const SAVED_LOCATIONS_FILE = path.join(DATA_DIR, 'savedLocations.json');

// Countries where Feeld is NOT available — mirror of web/src/config/bannedCountries.ts.
// Keep in sync if that file changes. Used to filter incoming saved-location syncs.
const BANNED_COUNTRY_CODES = new Set([
  'AL','DZ','AO','AM','AZ','BH','BD','BJ','BW','BN','BF','CM','TD','CN','CG','CU',
  'EG','GA','GM','HK','IR','IQ','JO','KZ','KE','LA','LR','MO','MG','MW','ML','MR',
  'MU','MM','MA','MZ','NA','NE','OM','PK','PG','PH','QA','RU','SA','SN','SL','SD',
  'TW','TJ','TZ','TH','TG','TN','TM','UG','UZ','YE','ZM','ZW',
]);
const BANNED_COUNTRY_NAMES = new Set([
  'albania','algeria','angola','armenia','azerbaijan','bahrain','bangladesh','benin','botswana',
  'brunei darussalam','brunei','burkina faso','cameroon','chad','china',"people's republic of china",
  'republic of the congo','congo','congo, republic of','congo-brazzaville','cuba','egypt','gabon',
  'gambia','the gambia','hong kong','iran','islamic republic of iran','iraq','jordan','kazakhstan',
  'kenya','laos',"lao people's democratic republic",'lao pdr','liberia','macau','macao','madagascar',
  'malawi','mali','mauritania','mauritius','myanmar','burma','mayanmar','morocco','mozambique',
  'namibia','niger','oman','pakistan','papua new guinea','philippines','qatar','russia',
  'russian federation','saudi arabia','senegal','sierra leone','sudan','taiwan',
  'taiwan, province of china','republic of china','tajikistan','tanzania',
  'united republic of tanzania','thailand','togo','tunisia','turkmenistan','uganda','uzbekistan',
  'yemen','zambia','zimbabwe',
]);

function isBannedLocation(loc) {
  if (!loc) return false;
  if (loc.countryCode && BANNED_COUNTRY_CODES.has(String(loc.countryCode).trim().toUpperCase())) return true;
  if (loc.country && BANNED_COUNTRY_NAMES.has(String(loc.country).trim().toLowerCase())) return true;
  return false;
}

class FeeldAPIClient {
  constructor() {
    this.accessToken = null;
    this.expiresAt = 0;
    this.profileId = null;
    this.refreshToken = null;
    this.analyticsId = null;
  }

  loadCredentials() {
    if (fs.existsSync(AUTH_TOKENS_FILE)) {
      try {
        const data = JSON.parse(fs.readFileSync(AUTH_TOKENS_FILE, 'utf8'));
        this.profileId = data.profileId || null;
        this.refreshToken = data.refreshToken || null;
        this.analyticsId = data.analyticsId || null;
        return !!this.profileId && !!this.refreshToken;
      } catch (e) {
        console.error('[FeeldAPI] Failed to load credentials:', e.message);
        return false;
      }
    }
    return false;
  }

  saveCredentials(profileId, refreshToken, analyticsId) {
    this.profileId = profileId;
    this.refreshToken = refreshToken;
    this.analyticsId = analyticsId || this.analyticsId;
    fs.writeFileSync(AUTH_TOKENS_FILE, JSON.stringify({
      profileId,
      refreshToken,
      analyticsId: this.analyticsId,
      updatedAt: new Date().toISOString(),
    }, null, 2));
    console.log('[FeeldAPI] Credentials saved');
  }

  async refreshAccessToken() {
    if (!this.refreshToken) {
      throw new Error('No refresh token available — seed from browser first');
    }

    // Mirror the headers the real iOS Feeld app sends on token refresh —
    // anything missing here flags the request as "not the iOS app."
    const response = await fetch(FIREBASE_REFRESH_URL, {
      method: 'POST',
      headers: {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'en',
        'Connection': 'keep-alive',
        'Content-Type': 'application/json',
        'Host': 'securetoken.googleapis.com',
        'User-Agent': `FirebaseAuth.iOS/${FIREBASE_SDK_VERSION} ${IOS_BUNDLE_ID}/${APP_VERSION} iPhone/${OS_VERSION} hw/${IOS_DEVICE_MODEL}`,
        'X-Client-Version': `iOS/FirebaseSDK/${FIREBASE_SDK_VERSION}/FirebaseCore-iOS`,
        'X-Firebase-AppCheck': FIREBASE_APPCHECK_FALLBACK,
        'X-Firebase-GMPID': FIREBASE_GMP_ID,
        'X-Ios-Bundle-Identifier': IOS_BUNDLE_ID,
      },
      // iOS app uses camelCase keys — Firebase accepts both, but the iOS shape is what we want.
      body: JSON.stringify({
        grantType: 'refresh_token',
        refreshToken: this.refreshToken,
      }),
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Firebase token refresh failed: ${response.status} ${text}`);
    }

    const data = await response.json();
    // Firebase returns camelCase keys when the request body is camelCase, snake_case when
    // it's snake_case — accept either so a future body shape change doesn't silently break.
    const accessToken = data.accessToken ?? data.access_token;
    const expiresIn = data.expiresIn ?? data.expires_in;
    const newRefreshToken = data.refreshToken ?? data.refresh_token;

    this.accessToken = accessToken;
    this.expiresAt = Date.now() + parseInt(expiresIn) * 1000;

    // Update stored refresh token (Firebase rotates them)
    if (newRefreshToken && newRefreshToken !== this.refreshToken) {
      this.refreshToken = newRefreshToken;
      this.saveCredentials(this.profileId, this.refreshToken, this.analyticsId);
    }

    console.log('[FeeldAPI] Token refreshed, expires in', expiresIn, 'seconds');
    return this.accessToken;
  }

  async getToken() {
    if (!this.accessToken || Date.now() >= this.expiresAt - 60000) {
      await this.refreshAccessToken();
    }
    return this.accessToken;
  }

  async graphql(operationName, query, variables = {}, _retried = false) {
    const token = await this.getToken();
    const transactionId = crypto.randomUUID();

    const response = await fetch(GRAPHQL_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
        'x-profile-id': this.profileId,
        'x-app-version': APP_VERSION,
        'x-device-os': 'ios',
        'x-os-version': OS_VERSION,
        'x-transaction-id': transactionId,
        'x-event-analytics-id': this.analyticsId || crypto.randomUUID(),
        'User-Agent': 'feeld-mobile',
        'Accept': '*/*',
      },
      body: JSON.stringify({ operationName, query, variables }),
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`GraphQL request failed: ${response.status} ${text}`);
    }

    const result = await response.json();

    // Auto-retry on UNAUTHENTICATED: force-refresh token and replay
    if (result.errors && !_retried) {
      const isAuthError = result.errors.some(e => e.extensions?.code === 'UNAUTHENTICATED');
      if (isAuthError) {
        console.log(`[FeeldAPI] Auth error on ${operationName}, force-refreshing token...`);
        this.accessToken = null;
        this.expiresAt = 0;
        // Re-read credentials from disk in case another process updated them
        this.loadCredentials();
        await this.refreshAccessToken();
        return this.graphql(operationName, query, variables, true);
      }
    }

    if (result.errors) {
      throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
    }
    return result.data;
  }

  async updateLocation(lat, lng) {
    return this.graphql('DeviceLocationUpdate', `
      mutation DeviceLocationUpdate($input: DeviceLocationInput!) {
        deviceLocationUpdate(input: $input) {
          id
          location {
            device { latitude longitude geocode { city country __typename } __typename }
            __typename
          }
          __typename
        }
      }
    `, { input: { latitude: lat, longitude: lng } });
  }

  async getSearchSettings() {
    return this.graphql('DiscoverSearchSettingsQuery', `
      query DiscoverSearchSettingsQuery($profileId: String!) {
        profile(id: $profileId) {
          id ageRange distanceMax lookingFor desiringFor recentlyOnline __typename
        }
      }
    `, { profileId: this.profileId });
  }

  async discoverProfiles(filters = {}) {
    return this.graphql('DiscoverProfiles', `
      query DiscoverProfiles($input: ProfileDiscoveryInput!) {
        discovery(input: $input) {
          nodes {
            id age imaginaryName gender sexuality isIncognito isMajestic
            verificationStatus connectionGoals desires bio interests
            distance { km mi __typename }
            photos {
              id publicId pictureIsSafe pictureIsPrivate pictureUrl
              pictureUrls { small medium large __typename }
              pictureType __typename
            }
            interactionStatus { message mine theirs __typename }
            __typename
          }
          hasNextBatch __typename
        }
      }
    `, { input: { filters } });
  }
}

const feeldAPI = new FeeldAPIClient();

// ============================================================
// Location Rotation Cron
// ============================================================

function readRotationState() {
  if (fs.existsSync(ROTATION_STATE_FILE)) {
    try {
      return JSON.parse(fs.readFileSync(ROTATION_STATE_FILE, 'utf8'));
    } catch (e) {
      console.error('[Rotation] Failed to read state:', e.message);
    }
  }
  return {
    groups: [],
    activeGroupId: null,
    intervalHours: 4,
    enabled: false,
    currentLocationIdx: 0,
    lastRotation: null,
    lastResult: null,
    history: [],
  };
}

function writeRotationState(state) {
  fs.writeFileSync(ROTATION_STATE_FILE, JSON.stringify(state, null, 2));
}

function readSavedLocations() {
  if (fs.existsSync(SAVED_LOCATIONS_FILE)) {
    try {
      const raw = JSON.parse(fs.readFileSync(SAVED_LOCATIONS_FILE, 'utf8'));
      const arr = Array.isArray(raw) ? raw : [];
      // Strip any banned-country entries that might be left over from before
      // the country filter was added. Rotation and selection both go through here.
      return arr.filter((loc) => !isBannedLocation(loc));
    } catch (e) {
      console.error('[Rotation] Failed to read saved locations:', e.message);
    }
  }
  return [];
}

function writeSavedLocations(locations) {
  fs.writeFileSync(SAVED_LOCATIONS_FILE, JSON.stringify(locations, null, 2));
}

// Helper to sanitize profile fields that may be {__typename: "..."} objects
function safeStr(v) {
  return typeof v === 'string' ? v : '';
}

function sanitizeProfile(p) {
  return {
    id: p.id,
    imaginaryName: safeStr(p.imaginaryName),
    age: p.age,
    gender: safeStr(p.gender),
    sexuality: safeStr(p.sexuality),
    bio: safeStr(p.bio),
    desires: p.desires,
    connectionGoals: p.connectionGoals,
    interests: p.interests,
    isMajestic: p.isMajestic,
    isIncognito: p.isIncognito,
    verificationStatus: safeStr(p.verificationStatus),
    distance: p.distance,
    photos: p.photos,
    interactionStatus: p.interactionStatus,
  };
}

async function performRotation() {
  const state = readRotationState();
  if (!state.enabled || !state.activeGroupId) return;

  const now = Date.now();
  if (state.lastRotation && (now - new Date(state.lastRotation).getTime()) < state.intervalHours * 3600000) {
    return; // Not time yet
  }

  const group = state.groups.find(g => g.id === state.activeGroupId);
  if (!group || group.locationIds.length === 0) {
    console.log('[Rotation] Active group not found or empty');
    return;
  }

  // Load credentials
  if (!feeldAPI.loadCredentials()) {
    console.log('[Rotation] No credentials — seed from browser first');
    state.lastResult = { status: 'error', error: 'No credentials', timestamp: new Date().toISOString() };
    writeRotationState(state);
    return;
  }

  // Resolve next location
  const savedLocations = readSavedLocations();
  const nextIdx = (state.currentLocationIdx + 1) % group.locationIds.length;
  const locationId = group.locationIds[nextIdx];
  const loc = savedLocations.find(l => l.id === locationId);

  if (!loc) {
    console.log('[Rotation] Location ID not found:', locationId);
    state.lastResult = { status: 'error', error: `Location ${locationId} not found`, timestamp: new Date().toISOString() };
    writeRotationState(state);
    return;
  }

  console.log(`[Rotation] Rotating to: ${loc.name} (${loc.latitude}, ${loc.longitude})`);

  try {
    // 1. Update location
    await feeldAPI.updateLocation(loc.latitude, loc.longitude);
    console.log('[Rotation] Location updated');

    // 2. Fetch search settings
    let filters = {
      ageRange: [22, 59],
      maxDistance: 100,
      lookingFor: ['WOMAN', 'MAN_WOMAN_COUPLE', 'WOMAN_WOMAN_COUPLE'],
      recentlyOnline: false,
      desiringFor: [],
    };

    try {
      const settings = await feeldAPI.getSearchSettings();
      const profile = settings?.profile;
      if (profile) {
        filters = {
          ageRange: profile.ageRange || filters.ageRange,
          maxDistance: profile.distanceMax || filters.maxDistance,
          lookingFor: profile.lookingFor || filters.lookingFor,
          recentlyOnline: profile.recentlyOnline || false,
          desiringFor: profile.desiringFor || [],
        };
      }
    } catch (e) {
      console.log('[Rotation] Failed to fetch search settings, using defaults:', e.message);
    }

    // 3. Discover profiles (one batch)
    const discoveryResult = await feeldAPI.discoverProfiles(filters);
    const profiles = discoveryResult?.discovery?.nodes || [];
    console.log(`[Rotation] Discovered ${profiles.length} profiles at ${loc.name}`);

    // 4. Save who-liked-you profiles
    let likedMeCount = 0;
    const whoLikedYouPath = path.join(DATA_DIR, 'whoLikedYou.json');
    let whoLikedYouData = { profiles: [], updatedAt: null };
    if (fs.existsSync(whoLikedYouPath)) {
      try { whoLikedYouData = JSON.parse(fs.readFileSync(whoLikedYouPath, 'utf8')); } catch (e) {}
    }

    for (const p of profiles) {
      if (p.interactionStatus?.theirs === 'LIKED') {
        const sanitized = sanitizeProfile(p);
        const existingIdx = whoLikedYouData.profiles.findIndex(ep => ep.id === p.id);
        if (existingIdx >= 0) {
          const orig = whoLikedYouData.profiles[existingIdx].discoveredAt;
          whoLikedYouData.profiles[existingIdx] = { ...sanitized, discoveredAt: orig, updatedAt: new Date().toISOString() };
        } else {
          whoLikedYouData.profiles.unshift({ ...sanitized, discoveredAt: new Date().toISOString() });
        }
        likedMeCount++;
      }
    }

    if (likedMeCount > 0) {
      whoLikedYouData.updatedAt = new Date().toISOString();
      fs.writeFileSync(whoLikedYouPath, JSON.stringify(whoLikedYouData, null, 2));
      console.log(`[Rotation] Saved ${likedMeCount} who-liked-you profiles`);
    }

    // 5. Batch-save all discovered profiles
    if (profiles.length > 0) {
      const discoveredPath = path.join(DATA_DIR, 'discoveredProfiles.json');
      let discoveredData = { profiles: [], updatedAt: null };
      if (fs.existsSync(discoveredPath)) {
        try { discoveredData = JSON.parse(fs.readFileSync(discoveredPath, 'utf8')); } catch (e) {}
      }

      const existingMap = new Map(discoveredData.profiles.map(p => [p.id, p]));
      for (const p of profiles) {
        const sanitized = sanitizeProfile(p);
        sanitized.discoveredLocation = loc.name;
        const existing = existingMap.get(p.id);
        if (existing) {
          existingMap.set(p.id, { ...sanitized, discoveredAt: existing.discoveredAt, updatedAt: new Date().toISOString() });
        } else {
          existingMap.set(p.id, { ...sanitized, discoveredAt: new Date().toISOString() });
        }
      }

      let allProfiles = Array.from(existingMap.values());
      allProfiles.sort((a, b) => new Date(b.discoveredAt || 0).getTime() - new Date(a.discoveredAt || 0).getTime());
      if (allProfiles.length > 2000) allProfiles = allProfiles.slice(0, 2000);

      discoveredData.profiles = allProfiles;
      discoveredData.updatedAt = new Date().toISOString();
      fs.writeFileSync(discoveredPath, JSON.stringify(discoveredData, null, 2));
    }

    // 6. Update state
    const result = {
      status: 'success',
      profilesFound: profiles.length,
      likedMeFound: likedMeCount,
      location: loc.name,
      timestamp: new Date().toISOString(),
    };
    state.currentLocationIdx = nextIdx;
    state.lastRotation = new Date().toISOString();
    state.lastResult = result;
    state.history = [result, ...(state.history || [])].slice(0, 10);
    writeRotationState(state);

    console.log(`[Rotation] Complete: ${profiles.length} profiles, ${likedMeCount} liked me at ${loc.name}`);
  } catch (e) {
    console.error('[Rotation] Error:', e.message);
    const result = { status: 'error', error: e.message, location: loc.name, timestamp: new Date().toISOString() };
    state.lastResult = result;
    state.history = [result, ...(state.history || [])].slice(0, 10);
    writeRotationState(state);
  }
}

// Check every 5 minutes if it's time to rotate
let rotationInterval = null;

function startRotationCron() {
  if (rotationInterval) clearInterval(rotationInterval);
  rotationInterval = setInterval(() => {
    performRotation().catch(e => console.error('[Rotation] Cron error:', e.message));
  }, 5 * 60 * 1000); // 5 minutes
  console.log('[Rotation] Cron started (checks every 5 min)');
}

// ============================================================
// Location Rotation API Endpoints
// ============================================================

// GET /api/location-rotation — Return current rotation state
app.get('/api/location-rotation', (req, res) => {
  res.json(readRotationState());
});

// PUT /api/location-rotation — Update rotation config
app.put('/api/location-rotation', (req, res) => {
  const { groups, activeGroupId, intervalHours, enabled } = req.body;
  const state = readRotationState();

  if (groups !== undefined) state.groups = groups;
  if (activeGroupId !== undefined) state.activeGroupId = activeGroupId;
  if (intervalHours !== undefined) state.intervalHours = intervalHours;
  if (enabled !== undefined) state.enabled = enabled;

  writeRotationState(state);
  res.json({ success: true, state });
});

// POST /api/location-rotation/rotate-now — Force immediate rotation
app.post('/api/location-rotation/rotate-now', async (req, res) => {
  const state = readRotationState();
  if (!state.enabled || !state.activeGroupId) {
    return res.status(400).json({ success: false, error: 'Rotation not enabled or no active group' });
  }

  // Clear lastRotation to force immediate execution
  state.lastRotation = null;
  writeRotationState(state);

  try {
    await performRotation();
    res.json({ success: true, state: readRotationState() });
  } catch (e) {
    res.status(500).json({ success: false, error: e.message });
  }
});

// POST /api/location-rotation/seed-token — Browser sends its current refresh token
app.post('/api/location-rotation/seed-token', (req, res) => {
  const { refreshToken, profileId, analyticsId } = req.body;
  if (!refreshToken || !profileId) {
    return res.status(400).json({ success: false, error: 'refreshToken and profileId required' });
  }
  feeldAPI.saveCredentials(profileId, refreshToken, analyticsId);
  res.json({ success: true });
});

// GET /api/location-rotation/status — Quick status for UI
app.get('/api/location-rotation/status', (req, res) => {
  const state = readRotationState();
  const savedLocations = readSavedLocations();

  let currentLocation = null;
  let nextRotation = null;

  if (state.enabled && state.activeGroupId) {
    const group = state.groups.find(g => g.id === state.activeGroupId);
    if (group && group.locationIds.length > 0) {
      const locId = group.locationIds[state.currentLocationIdx % group.locationIds.length];
      currentLocation = savedLocations.find(l => l.id === locId) || null;
    }

    if (state.lastRotation) {
      const nextTime = new Date(state.lastRotation).getTime() + state.intervalHours * 3600000;
      nextRotation = new Date(nextTime).toISOString();
    }
  }

  res.json({
    enabled: state.enabled,
    activeGroup: state.activeGroupId ? state.groups.find(g => g.id === state.activeGroupId) : null,
    currentLocation,
    nextRotation,
    lastResult: state.lastResult,
    intervalHours: state.intervalHours,
  });
});

// GET /api/saved-locations — Return saved locations list
app.get('/api/saved-locations', (req, res) => {
  res.json(readSavedLocations());
});

// PUT /api/saved-locations — Browser syncs its saved locations here.
// Strips any locations in Feeld-restricted countries (defense in depth against
// stale clients or directly-crafted requests).
app.put('/api/saved-locations', (req, res) => {
  const { locations } = req.body;
  if (!Array.isArray(locations)) {
    return res.status(400).json({ success: false, error: 'locations array required' });
  }
  const filtered = [];
  const rejected = [];
  for (const loc of locations) {
    if (isBannedLocation(loc)) {
      rejected.push({ id: loc.id, name: loc.name, country: loc.country, countryCode: loc.countryCode });
    } else {
      filtered.push(loc);
    }
  }
  if (rejected.length) {
    console.warn(`[saved-locations] dropped ${rejected.length} banned-country location(s):`, rejected);
  }
  writeSavedLocations(filtered);
  res.json({ success: true, count: filtered.length, rejected: rejected.length });
});

// ============================================================
// Start server + rotation cron
// ============================================================

app.listen(PORT, () => {
  console.log(`Data server running on http://localhost:${PORT}`);
  console.log(`Data stored in: ${DATA_DIR}`);

  // Load credentials and start rotation cron
  feeldAPI.loadCredentials();
  startRotationCron();
});
