const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const DataService = require('../services/data');
const LogicService = require('../services/logic');
const AIAssistantService = require('../services/ai-assistant');
const io = require('../io');

// Helper to get IP
const getIp = (req) => (req.headers['x-forwarded-for'] || '').toString().split(',')[0].trim() || req.ip || req.connection?.remoteAddress || '';

const normalizeTicketId = (v) => {
  const s0 = String(v || '').replace(/\s+/g, '');
  if (!s0) return '';
  const m = s0.match(/^([A-Za-z]{2})-?([0-9]+)$/);
  if (m) {
    const prefix = m[1].toUpperCase();
    let num = m[2];
    if (num.length < 8) num = num.padStart(8, '0');
    else if (num.length > 8) num = num.slice(-8);
    return `${prefix}-${num}`;
  }
  return s0.toLowerCase();
};

const normalizeIcCardId = (v) => {
  const s0 = String(v || '').replace(/\s+/g, '').toUpperCase();
  if (!s0) return '';
  const m = s0.match(/^IC-?([0-9]+)$/);
  if (m) return `IC-${m[1].padStart(6, '0').slice(-6)}`;
  return s0;
};

const buildIcCardId = () => {
  const idx = DataService.getIcCardIndex() || {};
  let id = '';
  do {
    id = `IC-${String(crypto.randomInt(0, 1000000)).padStart(6, '0')}`;
  } while (idx[id]);
  return id;
};

const mapIcCardStatus = (status) => {
  const s = String(status || '').trim().toLowerCase();
  if (s === 'pending_pickup') return '待领卡';
  if (s === 'active') return '正常';
  if (s === 'disabled') return '停用';
  if (s === 'lost') return '挂失';
  if (s === 'refunded') return '已退卡';
  return status || '未知';
};

const mapIcCardType = (type) => {
  const t = String(type || '').trim().toLowerCase();
  if (t === 'stored_value') return '储值卡';
  if (t === 'monthly') return '月票卡';
  if (t === 'tourist') return '纪念卡';
  return type || '未分类';
};

const IC_CARD_HOLDER_NAME_RE = /^[A-Za-z][A-Za-z .,'()&@/_\-+]*$/;

const displayIcCardId = (card) => {
  const status = String(card?.status || '').trim().toLowerCase();
  const source = String(card?.source || '').trim().toLowerCase();
  const rawId = String(card?.card_id || '').trim();
  if (status === 'pending_pickup' && source === 'online') return '待出卡';
  return rawId || '---';
};

const presentIcCard = (card) => card ? ({
  ...card,
  display_card_id: displayIcCardId(card)
}) : card;

const appendReqLog = (req, { category, type, detail, source, level } = {}) => {
  const entry = {
    ts: new Date().toISOString(),
    ip: getIp(req),
    ua: String(req.headers['user-agent'] || ''),
    method: req.method,
    path: req.originalUrl || req.path || '',
    category: String(category || '').trim() || 'admin',
    source: source == null ? undefined : String(source || '').trim(),
    level: level == null ? undefined : String(level || '').trim(),
    type: String(type || '').trim() || 'event',
    detail: (detail === undefined) ? null : detail
  };
  DataService.appendLog(entry);
};

const normalizeOrderCode = (v) => String(v || '').trim().toUpperCase();

const toMoney = (v) => {
  const n = Number(v);
  if (!Number.isFinite(n)) return 0;
  return Math.round(n * 100) / 100;
};

const toFiniteNumberOrUndef = (v) => {
  if (v == null) return undefined;
  const n = Number(v);
  return Number.isFinite(n) ? n : undefined;
};

const isTruthy = (v) => v === true || v === 1 || v === '1' || String(v || '').toLowerCase() === 'true';

const buildStationResolver = () => {
  const stations = DataService.getStations?.() || [];
  const codeByKey = new Map();
  const nameToCode = new Map();
  const normKey = (v) => String(v || '').trim().toUpperCase().replace(/\s+/g, '');
  const looksLikeStationCode = (v) => /^[A-Z0-9]+(?:-[A-Z0-9]+)+$/i.test(String(v || '').trim());
  for (const s of stations) {
    if (!s) continue;
    const code = String(s.code || '').trim();
    if (!code) continue;
    codeByKey.set(normKey(code), code);
    const cn = String(s.name || s.cn_name || s.station_name || '').trim();
    if (cn) nameToCode.set(normKey(cn), code);
    const en = String(s.en_name || s.en || s.enName || '').trim();
    if (en) nameToCode.set(normKey(en), code);
  }
  return (v) => {
    const raw = String(v || '').trim();
    if (!raw) return '';
    const key = normKey(raw);
    if (codeByKey.has(key)) return codeByKey.get(key);
    if (looksLikeStationCode(raw)) return raw;
    return nameToCode.get(key) || raw;
  };
};

const findIcCardByOrderCode = (code) => {
  const normalized = normalizeOrderCode(code);
  if (!normalized) return null;
  return (DataService.getIcCards() || []).find((item) => normalizeOrderCode(item?.order_code) === normalized) || null;
};

const resolveCurrentStationCode = (body, resolveStation) => {
  const codes = Array.isArray(body?.station_codes)
    ? body.station_codes.map((item) => resolveStation(item)).filter(Boolean)
    : [];
  const direct = resolveStation(body?.station_code);
  const fallback = resolveStation(DataService.getConfig?.().current_station || '');
  return codes[0] || direct || fallback || '';
};

// Config
router.get('/config', (req, res) => {
  res.json({
    api_base: DataService.getConfig().api_base,
    current_station: DataService.getConfig().current_station,
    stations: DataService.getStations(),
    lines: DataService.getLines(),
    fares: DataService.getFares(),
    transfers: DataService.getConfig().transfers || [],
    promotion: DataService.getConfig().promotion || { name: '', discount: 1 }
  });
});

router.post('/ai-assistant', async (req, res) => {
  try {
    const message = String(req.body?.message || '').trim();
    if (!message) return res.status(400).json({ ok: false, error: 'message required' });

    const result = await AIAssistantService.askAssistant({
      message,
      history: req.body?.history,
      page: req.body?.page,
      context: req.body?.context,
      config: DataService.getConfig(),
      stations: DataService.getStations(),
      lines: DataService.getLines(),
      fares: DataService.getFares()
    });

    appendReqLog(req, {
      category: 'public',
      type: 'ai_assistant_ask',
      detail: {
        page: String(req.body?.page || '').trim(),
        question: message.slice(0, 280),
        model: result.model
      }
    });
    res.json({ ok: true, reply: result.reply, model: result.model });
  } catch (e) {
    const code = e?.code || '';
    const status = code === 'INVALID_MESSAGE' ? 400 : (code === 'AI_NOT_CONFIGURED' ? 503 : 500);
    appendReqLog(req, {
      category: 'system',
      type: 'ai_assistant_failed',
      level: status >= 500 ? 'error' : 'warn',
      detail: {
        page: String(req.body?.page || '').trim(),
        error: e?.message || String(e)
      }
    });
    res.status(status).json({
      ok: false,
      error: code === 'AI_NOT_CONFIGURED'
        ? 'AI 助手尚未配置 DeepSeek API Key'
        : (e?.message || 'ai assistant request failed')
    });
  }
});

router.put('/config', async (req, res) => {
  try {
    const incoming = req.body || {};
    const cfg = DataService.getConfig();
    if (incoming.api_base && typeof incoming.api_base === 'string') cfg.api_base = incoming.api_base;
    if (Array.isArray(incoming.transfers)) cfg.transfers = incoming.transfers;
    if (incoming.current_station && typeof incoming.current_station === 'object') cfg.current_station = incoming.current_station;
    if (incoming.promotion) {
      const p = incoming.promotion || {};
      const d = Number(p.discount);
      if (!(d >= 0 && d <= 1)) return res.status(400).json({ ok: false, error: 'promotion.discount must be between 0 and 1' });
      cfg.promotion = { name: String(p.name || ''), discount: d };
    }
    await DataService.saveConfig(cfg);
    appendReqLog(req, { category: 'admin', type: 'update_config_generic', detail: incoming });
    io.emit('config:updated', cfg);
    res.json({ ok: true, config: cfg });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'update_config_generic_failed', level: 'error', detail: { error: e?.message || String(e) } });
    res.status(500).json({ ok: false, error: 'failed to save config' });
  }
});

