'use strict';

/**
 * A1 Session 9 — POST /api/slips/parse (Slip Reader).
 *
 * Auth required. Accepts a base64/data-URL slip screenshot (≤4MB decoded)
 * OR raw slip `text` (paste path + hermetic tests — skips OCR entirely).
 * OCR is tesseract.js (self-hosted WASM, zero API spend); parsing is
 * `slipReader.parseSlipText` — extracted values are USER-SLIP values,
 * never written into any market cache.
 *
 * Rate limit: `slips:{user}:{YYYY-MM-DD}` (UTC day) — free 1/day,
 * paid (analyst/desk) 10/day. Redis counter with an in-memory mirror so
 * a degraded Redis still enforces the cap within this process.
 */

const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { cacheGet, cacheSet } = require('../utils/redis');
const slipReader = require('../services/slipReader');

const router = express.Router();

const MAX_IMAGE_BYTES = 4 * 1024 * 1024; // 4MB decoded
const MAX_TEXT_CHARS = 20_000;
const COUNTER_TTL_S = 48 * 3600; // outlives the UTC day it counts
const MEM_MAX = 20_000;

const mem = new Map(); // dayKey → count (in-memory mirror / fallback)

function dailyLimit(tier) {
  return tier === 'free' ? 1 : 10;
}

function dayKey(userId, now = new Date()) {
  return `slips:${userId}:${now.toISOString().slice(0, 10)}`;
}

/**
 * Consume one parse slot for the user's UTC day. Uses max(redis, memory)
 * as the used count so neither store resetting under-counts alone.
 */
async function takeSlot(user, deps = {}) {
  const get = deps.cacheGet || cacheGet;
  const set = deps.cacheSet || cacheSet;
  const limit = dailyLimit(user.tier || 'free');
  const key = dayKey(user.id, deps.now);

  let redisUsed = 0;
  try {
    const v = await get(key);
    redisUsed = Number(v) || 0;
  } catch { /* degraded redis → memory mirror carries it */ }

  const used = Math.max(redisUsed, mem.get(key) || 0);
  if (used >= limit) return { ok: false, used, limit };

  if (mem.size > MEM_MAX) {
    const oldest = mem.keys().next().value;
    if (oldest !== undefined) mem.delete(oldest);
  }
  mem.set(key, used + 1);
  try { await set(key, used + 1, COUNTER_TTL_S); } catch { /* best effort */ }
  return { ok: true, used: used + 1, limit };
}

/** Decode a base64 / data-URL image field into a Buffer, or null. */
function decodeImage(image) {
  if (typeof image !== 'string' || !image) return null;
  const b64 = image.replace(/^data:image\/[a-z+.-]+;base64,/i, '').replace(/\s/g, '');
  if (!/^[A-Za-z0-9+/=]+$/.test(b64)) return null;
  try {
    const buf = Buffer.from(b64, 'base64');
    return buf.length > 0 ? buf : null;
  } catch {
    return null;
  }
}

router.post('/parse', requireAuth, async (req, res) => {
  const { image, text, book } = req.body || {};

  // ── Validate input BEFORE burning a daily slot ──
  let buffer = null;
  let rawText = null;
  if (typeof text === 'string' && text.trim()) {
    if (text.length > MAX_TEXT_CHARS) {
      return res.status(400).json({ error: 'Slip text too long.' });
    }
    rawText = text;
  } else if (image != null) {
    buffer = decodeImage(image);
    if (!buffer) {
      return res.status(400).json({ error: 'Could not decode the image. Send base64 or a data URL.' });
    }
    if (buffer.length > MAX_IMAGE_BYTES) {
      return res.status(400).json({ error: 'Image too large. 4MB max.' });
    }
  } else {
    return res.status(400).json({ error: 'Send an image (base64) or slip text.' });
  }

  // ── Daily quota ──
  const slot = await takeSlot(req.user, router.__deps || {});
  if (!slot.ok) {
    res.set('X-Slips-Used', String(slot.used));
    res.set('X-Slips-Limit', String(slot.limit));
    return res.status(429).json({
      error: slot.limit === 1
        ? 'One slip read per day on the free tier. Upgrade for ten.'
        : 'Daily slip-read limit reached.',
      used: slot.used,
      limit: slot.limit,
      tier: req.user.tier || 'free',
    });
  }

  // ── OCR (image path only) ──
  if (rawText == null) {
    try {
      const recognize = (router.__deps && router.__deps.recognizeImage) || slipReader.recognizeImage;
      rawText = await recognize(buffer);
    } catch (err) {
      console.error('[slips] OCR failed:', err.message);
      return res.status(503).json({ error: 'The reader could not process this image. Try again or paste the slip text.' });
    }
  }

  // ── Parse. Values are user-slip values; unreadable fields stay null. ──
  const result = slipReader.parseSlipText(rawText, book);
  res.set('X-Slips-Used', String(slot.used));
  res.set('X-Slips-Limit', String(slot.limit));
  return res.json(result);
});

// Test hooks — injectable deps + counter reset (same pattern as scanLimit).
router.__internals = {
  mem,
  dayKey,
  takeSlot,
  dailyLimit,
  decodeImage,
  resetForTests: () => mem.clear(),
  setDeps: (deps) => { router.__deps = deps; },
};

module.exports = router;
