import { extractBearerToken, verifyToken, TokenError } from './verify.js';

const USER_CACHE_TTL_MS = 60_000;

// Source IPs that bypass token auth — used so the office kiosk can render
// /small without anyone having to log in daily on the device. Synthetic user
// has no permissions, so only endpoints that don't gate on requirePermission()
// are reachable. Requires server.js `trust proxy` setting so req.ip is the
// real client behind Caddy, not 127.0.0.1.
function parseKioskIps(raw) {
  return new Set(
    (raw || '')
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean)
  );
}

function createUserCache() {
  const entries = new Map();
  return {
    get(userId) {
      const hit = entries.get(userId);
      if (!hit) return null;
      if (Date.now() - hit.cachedAt > USER_CACHE_TTL_MS) {
        entries.delete(userId);
        return null;
      }
      return hit.user;
    },
    set(userId, user) {
      entries.set(userId, { user, cachedAt: Date.now() });
    },
    invalidate(userId) {
      entries.delete(userId);
    },
  };
}

async function loadUser(pool, userId) {
  const userResult = await pool.query(
    'SELECT id, username, email, is_admin, is_active FROM users WHERE id = $1',
    [userId]
  );
  const user = userResult.rows[0];
  if (!user) return null;

  if (user.is_admin) {
    user.permissions = [];
    return user;
  }

  const permResult = await pool.query(
    `SELECT p.code
       FROM permissions p
       JOIN user_permissions up ON p.id = up.permission_id
      WHERE up.user_id = $1`,
    [userId]
  );
  user.permissions = permResult.rows.map((r) => r.code);
  return user;
}

export function authenticate({ pool, secret = process.env.JWT_SECRET, kioskIps = process.env.KIOSK_IPS }) {
  const cache = createUserCache();
  const kioskIpSet = parseKioskIps(kioskIps);

  return async function authenticateMiddleware(req, res, next) {
    // Kiosk IP bypass ONLY when no Authorization header was provided. A real
    // user on the same network (e.g. logged-in staff sharing the office NAT)
    // must keep their actual identity and permissions — otherwise this bypass
    // silently downgrades them to the permissionless kiosk user and they get
    // 403 on every gated route.
    if (
      kioskIpSet.size > 0 &&
      kioskIpSet.has(req.ip) &&
      !req.headers.authorization
    ) {
      req.user = {
        id: 'kiosk',
        username: 'kiosk',
        is_admin: false,
        is_active: true,
        permissions: [],
        is_kiosk: true,
      };
      return next();
    }

    let decoded;
    try {
      const token = extractBearerToken(req.headers.authorization);
      decoded = verifyToken(token, secret);
    } catch (err) {
      if (err instanceof TokenError) {
        return res.status(401).json({ error: err.message });
      }
      return res.status(401).json({ error: 'Authentication required' });
    }

    try {
      let user = cache.get(decoded.userId);
      if (!user) {
        user = await loadUser(pool, decoded.userId);
        if (!user) {
          return res.status(401).json({ error: 'User not found' });
        }
        cache.set(decoded.userId, user);
      }
      if (!user.is_active) {
        cache.invalidate(decoded.userId);
        return res.status(403).json({ error: 'Account inactive' });
      }
      req.user = user;
      next();
    } catch (err) {
      next(err);
    }
  };
}

export function requirePermission(code) {
  return function requirePermissionMiddleware(req, res, next) {
    if (!req.user) return res.status(401).json({ error: 'Authentication required' });
    if (req.user.is_admin) return next();
    if (Array.isArray(req.user.permissions) && req.user.permissions.includes(code)) {
      return next();
    }
    res.status(403).json({ error: 'Insufficient permissions' });
  };
}

export function requireAdmin(req, res, next) {
  if (!req.user) return res.status(401).json({ error: 'Authentication required' });
  if (req.user.is_admin) return next();
  res.status(403).json({ error: 'Admin only' });
}