// Stations
router.get('/stations', (req, res) => res.json(DataService.getStations()));
router.post('/stations', async (req, res) => {
  try {
    const all = DataService.getStations();
    all.push(req.body);
    await DataService.saveStations(all);
    appendReqLog(req, { category: 'admin', type: 'update_station', detail: req.body });
    io.emit('stations:updated', all);
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'update_station_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to save stations' });
  }
});
router.put('/stations/:code', async (req, res) => {
  try {
    const all = DataService.getStations();
    const idx = all.findIndex(s => s.code === req.params.code);
    if (idx < 0) return res.status(404).json({ ok: false, error: 'station not found' });
    const incoming = req.body || {};
    const current = all[idx] || {};
    const oldCode = String(current.code || '').trim();
    const requestedCode = (incoming.code == null) ? oldCode : String(incoming.code || '').trim();
    const renaming = requestedCode && oldCode && requestedCode !== oldCode;

    if (!requestedCode) return res.status(400).json({ ok: false, error: 'station code required' });
    if (renaming && all.some(s => String(s.code || '').trim() === requestedCode)) {
      return res.status(409).json({ ok: false, error: 'station code already exists' });
    }

    const updated = { ...current, ...incoming, code: requestedCode };
    all[idx] = updated;

    const patchCode = (v) => (String(v || '').trim() === oldCode ? requestedCode : v);
    const patchList = (arr) => (Array.isArray(arr) ? arr.map(x => patchCode(x)).filter(Boolean) : arr);

    for (let i = 0; i < all.length; i++) {
      if (i === idx) continue;
      const s = all[i];
      if (s && s.transfer_to) {
        all[i] = { ...s, transfer_to: patchList(s.transfer_to) };
      }
    }

    const cfg = DataService.getConfig();
    let cfgChanged = false;
    if (cfg && cfg.current_station && cfg.current_station.code === oldCode) {
      cfg.current_station.code = requestedCode;
      cfgChanged = true;
    }
    if (cfg && Array.isArray(cfg.transfers)) {
      const before = JSON.stringify(cfg.transfers);
      cfg.transfers = cfg.transfers.map(p => Array.isArray(p) ? [patchCode(p[0]), patchCode(p[1])] : p);
      if (JSON.stringify(cfg.transfers) !== before) cfgChanged = true;
    }

    const lines = DataService.getLines();
    let linesChanged = false;
    for (let i = 0; i < lines.length; i++) {
      const l = lines[i];
      if (!l || !Array.isArray(l.stations)) continue;
      const before = JSON.stringify(l.stations);
      const after = patchList(l.stations);
      if (JSON.stringify(after) !== before) {
        lines[i] = { ...l, stations: after };
        linesChanged = true;
      }
    }

    const fares = DataService.getFares();
    let faresChanged = false;
    for (let i = 0; i < fares.length; i++) {
      const f = fares[i];
      if (!f) continue;
      const from = patchCode(f.from);
      const to = patchCode(f.to);
      if (from !== f.from || to !== f.to) {
        fares[i] = { ...f, from, to };
        faresChanged = true;
      }
    }

    const orders = DataService.getOrders();
    let ordersChanged = false;
    for (let i = 0; i < orders.length; i++) {
      const o = orders[i];
      if (!o) continue;
      const start = patchCode(o.start);
      const terminal = patchCode(o.terminal);
      if (start !== o.start || terminal !== o.terminal) {
        orders[i] = { ...o, start, terminal };
        ordersChanged = true;
      }
    }

    const orderIndex = DataService.getOrderIndex();
    let orderIndexChanged = false;
    for (const k of Object.keys(orderIndex)) {
      const o = orderIndex[k];
      if (!o) continue;
      const start = patchCode(o.start);
      const terminal = patchCode(o.terminal);
      if (start !== o.start || terminal !== o.terminal) {
        orderIndex[k] = { ...o, start, terminal };
        orderIndexChanged = true;
      }
    }

    const ticketIndex = DataService.getTicketIndex();
    let ticketIndexChanged = false;
    for (const k of Object.keys(ticketIndex)) {
      const t = ticketIndex[k];
      if (!t) continue;
      const patched = {
        ...t,
        start: patchCode(t.start),
        terminal: patchCode(t.terminal),
        station_code: patchCode(t.station_code)
      };
      if (patched.start !== t.start || patched.terminal !== t.terminal || patched.station_code !== t.station_code) {
        ticketIndex[k] = patched;
        ticketIndexChanged = true;
      }
    }

    await DataService.saveStations(all);
    io.emit('stations:updated', all);
    if (cfgChanged) await DataService.saveConfig(cfg);
    if (linesChanged) {
      await DataService.saveLines(lines);
      io.emit('lines:updated', lines);
    }
    if (faresChanged) {
      await DataService.saveFares(fares);
      io.emit('fares:updated', fares);
    }
    if (ordersChanged) await DataService.saveOrders(orders);
    if (orderIndexChanged) await DataService.saveOrderIndex(orderIndex);
    if (ticketIndexChanged) await DataService.saveTicketIndex(ticketIndex);

    appendReqLog(req, { category: 'admin', type: 'update_station', detail: { code: req.params.code, payload: incoming, renamed: renaming ? { from: oldCode, to: requestedCode } : null } });
    res.json({ ok: true, renamed: renaming ? { from: oldCode, to: requestedCode } : null });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'update_station_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to save stations' });
  }
});
router.delete('/stations/:code', async (req, res) => {
  try {
    const code = req.params.code;
    let allStations = DataService.getStations();
    const initialLen = allStations.length;
    allStations = allStations.filter(s => s.code !== code);
    
    if (allStations.length !== initialLen) {
        await DataService.saveStations(allStations);
        io.emit('stations:updated', allStations);
    }
  
    let allFares = DataService.getFares();
    const initialFaresLen = allFares.length;
    allFares = allFares.filter(f => f.from !== code && f.to !== code);
    
    if (allFares.length !== initialFaresLen) {
        await DataService.saveFares(allFares);
        io.emit('fares:updated', allFares);
    }
  
    appendReqLog(req, { category: 'admin', type: 'delete_station', detail: { code } });
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'delete_station_failed', level: 'error', detail: { error: e?.message || String(e), code: req.params.code } });
    res.status(500).json({ ok: false, error: 'failed to delete station' });
  }
});

