import crypto from 'crypto';
import { Request, Response } from 'express';
import { getConnectionManager } from 'typeorm';
import { config } from '../config';
import { deleteStationEntry, getStationEntry, setStationEntry, StationKVEntry } from '../nats/StationKV';
import { ScanStation } from '../models/entities/ScanStation';
import authchecker from './authchecker';

/**
 * Computes the HMAC-SHA256 of the provided token using the station token secret.
 */
function computeHmac(token: string): string {
    return crypto.createHmac('sha256', config.station_token_secret).update(token).digest('hex');
}

/**
 * Constant-time comparison of two hex HMAC strings.
 * Returns true if they match.
 */
function verifyHmac(provided_token: string, storedHash: string): boolean {
    const expectedHash = computeHmac(provided_token);
    const expectedBuf = Buffer.from(expectedHash);
    const storedBuf = Buffer.from(storedHash);
    return expectedBuf.length === storedBuf.length && crypto.timingSafeEqual(expectedBuf, storedBuf);
}

/**
 * This middleware handles the authentication of scan station api tokens.
 * The tokens have to be provided via Bearer authorization header.
 *
 * Auth flow:
 *   1. Extract prefix from token (PREFIX.KEY format)
 *   2. Try NATS KV cache lookup by prefix — warm path: HMAC verify, no DB
 *   3. On cache miss: DB lookup → HMAC verify → write to KV cache
 *   4. On no station match at all: fall back to JWT auth (SCAN:CREATE permission)
 *
 * On success sets req.isStationAuth = true and req.stationId on the request object.
 * These are internal server-side properties — not HTTP headers, not spoofable by clients.
 *
 * You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
 * @param req Express request object.
 * @param res Express response object.
 * @param next Next function to call on success.
 */
const ScanAuth = async (req: Request, res: Response, next: () => void) => {
    let provided_token: string = req.headers['authorization'];
    if (!provided_token) {
        res.status(401).send({ http_code: 401, short: 'no_token', message: 'No api token provided.' });
        return;
    }

    provided_token = provided_token.replace('Bearer ', '');

    const prefix = provided_token.split('.')[0];
    if (!prefix) {
        res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
        return;
    }

    // --- KV cache lookup (warm path) ---
    const cached = await getStationEntry(prefix);
    if (cached) {
        if (!cached.enabled) {
            res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
            return;
        }
        if (!verifyHmac(provided_token, cached.tokenHash)) {
            res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
            return;
        }
        req.isStationAuth = true;
        req.stationId = cached.id;
        next();
        return;
    }

    // --- DB lookup (cold path) ---
    const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix }, { relations: ['track'] });

    if (!station) {
        // No station with this prefix — fall back to JWT auth
        let user_authorized = false;
        try {
            const action = { request: req, response: res, context: null, next };
            user_authorized = await authchecker(action, ['SCAN:CREATE']);
        } finally {
            if (!user_authorized) {
                res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
                return;
            }
            next();
        }
        return;
    }

    // Station found — verify token before caching
    const tokenHash = computeHmac(provided_token);
    const storedBuf = Buffer.from(station.key);
    const computedBuf = Buffer.from(tokenHash);
    const valid = computedBuf.length === storedBuf.length && crypto.timingSafeEqual(computedBuf, storedBuf);

    if (!valid) {
        res.status(401).send({ http_code: 401, short: 'invalid_token', message: 'Api token non-existent or invalid syntax.' });
        return;
    }

    if (!station.enabled) {
        res.status(401).send({ http_code: 401, short: 'station_disabled', message: 'Station is disabled.' });
        return;
    }

    // Write to KV cache for subsequent requests
    const entry: StationKVEntry = {
        id: station.id,
        enabled: station.enabled,
        tokenHash,
        trackId: station.track.id,
        trackDistance: station.track.distance,
        minimumLapTime: station.track.minimumLapTime ?? 0,
    };
    await setStationEntry(prefix, entry);

    req.isStationAuth = true;
    req.stationId = station.id;
    next();
};

export default ScanAuth;
export { deleteStationEntry };
