import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * Pintu autentikasi di depan router: memeriksa header Authorization sebelum
 * permintaan diteruskan ke handler target. Token berformat
 * "nama:saldo:tandaTangan" dengan tanda tangan HMAC-SHA256 atas muatan.
 * Permintaan tanpa token, dengan token palsu, atau dari token yang saldonya
 * habis dijawab 401 Unauthorized tanpa pernah menyentuh router.
 */
public class FilterAuth implements HttpHandler {

    private final HttpHandler target;
    private final String kunciRahasia;

    /**
     * Saldo tersisa per token. Handler bawaan HttpServer berjalan sekuensial
     * pada satu thread dispatcher sehingga HashMap cukup; bila server
     * dipasang executor, ganti ke ConcurrentHashMap.
     */
    private final Map<String, Integer> sisaSaldo = new HashMap<>();

    public FilterAuth(HttpHandler target, String kunciRahasia) {
        this.target = target;
        this.kunciRahasia = kunciRahasia;
    }

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        try {
            verifikasi(exchange);
        } catch (ApiException e) {
            tolak(exchange, e.status(), e.getMessage());
            return;   // permintaan ditolak: router tidak dipanggil
        }
        target.handle(exchange);   // router punya penanganan galat dan penutupan exchange sendiri
    }

    /**
     * Memeriksa token akses pada header Authorization.
     * Melempar ApiException 401 bila permintaan tidak sah.
     */
    private void verifikasi(HttpExchange ex) {
        String header = ex.getRequestHeaders().getFirst("Authorization");
        if (header == null || header.isBlank()) {
            throw new ApiException(401, "header Authorization wajib berisi token akses");
        }
        if (!header.startsWith("Bearer ")) {
            throw new ApiException(401, "header Authorization harus berformat 'Bearer <token>',"
                    + " diterima: '" + header + "'");
        }
        String token = header.substring("Bearer ".length()).strip();

        // Tanda tangan heksadesimal tidak memuat ':', jadi titik dua terakhir
        // pasti menjadi pemisah antara muatan dan tanda tangan.
        int posisiTanda = token.lastIndexOf(':');
        if (posisiTanda < 0) {
            throw new ApiException(401, "token tidak berformat nama:saldo:tandaTangan");
        }
        String muatan = token.substring(0, posisiTanda);
        String tanda = token.substring(posisiTanda + 1);
        if (!TandaTangan.cocok(kunciRahasia, muatan, tanda)) {
            throw new ApiException(401, "tanda tangan token tidak cocok");
        }

        String[] bagi = muatan.split(":", 2);
        if (bagi.length != 2 || bagi[0].isBlank()) {
            throw new ApiException(401, "muatan token tidak berformat nama:saldo");
        }
        int saldoAwal;
        try {
            saldoAwal = Integer.parseInt(bagi[1]);
        } catch (NumberFormatException e) {
            throw new ApiException(401, "saldo pada muatan token bukan angka");
        }

        // Hanya permintaan bertoken sah yang mengurangi saldo.
        int sisa = sisaSaldo.getOrDefault(token, saldoAwal);
        if (sisa <= 0) {
            throw new ApiException(401, "saldo token '" + bagi[0] + "' habis");
        }
        sisaSaldo.put(token, sisa - 1);
    }

    /**
     * Menulis respons penolakan: body JSON, header WWW-Authenticate yang
     * diwajibkan RFC 7235 untuk setiap respons 401, lalu menutup exchange.
     */
    private static void tolak(HttpExchange ex, int status, String pesan) throws IOException {
        ex.getRequestBody().readAllBytes();   // buang body belum terbaca agar koneksi tetap sehat
        byte[] isi = JsonMini.error(pesan).getBytes(StandardCharsets.UTF_8);
        ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
        ex.getResponseHeaders().set("WWW-Authenticate", "Bearer realm=\"api-tugas\"");
        ex.sendResponseHeaders(status, isi.length);
        try (OutputStream os = ex.getResponseBody()) {
            os.write(isi);
        } finally {
            ex.close();
        }
    }
}