// Lines
router.get('/lines', (req, res) => res.json(DataService.getLines()));
router.post('/lines', async (req, res) => {
  try {
    const all = DataService.getLines();
    all.push(req.body);
    await DataService.saveLines(all);
    appendReqLog(req, { category: 'admin', type: 'add_line', detail: req.body });
    io.emit('lines:updated', all);
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'add_line_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to save lines' });
  }
});
router.put('/lines/:id', async (req, res) => {
  try {
    const all = DataService.getLines();
    const idx = all.findIndex(l => l.id === req.params.id);
    if (idx < 0) return res.status(404).json({ ok: false, error: 'line not found' });
    const incoming = req.body || {};
    const current = all[idx] || {};
    const oldId = String(current.id || '').trim();
    const requestedId = (incoming.id == null) ? oldId : String(incoming.id || '').trim();
    const renaming = requestedId && oldId && requestedId !== oldId;
    if (!requestedId) return res.status(400).json({ ok: false, error: 'line id required' });
    if (renaming && all.some((l, i) => i !== idx && String(l.id || '').trim() === requestedId)) {
      return res.status(409).json({ ok: false, error: 'line id already exists' });
    }
    all[idx] = { ...incoming, id: requestedId };
    await DataService.saveLines(all);
    appendReqLog(req, { category: 'admin', type: 'update_line', detail: { id: req.params.id, payload: req.body, renamed: renaming ? { from: oldId, to: requestedId } : null } });
    io.emit('lines:updated', all);
    res.json({ ok: true, renamed: renaming ? { from: oldId, to: requestedId } : null });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'update_line_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to save lines' });
  }
});
router.delete('/lines/:id', async (req, res) => {
  try {
    let all = DataService.getLines();
    all = all.filter(l => l.id !== req.params.id);
    await DataService.saveLines(all);
    io.emit('lines:updated', all);
    appendReqLog(req, { category: 'admin', type: 'delete_line', detail: { id: req.params.id } });
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'delete_line_failed', level: 'error', detail: { error: e?.message || String(e), id: req.params.id } });
    res.status(500).json({ ok: false, error: 'failed to delete line' });
  }
});

// Fares
router.get('/fares', (req, res) => res.json(DataService.getFares()));
router.post('/fares', async (req, res) => {
  try {
    const all = DataService.getFares();
    const { from, to } = req.body || {};
    if (!from || !to) return res.status(400).json({ ok: false, error: 'from/to required' });
    const rest = all.filter(f => !(f.from === from && f.to === to));
    const payload = {
      from, to,
      cost_regular: req.body.cost_regular ?? req.body.cost ?? 0,
      cost_express: req.body.cost_express ?? req.body.cost ?? 0
    };
    rest.push(payload);
    await DataService.saveFares(rest);
    appendReqLog(req, { category: 'admin', type: 'update_fare', detail: payload });
    io.emit('fares:updated', rest);
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'update_fare_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to save fares' });
  }
});
router.delete('/fares', async (req, res) => {
  try {
    const { from, to } = req.body;
    let all = DataService.getFares();
    all = all.filter(f => !(f.from === from && f.to === to));
    await DataService.saveFares(all);
    appendReqLog(req, { category: 'admin', type: 'delete_fare', detail: { from: req.body?.from, to: req.body?.to } });
    io.emit('fares:updated', all);
    res.json({ ok: true });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'delete_fare_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to delete fare' });
  }
});

// Stats Ingest
router.post('/stats/upload', async (req, res) => {
  const r = req.body || {};
  if (!r.window_day && !r.window_hour) {
    appendReqLog(req, { category: 'device', type: 'stats_upload_invalid', level: 'warn', detail: { error: 'missing window_day/hour', payload: r } });
    return res.status(400).json({ ok: false, error: 'missing window_day/hour' });
  }
  
  const item = {
    device: r.device || 'unknown',
    station_code: r.station_code || '',
    station_name: r.station_name || '',
    sold_tickets: Number(r.sold_tickets || 0),
    sold_trips: Number(r.sold_trips || 0),
    revenue: Number(r.revenue || 0),
    ts: Number(r.ts || Date.now()),
    window_hour: String(r.window_hour || ''),
    window_day: String(r.window_day || ''),
    type: 'ticket'
  };
  
  await DataService.appendStatTicket(item);
  
  io.emit('stats:ticket:updated', item);
  appendReqLog(req, { category: 'device', type: 'stats_upload', detail: item });
  res.json({ ok: true });
});

router.post('/stats/ticket', async (req, res) => {
    const { device, station_code, station_name, sold_tickets, sold_trips, revenue, ts, window_hour, window_day } = req.body || {};
    if(device !== 'ticket_machine') {
      appendReqLog(req, { category: 'device', type: 'stats_ticket_invalid', level: 'warn', detail: { error: 'device must be ticket_machine', payload: req.body } });
      return res.status(400).json({ ok:false, error:'device must be ticket_machine' });
    }
    const item = { device, station_code, station_name, sold_tickets: sold_tickets||0, sold_trips: sold_trips||0, revenue: revenue||0, ts: ts||Date.now(), window_hour, window_day };
    await DataService.appendStatTicket(item);
    io.emit('stats:ticket:updated', item);
    appendReqLog(req, { category: 'device', type: 'stats_ticket', detail: item });
    res.json({ ok:true });
});

router.post('/stats/gate', async (req, res) => {
    const { device, station_code, entries, exits, ts, window_hour, window_day } = req.body || {};
    if(device !== 'gate') {
      appendReqLog(req, { category: 'device', type: 'stats_gate_invalid', level: 'warn', detail: { error: 'device must be gate', payload: req.body } });
      return res.status(400).json({ ok:false, error:'device must be gate' });
    }
    const item = { device, station_code, entries: entries||0, exits: exits||0, ts: ts||Date.now(), window_hour, window_day };
    await DataService.appendStatGate(item);
    io.emit('stats:gate:updated', item);
    appendReqLog(req, { category: 'device', type: 'stats_gate', detail: item });
    res.json({ ok:true });
});

router.get('/stats/ticket/total', (req, res) => {
    const all = DataService.getStatsTicket();
    
    // Get local date string YYYY-MM-DD
    const now = new Date();
    // Assuming server is in same timezone as operations (or China Standard Time UTC+8)
    // A simple hack for local YYYY-MM-DD
    const offset = now.getTimezoneOffset() * 60000;
    const localTime = new Date(now.getTime() - offset);
    const today = localTime.toISOString().split('T')[0];

    const total = all.reduce((acc, cur) => {
        // Total overall stats
        acc.total_tickets += (cur.sold_tickets || 0);
        acc.total_revenue += (cur.revenue || 0);

        // Filter for today's stats
        if (cur.window_day === today) {
            acc.sold_tickets += (cur.sold_tickets || 0);
            acc.revenue += (cur.revenue || 0);
        }
        return acc;
    }, { sold_tickets: 0, revenue: 0, total_tickets: 0, total_revenue: 0 });
    res.json({ ok: true, total });
});

// Logs
router.get('/logs', async (req, res) => {
    const max = Number(req.query.max) || 200;
    const category = req.query.category;
    const type = req.query.type;
    const q = req.query.q;
    const since = req.query.since;
    const until = req.query.until;
    res.json({ ok: true, logs: await DataService.readLogs({ max, category, type, q, since, until }) });
});

router.post('/log', (req, res) => {
  const body = req.body || {};
  const type = String(body.type || '').trim();
  if (!type) return res.status(400).json({ ok: false, error: 'type required' });
  appendReqLog(req, {
    category: body.category || body.source || 'admin',
    source: body.source,
    level: body.level,
    type,
    detail: body.detail
  });
  res.json({ ok: true });
});

// Ticket Management (Admin)
router.get('/tickets', (req, res) => {
    const q = String(req.query.q||'').trim().toLowerCase();
    const idx = DataService.getTicketIndex();
    let list = Object.entries(idx).map(([ticket_id, data])=>({ ticket_id, ...data }));
    if(q){
      list = list.filter(t => t.ticket_id.toLowerCase().includes(q) || String(t.station_code||'').toLowerCase().includes(q) || String(t.start||'').toLowerCase().includes(q) || String(t.terminal||'').toLowerCase().includes(q));
    }
    list.sort((a,b)=>Number(b.last_update_ts||0)-Number(a.last_update_ts||0));
    list = list.map(t => {
      const tripsRemaining = (t.trips_remaining ?? t.rides_remaining);
      const tripsTotal = (t.trips_total ?? t.rides_total);
      const shouldBeUsed =
        (typeof tripsRemaining === 'number' && tripsRemaining <= 0) ||
        ((tripsTotal == null || Number(tripsTotal) <= 1) && String(t.last_action || '') === 'exit');

      const status = (t.status && t.status !== 'valid') ? t.status : (shouldBeUsed ? 'used' : (t.status || 'valid'));
      return { ...t, status };
    });
    res.json({ ok:true, tickets:list });
});

router.get('/tickets/:id', async (req, res) => {
    const id = normalizeTicketId(String(req.params.id||'').trim());
    if(!id) return res.status(400).json({ ok:false, error:'ticket_id required' });
    const idx = DataService.getTicketIndex();
    const events = await DataService.getTicketEvents(id);
    const raw = idx[id] || {};
    const tripsRemaining = (raw.trips_remaining ?? raw.rides_remaining);
    const tripsTotal = (raw.trips_total ?? raw.rides_total);
    const shouldBeUsed =
      (typeof tripsRemaining === 'number' && tripsRemaining <= 0) ||
      ((tripsTotal == null || Number(tripsTotal) <= 1) && String(raw.last_action || '') === 'exit');
    const status = (raw.status && raw.status !== 'valid') ? raw.status : (shouldBeUsed ? 'used' : (raw.status || 'valid'));
    res.json({ ok:true, ticket_id:id, index: { ...raw, status }, events });
});

// IC Card Management
router.get('/ic-cards', async (req, res) => {
  const q = String(req.query.q || '').trim().toLowerCase();
  const status = String(req.query.status || '').trim().toLowerCase();
  const source = String(req.query.source || '').trim().toLowerCase();
  let list = DataService.getIcCards().map((card) => ({
    ...presentIcCard(card),
    status_label: mapIcCardStatus(card.status),
    card_type_label: mapIcCardType(card.card_type)
  }));
  if (q) {
    list = list.filter((card) => {
      const haystack = [
        card.card_id,
        card.order_code,
        card.voucher_code,
        card.code,
        card.holder_name,
        card.source
      ].map((x) => String(x || '').toLowerCase()).join('\n');
      return haystack.includes(q);
    });
  }
  if (status) list = list.filter((card) => String(card.status || '').toLowerCase() === status);
  if (source) list = list.filter((card) => String(card.source || '').toLowerCase() === source);
  list.sort((a, b) => Number(b.last_update_ts || 0) - Number(a.last_update_ts || 0));
  res.json({ ok: true, cards: list });
});

router.get('/ic-cards/:id', async (req, res) => {
  const id = normalizeIcCardId(req.params.id);
  if (!id) return res.status(400).json({ ok: false, error: 'card_id required' });
  const card = DataService.getIcCardIndex()[id];
  if (!card) return res.status(404).json({ ok: false, error: 'ic card not found' });
  const events = await DataService.getIcCardEvents(id);
  res.json({
    ok: true,
    card: {
      ...presentIcCard(card),
      status_label: mapIcCardStatus(card.status),
      card_type_label: mapIcCardType(card.card_type)
    },
    events
  });
});

router.post('/ic-cards', async (req, res) => {
  try {
    const body = req.body || {};
    const card_id = normalizeIcCardId(body.card_id) || buildIcCardId();
    if (DataService.getIcCardIndex()[card_id]) {
      return res.status(409).json({ ok: false, error: 'ic card already exists' });
    }
    const balance = Math.max(0, Number(body.balance ?? body.initial_balance ?? 0) || 0);
    const holder_name = String(body.holder_name || '').trim();
    if (!holder_name) return res.status(400).json({ ok: false, error: 'holder_name required' });
    if (!IC_CARD_HOLDER_NAME_RE.test(holder_name)) {
      return res.status(400).json({ ok: false, error: 'holder_name must use English letters and symbols only' });
    }
    const now = Date.now();
    const card = {
      card_id,
      holder_name,
      card_type: 'stored_value',
      status: String(body.status || 'active').trim(),
      balance,
      deposit: 0,
      source: String(body.source || 'admin').trim(),
      order_code: String(body.order_code || '').trim().toUpperCase(),
      purchase_amount: Math.max(0, Number(body.purchase_amount ?? balance) || 0),
      created_ts: now,
      activated_ts: now,
      last_update_ts: now
    };
    await DataService.upsertIcCard(card);
    await DataService.appendIcCardEvent({
      ts: now,
      type: 'create',
      card_id,
      operator: 'admin',
      detail: { balance, source: card.source, holder_name: card.holder_name }
    });
    appendReqLog(req, { category: 'admin', type: 'ic_card_create', detail: { card_id, holder_name: card.holder_name, balance, source: card.source } });
    res.json({ ok: true, card_id, display_card_id: displayIcCardId(card), card: presentIcCard(card) });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'ic_card_create_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to create ic card' });
  }
});

router.put('/ic-cards/:id', async (req, res) => {
  try {
    const id = normalizeIcCardId(req.params.id);
    if (!id) return res.status(400).json({ ok: false, error: 'card_id required' });
    const current = DataService.getIcCardIndex()[id];
    if (!current) return res.status(404).json({ ok: false, error: 'ic card not found' });
    const body = req.body || {};
    const nextHolderName = body.holder_name == null ? current.holder_name : String(body.holder_name || '').trim();
    if (!nextHolderName) return res.status(400).json({ ok: false, error: 'holder_name required' });
    if (!IC_CARD_HOLDER_NAME_RE.test(nextHolderName)) {
      return res.status(400).json({ ok: false, error: 'holder_name must use English letters and symbols only' });
    }
    const patch = {
      holder_name: nextHolderName,
      card_type: 'stored_value',
      status: body.status == null ? current.status : String(body.status || 'active').trim(),
      deposit: 0
    };
    if (body.balance != null) patch.balance = Math.max(0, Number(body.balance) || 0);
    if (patch.status === 'active' && !current.activated_ts) patch.activated_ts = Date.now();
    const card = await DataService.upsertIcCard({ ...current, ...patch, card_id: id });
    await DataService.appendIcCardEvent({
      ts: Date.now(),
      type: 'update',
      card_id: id,
      operator: 'admin',
      detail: patch
    });
    appendReqLog(req, { category: 'admin', type: 'ic_card_update', detail: { card_id: id, patch } });
    res.json({ ok: true, card: presentIcCard(card) });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'ic_card_update_failed', level: 'error', detail: { error: e?.message || String(e), card_id: req.params.id, payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to update ic card' });
  }
});

router.delete('/ic-cards/:id', async (req, res) => {
  try {
    const id = normalizeIcCardId(req.params.id);
    if (!id) return res.status(400).json({ ok: false, error: 'card_id required' });
    const current = DataService.getIcCardIndex()[id];
    if (!current) return res.status(404).json({ ok: false, error: 'ic card not found' });
    await DataService.appendIcCardEvent({
      ts: Date.now(),
      type: 'delete',
      card_id: id,
      operator: 'admin',
      detail: { holder_name: current.holder_name || '', order_code: current.order_code || '', source: current.source || '' }
    });
    await DataService.deleteIcCard(id);
    appendReqLog(req, { category: 'admin', type: 'ic_card_delete', detail: { card_id: id, holder_name: current.holder_name || '' } });
    res.json({ ok: true, card_id: id });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'ic_card_delete_failed', level: 'error', detail: { error: e?.message || String(e), card_id: req.params.id } });
    res.status(500).json({ ok: false, error: 'failed to delete ic card' });
  }
});

router.post('/ic-cards/:id/topup', async (req, res) => {
  try {
    const id = normalizeIcCardId(req.params.id);
    if (!id) return res.status(400).json({ ok: false, error: 'card_id required' });
    const current = DataService.getIcCardIndex()[id];
    if (!current) return res.status(404).json({ ok: false, error: 'ic card not found' });
    const amount = Math.round((Number(req.body?.amount) || 0) * 100) / 100;
    if (!(amount > 0)) return res.status(400).json({ ok: false, error: 'topup amount must be greater than 0' });
    const balance = Math.round(((Number(current.balance || 0) + amount) * 100)) / 100;
    const card = await DataService.upsertIcCard({ ...current, card_id: id, balance });
    await DataService.appendIcCardEvent({
      ts: Date.now(),
      type: 'topup',
      card_id: id,
      operator: 'admin',
      amount,
      balance
    });
    appendReqLog(req, { category: 'admin', type: 'ic_card_topup', detail: { card_id: id, amount, balance } });
    res.json({ ok: true, card: presentIcCard(card) });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'ic_card_topup_failed', level: 'error', detail: { error: e?.message || String(e), card_id: req.params.id, payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to top up ic card' });
  }
});

router.post('/ic-cards/:id/sync', async (req, res) => {
  try {
    const id = normalizeIcCardId(req.params.id);
    if (!id) return res.status(400).json({ ok: false, error: 'card_id required' });
    const current = DataService.getIcCardIndex()[id];
    if (!current) return res.status(404).json({ ok: false, error: 'ic card not found' });

    const body = req.body || {};
    const resolveStation = buildStationResolver();
    const ts = toFiniteNumberOrUndef(body.ts) ?? Date.now();
    const syncTypeRaw = String(body.type || body.event_type || body.last_event || 'sync').trim().toLowerCase();
    const syncType = syncTypeRaw === 'refill' ? 'topup' : (syncTypeRaw || 'sync');
    const action = String(body.action || body.last_action || '').trim().toLowerCase();
    const device = String(body.device || body.source || 'device').trim() || 'device';
    const station_code = resolveCurrentStationCode(body, resolveStation);
    const entry_station = resolveStation(body.entry_station || body.start_station || current.entry_station || '');
    const exit_station = resolveStation(body.exit_station || current.exit_station || '');
    const balance = body.balance == null ? toMoney(current.balance) : Math.max(0, toMoney(body.balance));
    const fare = body.fare == null ? toMoney(body.last_fare ?? current.last_fare ?? 0) : toMoney(body.fare);
    const amount = body.amount == null ? toFiniteNumberOrUndef(body.topup_amount ?? body.refill_amount) : toFiniteNumberOrUndef(body.amount);

    let entered = body.entered == null ? isTruthy(current.entered) : isTruthy(body.entered);
    let exited = body.exited == null ? isTruthy(current.exited) : isTruthy(body.exited);
    if (action === 'entry') {
      entered = true;
      exited = false;
    } else if (action === 'exit') {
      entered = false;
      exited = true;
    }

    const patch = {
      balance,
      entered,
      exited,
      entry_station: action === 'exit' ? entry_station : (entered ? (station_code || entry_station) : entry_station),
      exit_station: action === 'exit' ? (station_code || exit_station) : exit_station,
      last_fare: fare == null ? toMoney(current.last_fare ?? 0) : fare,
      last_action: action || (syncType === 'topup' ? 'topup' : current.last_action || ''),
      last_station_code: station_code || current.last_station_code || '',
      last_result: String(body.result || current.last_result || 'pass').trim() || 'pass',
      last_reason: String(body.reason || '').trim(),
      last_event: syncType
    };
    if (action === 'entry') patch.entry_ts = ts;
    if (action === 'exit') patch.exit_ts = ts;

    const card = await DataService.upsertIcCard({ ...current, card_id: id, ...patch });
    const event = {
      ts,
      type: syncType,
      card_id: id,
      action: action || undefined,
      result: patch.last_result,
      reason: patch.last_reason || undefined,
      station_code: station_code || undefined,
      entry_station: card.entry_station || undefined,
      exit_station: card.exit_station || undefined,
      fare: patch.last_fare,
      amount: amount == null ? undefined : toMoney(amount),
      balance: toMoney(card.balance),
      remaining_balance: toMoney(card.balance),
      device
    };
    await DataService.appendIcCardEvent(event);
    io.emit(syncType === 'topup' ? 'ic-card:topup' : 'ic-card:sync', event);
    appendReqLog(req, { category: 'device', type: 'ic_card_sync', detail: event });
    res.json({ ok: true, card: presentIcCard(card), event });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'ic_card_sync_failed', level: 'error', detail: { error: e?.message || String(e), card_id: req.params.id, payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to sync ic card' });
  }
});

router.post('/cards/open', async (req, res) => {
  try {
    const body = req.body || {};
    const now = Date.now();
    const voucher_code = normalizeOrderCode(body.voucher_code || body.order_code || body.code);
    const device = String(body.device || 'ticket_machine').trim() || 'ticket_machine';
    const station_code = String(body.station_code || '').trim();
    const holder_name = String(body.holder_name || '').trim();
    const note = String(body.note || '').trim();
    const card_type = String(body.card_type || 'stored_value').trim() || 'stored_value';
    const payment_mode = String(body.payment_mode || '').trim().toLowerCase();
    const card_mode = String(body.card_mode || '').trim().toLowerCase();

    if (voucher_code) {
      const current = findIcCardByOrderCode(voucher_code);
      if (!current) {
        appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: false, reason: 'not_found', voucher_code, device, payload: body } });
        return res.status(404).json({ ok: false, error: 'ic card order not found' });
      }
      const status = String(current.status || '').trim().toLowerCase();
      if (status && status !== 'pending_pickup') {
        appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: false, reason: 'already_used', voucher_code, card_id: current.card_id, status, device } });
        return res.status(409).json({ ok: false, error: 'card order already redeemed' });
      }

      const current_card_id = normalizeIcCardId(current.card_id);
      const issued_card_id = normalizeIcCardId(body.card_id) || current_card_id || buildIcCardId();
      const deposit = toMoney(body.deposit ?? current.deposit ?? 0);
      const balance = toMoney(body.balance ?? body.topup ?? current.balance ?? 0);
      const purchase_amount = toMoney(body.order_value ?? body.amount_paid ?? current.purchase_amount ?? (deposit + balance));
      const amount_paid = toMoney(body.amount_paid);
      if (purchase_amount > 0 && amount_paid < purchase_amount) {
        appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: false, reason: 'payment_required', voucher_code, card_id: current.card_id, purchase_amount, amount_paid, device } });
        return res.status(409).json({ ok: false, error: 'payment required before redeem' });
      }
      const card = await DataService.upsertIcCard({
        ...current,
        card_id: issued_card_id,
        holder_name: holder_name || current.holder_name || '',
        note: note || current.note || '',
        card_type: String(current.card_type || card_type).trim() || 'stored_value',
        status: 'active',
        balance,
        deposit,
        purchase_amount,
        source: String(current.source || 'online').trim() || 'online',
        payment_mode: payment_mode || current.payment_mode || 'online',
        card_mode: card_mode || current.card_mode || 'redeem',
        activated_ts: current.activated_ts || now,
        redeemed_ts: now,
        redeemed_station_code: station_code || current.redeemed_station_code || '',
        redeem_device: device,
        entered: false,
        exited: false,
        entry_station: '',
        exit_station: '',
        last_fare: 0,
        last_action: 'open',
        last_event: 'open',
        last_result: 'pass',
        last_reason: ''
      });

      const event = {
        ts: now,
        type: 'redeem',
        card_id: card.card_id,
        order_code: voucher_code,
        station_code,
        device,
        balance,
        deposit,
        purchase_amount
      };
      await DataService.appendIcCardEvent(event);
      if (current_card_id && current_card_id !== issued_card_id) {
        await DataService.deleteIcCard(current.card_id);
      }
      io.emit('ic-card:opened', { card_id: card.card_id, order_code: voucher_code, status: 'active' });
      appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: true, mode: 'redeem', voucher_code, card_id: card.card_id, issued_card_id, station_code, device, balance, deposit } });
      return res.json({ ok: true, mode: 'redeem', card_id: card.card_id, display_card_id: displayIcCardId(card), card: presentIcCard(card), data: presentIcCard(card) });
    }

    const card_id = normalizeIcCardId(body.card_id) || buildIcCardId();
    const current = DataService.getIcCardIndex()[card_id] || {};
    const currentStatus = String(current.status || '').trim().toLowerCase();
    if (current.card_id && currentStatus === 'active') {
      appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: false, reason: 'already_exists', card_id, device } });
      return res.status(409).json({ ok: false, error: 'ic card already active' });
    }

    const deposit = toMoney(body.deposit ?? current.deposit ?? 0);
    const balance = toMoney(body.balance ?? body.topup ?? current.balance ?? 0);
    const purchase_amount = toMoney(body.order_value ?? body.amount_paid ?? current.purchase_amount ?? (deposit + balance));
    const card = await DataService.upsertIcCard({
      ...current,
      card_id,
      order_code: normalizeOrderCode(body.order_code || current.order_code),
      holder_name: holder_name || current.holder_name || '',
      note: note || current.note || '',
      card_type: String(current.card_type || card_type).trim() || 'stored_value',
      status: 'active',
      balance,
      deposit,
      purchase_amount,
      source: String(body.source || current.source || 'ticket_machine').trim() || 'ticket_machine',
      payment_mode: payment_mode || current.payment_mode || 'local',
      card_mode: card_mode || current.card_mode || 'open',
      created_ts: current.created_ts || now,
      activated_ts: current.activated_ts || now,
      opened_ts: now,
      issue_station_code: station_code || current.issue_station_code || '',
      issue_device: device,
      entered: false,
      exited: false,
      entry_station: '',
      exit_station: '',
      last_fare: 0,
      last_action: 'open',
      last_event: 'open',
      last_result: 'pass',
      last_reason: ''
    });

    const event = {
      ts: now,
      type: 'open',
      card_id,
      station_code,
      device,
      balance,
      deposit,
      purchase_amount
    };
    await DataService.appendIcCardEvent(event);
    io.emit('ic-card:opened', { card_id, status: 'active' });
    appendReqLog(req, { category: 'device', type: 'card_open', detail: { ok: true, mode: 'open', card_id, station_code, device, balance, deposit } });
    res.json({ ok: true, mode: 'open', card_id, display_card_id: displayIcCardId(card), card: presentIcCard(card), data: presentIcCard(card) });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'card_open_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to open ic card' });
  }
});

router.post('/cards/check', async (req, res) => {
  try {
    const body = req.body || {};
    const card_id = normalizeIcCardId(body.card_id);
    const action = String(body.action || '').trim().toLowerCase();
    const device = String(body.device || 'gate').trim() || 'gate';
    const ts = toFiniteNumberOrUndef(body.ts) ?? Date.now();
    if (!card_id || !action) return res.status(400).json({ ok: false, error: 'card_id and action required' });
    if (action !== 'entry' && action !== 'exit') return res.status(400).json({ ok: false, error: 'action must be entry/exit' });

    const resolveStation = buildStationResolver();
    const station_code = resolveCurrentStationCode(body, resolveStation);
    const current = DataService.getIcCardIndex()[card_id];
    const hintedBalance = toFiniteNumberOrUndef(body.balance);

    const deny = async (reason, extra = {}) => {
      const detail = {
        ts,
        type: 'check',
        card_id,
        action,
        result: 'deny',
        reason,
        station_code,
        device,
        ...extra
      };
      await DataService.appendIcCardEvent(detail);
      if (current) {
        await DataService.upsertIcCard({
          ...current,
          last_action: action,
          last_station_code: station_code || current.last_station_code || '',
          last_result: 'deny',
          last_reason: reason,
          last_event: 'check'
        });
      }
      io.emit('ic-card:check', detail);
      appendReqLog(req, { category: 'device', type: 'card_check', detail });
      return res.json({ ok: true, card_id, action, result: 'deny', reason, station_code, ...extra });
    };

    if (!current) return deny('not_found');
    const status = String(current.status || '').trim().toLowerCase();
    if (status && status !== 'active') return deny(`status_${status}`);
    if (!station_code) return deny('missing_station');

    const entered = isTruthy(current.entered != null ? current.entered : body.entered);
    const exited = isTruthy(current.exited != null ? current.exited : body.exited);
    const currentBalance = toMoney(current.balance ?? hintedBalance ?? 0);

    if (action === 'entry') {
      if (entered && !exited) return deny('already_entered', { balance: currentBalance, remaining_balance: currentBalance });

      const card = await DataService.upsertIcCard({
        ...current,
        entered: true,
        exited: false,
        entry_station: station_code,
        exit_station: '',
        last_fare: 0,
        last_action: 'entry',
        last_station_code: station_code,
        last_result: 'pass',
        last_reason: '',
        last_event: 'check',
        entry_ts: ts
      });
      const event = {
        ts,
        type: 'check',
        card_id,
        action,
        result: 'pass',
        station_code,
        entry_station: station_code,
        device,
        balance: toMoney(card.balance)
      };
      await DataService.appendIcCardEvent(event);
      io.emit('ic-card:check', event);
      appendReqLog(req, { category: 'device', type: 'card_check', detail: event });
      return res.json({
        ok: true,
        card_id,
        action,
        result: 'pass',
        station_code,
        entry_station: station_code,
        balance: toMoney(card.balance),
        remaining_balance: toMoney(card.balance),
        fare: 0
      });
    }

    if (!entered) return deny('not_entered', { balance: currentBalance, remaining_balance: currentBalance });
    if (exited) return deny('already_exited', { balance: currentBalance, remaining_balance: currentBalance });

    const entry_station = resolveStation(current.entry_station || body.entry_station || body.start_station || '');
    if (!entry_station) return deny('missing_entry_station', { balance: currentBalance, remaining_balance: currentBalance });

    const fareInfo = LogicService.computeFareBoth(entry_station, station_code);
    const fareRaw = toFiniteNumberOrUndef(fareInfo?.regular) ?? toFiniteNumberOrUndef(fareInfo?.express);
    if (fareRaw == null) return deny('fare_not_found', { balance: currentBalance, remaining_balance: currentBalance, entry_station, exit_station: station_code });

    const fare = toMoney(fareRaw);
    if (currentBalance < fare) {
      return deny('insufficient_balance', {
        balance: currentBalance,
        remaining_balance: currentBalance,
        fare,
        entry_station,
        exit_station: station_code
      });
    }

    const nextBalance = toMoney(currentBalance - fare);
    const card = await DataService.upsertIcCard({
      ...current,
      balance: nextBalance,
      entered: false,
      exited: true,
      exit_station: station_code,
      last_fare: fare,
      last_action: 'exit',
      last_station_code: station_code,
      last_result: 'pass',
      last_reason: '',
      last_event: 'check',
      exit_ts: ts
    });
    const event = {
      ts,
      type: 'check',
      card_id,
      action,
      result: 'pass',
      station_code,
      entry_station,
      exit_station: station_code,
      fare,
      balance: nextBalance,
      remaining_balance: nextBalance,
      device
    };
    await DataService.appendIcCardEvent(event);
    io.emit('ic-card:check', event);
    appendReqLog(req, { category: 'device', type: 'card_check', detail: event });
    return res.json({
      ok: true,
      card_id,
      action,
      result: 'pass',
      station_code,
      entry_station,
      exit_station: station_code,
      fare,
      balance: nextBalance,
      remaining_balance: nextBalance
    });
  } catch (e) {
    appendReqLog(req, { category: 'system', type: 'card_check_failed', level: 'error', detail: { error: e?.message || String(e), payload: req.body } });
    res.status(500).json({ ok: false, error: 'failed to check ic card' });
  }
});

// Export
router.get('/export', (req, res) => {
    res.json(DataService.buildExportPayload());
});

// Ticket Operations
router.post('/tickets/sale', async (req, res) => {
  const { start, terminal, train_type, cost, station_code, device, trips_total, trips_remaining } = req.body || {};
  const ticket_id = normalizeTicketId((req.body || {}).ticket_id);
  if (!ticket_id) return res.status(400).json({ ok: false, error: 'ticket_id required' });

  const toFiniteNumberOrUndef = (v) => {
    if (v == null) return undefined;
    const n = Number(v);
    return Number.isFinite(n) ? n : undefined;
  };
  const tTotal = toFiniteNumberOrUndef(trips_total);
  const tRemain = toFiniteNumberOrUndef(trips_remaining);

  const ev = {
    ts: Date.now(),
    type: 'sale',
    ticket_id,
    start,
    terminal,
    train_type,
    cost: cost || 0,
    station_code: station_code || '',
    device: device || 'unknown',
    trips_total: tTotal,
    trips_remaining: tRemain
  };

  await DataService.appendTicketEvent(ev);
  await DataService.upsertTicketIndex({
    ticket_id,
    start,
    terminal,
    train_type,
    cost: cost || 0,
    status: 'valid',
    station_code,
    last_event: 'sale',
    trips_total: tTotal,
    trips_remaining: tRemain,
    last_update_ts: Date.now()
  });

  const now = new Date();
  const statItem = {
    device: device || 'unknown',
    station_code: station_code || '',
    sold_tickets: 1,
    revenue: cost || 0,
    ts: Date.now(),
    window_hour: now.getHours().toString().padStart(2, '0'),
    window_day: now.toISOString().split('T')[0],
    type: 'ticket'
  };
  await DataService.appendStatTicket(statItem);

  io.emit('ticket:sale', ev);
  io.emit('stats:ticket:updated', statItem);
  appendReqLog(req, { category: 'device', type: 'ticket_sale', detail: { ...ev, trips_total: tTotal, trips_remaining: tRemain } });
  res.json({ ok: true, ticket_id });
});

router.post('/tickets/check', async (req, res) => {
  const body = req.body || {};
  const ticket_id = normalizeTicketId(body.ticket_id);
  const action = String(body.action || '').trim().toLowerCase();
  const device = String(body.device || 'gate');
  const tsIn = Number(body.ts);
  const ts = Number.isFinite(tsIn) ? tsIn : Date.now();
  const hintTripsTotal = body.trips_total;
  const hintTripsRemaining = body.trips_remaining;

  const normStationKey = (v) => String(v || '').trim().toUpperCase().replace(/\s+/g, '');
  const buildStationResolver = () => {
    const stations = DataService.getStations?.() || [];
    const codeByKey = new Map();
    const nameToCode = new Map();
    const looksLikeStationCode = (v) => /^[A-Z0-9]+(?:-[A-Z0-9]+)+$/i.test(String(v || '').trim());
    for (const s of stations) {
      if (!s) continue;
      const code = String(s.code || '').trim();
      if (!code) continue;
      codeByKey.set(normStationKey(code), code);
      const cn = String(s.name || s.cn_name || s.station_name || '').trim();
      if (cn) nameToCode.set(normStationKey(cn), code);
      const en = String(s.en_name || s.en || s.enName || '').trim();
      if (en) nameToCode.set(normStationKey(en), code);
    }
    return (v) => {
      const raw = String(v || '').trim();
      if (!raw) return '';
      const k = normStationKey(raw);
      if (codeByKey.has(k)) return codeByKey.get(k);
      if (looksLikeStationCode(raw)) return raw;
      return nameToCode.get(k) || raw;
    };
  };
  const resolveStation = buildStationResolver();

  const station_code_in = String(body.station_code || '').trim();
  const station_code_raw = resolveStation(station_code_in);
  const station_code_norm = normStationKey(station_code_raw);
  const station_codes = Array.isArray(body.station_codes)
    ? body.station_codes.map(x => resolveStation(x)).filter(Boolean)
    : [];
  const station_codes_norm = new Set(station_codes.map(normStationKey).filter(Boolean));

  if (!ticket_id || !action) return res.status(400).json({ ok: false, error: 'ticket_id and action required' });
  if (action !== 'entry' && action !== 'exit') return res.status(400).json({ ok: false, error: 'action must be entry/exit' });

  const idx = DataService.getTicketIndex();
  const cur = idx[ticket_id];

  const deny = async (reason) => {
    const ev = { ts, type: 'check', ticket_id, action, result: 'deny', reason, station_code: station_code_raw || '', device };
    await DataService.appendTicketEvent(ev);
    await DataService.upsertTicketIndex({
      ticket_id,
      last_action: action,
      last_station_code: station_code_raw || '',
      last_result: 'deny',
      last_reason: reason,
      last_event: 'check',
      last_update_ts: Date.now()
    });
    io.emit('ticket:check', ev);
    appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
    return res.json({ ok: true, ticket_id, action, result: 'deny', reason });
  };

  if (!cur) return deny('not_found');
  if (cur.status && cur.status !== 'valid') return deny(`status_${cur.status}`);

  const start = resolveStation(cur.start);
  const terminal = resolveStation(cur.terminal);
  if (!start || !terminal) return deny('missing_route');

  const matchesStation = (code) => {
    const c = resolveStation(code);
    const ck = normStationKey(c);
    if (!ck) return false;
    if (station_codes_norm.size > 0) return station_codes_norm.has(ck);
    if (station_code_norm) return station_code_norm === ck;
    return true;
  };

  const isTruthy = (v) => v === true || v === 1 || v === '1' || String(v || '').toLowerCase() === 'true';
  const entered = isTruthy(cur.entered);
  const exited = isTruthy(cur.exited);

  const toFiniteNumberOrUndef = (v) => {
    if (v == null) return undefined;
    const n = Number(v);
    return Number.isFinite(n) ? n : undefined;
  };
  const tripsTotal = toFiniteNumberOrUndef(cur.trips_total) ?? toFiniteNumberOrUndef(cur.rides_total) ?? toFiniteNumberOrUndef(hintTripsTotal) ?? 1;
  const tripsRemain0 =
    toFiniteNumberOrUndef(cur.trips_remaining) ??
    toFiniteNumberOrUndef(cur.rides_remaining) ??
    toFiniteNumberOrUndef(hintTripsRemaining);
  const tripsRemainEffective = (tripsRemain0 == null) ? tripsTotal : tripsRemain0;

  if (action === 'entry') {
    if (!matchesStation(start)) return deny('wrong_station');
    if (entered && !exited) return deny('already_entered');

    const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: start, device };
    await DataService.appendTicketEvent(ev);
    await DataService.upsertTicketIndex({
      ticket_id,
      entered: true,
      exited: false,
      last_action: 'entry',
      last_station_code: start,
      last_result: 'pass',
      last_reason: '',
      last_event: 'check',
      trips_total: tripsTotal,
      trips_remaining: tripsRemainEffective,
      last_update_ts: Date.now()
    });
    io.emit('ticket:check', ev);
    appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
    return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: start, trips_remaining: tripsRemainEffective, destroy_ticket: false });
  }

  if (!matchesStation(terminal)) return deny('wrong_station');
  if (!entered) return deny('not_entered');
  if (exited) return deny('already_exited');

  const newTripsRemaining = Math.max(0, (Number(tripsRemainEffective) || 0) - 1);
  const shouldSetUsed = newTripsRemaining <= 0;
  const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: terminal, device, trips_remaining: newTripsRemaining };
  await DataService.appendTicketEvent(ev);
  await DataService.upsertTicketIndex({
    ticket_id,
    exited: true,
    last_action: 'exit',
    last_station_code: terminal,
    last_result: 'pass',
    last_reason: '',
    last_event: 'check',
    trips_total: tripsTotal,
    trips_remaining: newTripsRemaining,
    status: shouldSetUsed ? 'used' : (cur.status || 'valid'),
    last_update_ts: Date.now()
  });
  io.emit('ticket:check', ev);
  appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
  return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: terminal, trips_remaining: newTripsRemaining, destroy_ticket: shouldSetUsed });
});

router.post('/tickets/status', async (req, res) => {
  const { action, station_code, device, result, reason } = req.body || {};
  const ticket_id = normalizeTicketId((req.body || {}).ticket_id);
  const ridesRemainingRaw = (req.body || {}).rides_remaining;
  const tripsRemainingRaw = (req.body || {}).trips_remaining;
  const toFiniteNumberOrUndef = (v) => {
    if (v == null) return undefined;
    const n = Number(v);
    return Number.isFinite(n) ? n : undefined;
  };
  const rides_remaining = toFiniteNumberOrUndef(ridesRemainingRaw);
  const trips_remaining = (tripsRemainingRaw == null) ? rides_remaining : toFiniteNumberOrUndef(tripsRemainingRaw);
  const tsIn = Number((req.body || {}).ts);
  const ts = Number.isFinite(tsIn) ? tsIn : Date.now();
  if (!ticket_id || !action) return res.status(400).json({ ok: false, error: 'ticket_id and action required' });

  const ev = {
    ts,
    type: 'status',
    ticket_id,
    action,
    result,
    reason,
    station_code: station_code || '',
    device: device || 'unknown',
    trips_remaining,
    rides_remaining
  };

  await DataService.appendTicketEvent(ev);
  const shouldSetUsed = String(action) === 'exit' && String(result || '') === 'pass' && typeof trips_remaining === 'number' && trips_remaining <= 0;
  const cur = DataService.getTicketIndex()[ticket_id] || {};
  const status = (cur.status && cur.status !== 'valid') ? cur.status : (shouldSetUsed ? 'used' : (cur.status || 'valid'));

  await DataService.upsertTicketIndex({
    ticket_id,
    last_action: action,
    last_station_code: station_code,
    last_result: result,
    last_reason: reason,
    trips_remaining,
    rides_remaining,
    last_event: 'status',
    status,
    last_update_ts: Date.now()
  });

  io.emit('ticket:status', ev);
  appendReqLog(req, { category: 'device', type: 'ticket_status', detail: ev });
  res.json({ ok: true, ticket_id });
});

// Voucher Detail
router.get('/orders', (req, res) => {
    const list = DataService.getOrders();
    const map = LogicService.buildStationNameMap();
    const nameFor = (c) => (map && map[c]) || c;
    
    // Enrich with status if not present (default: consumed=false means Available, but check expiry?)
    // Actually, logic for expiry isn't clear. Assuming 'valid' if !consumed.
    
    const enriched = list.map(o => ({
        ...o,
        start_name: nameFor(o.start),
        terminal_name: nameFor(o.terminal),
        status: o.consumed ? 'used' : (o.expired ? 'expired' : 'valid') // simple logic
    })).reverse(); // Newest first
    
    res.json({ ok: true, orders: enriched });
});

router.delete('/orders/:code', async (req, res) => {
    const code = req.params.code;
    let list = DataService.getOrders();
    const initialLen = list.length;
    list = list.filter(o => o.code !== code);
    
    if (list.length !== initialLen) {
        await DataService.saveOrders(list);
        
        // Also remove from index
        const idx = DataService.getOrderIndex();
        delete idx[code];
        await DataService.saveOrderIndex(idx);
        
        io.emit('order:deleted', { code });
    }
    
    appendReqLog(req, { category: 'admin', type: 'delete_order', detail: { code, deleted: list.length !== initialLen } });
    res.json({ ok: true });
});

module.exports = router;
