import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class AutentikasiOtorisasi {

    // ---- Sisi server: basis data pengguna. Peran TIDAK PERNAH dibaca dari klien. ----
    record Pengguna(String nama, String sandiTersimpan, List<String> peran) { }

    // Authentication yang berhasil: identitas + daftar otoritas.
    // Peran "USER" disimpan sebagai otoritas "ROLE_USER" (konvensi Spring Security).
    record Autentikasi(String nama, List<String> otoritas) {

        boolean punyaOtoritas(String o) {
            return otoritas.contains(o);
        }

        boolean punyaPeran(String p) {
            return otoritas.contains("ROLE_" + p);
        }
    }

    // Padanan UserDetailsService: mencari pengguna berdasarkan nama.
    static class LayananPengguna {

        private final Map<String, Pengguna> tabel = new LinkedHashMap<>();

        LayananPengguna() {
            tabel.put("budi", new Pengguna("budi", hash("kopiGayo123"), List.of("USER")));
            tabel.put("siti", new Pengguna("siti", hash("santiSiti456"), List.of("USER", "EDITOR")));
            tabel.put("admin", new Pengguna("admin", hash("rahasiaAdmin789"), List.of("USER", "ADMIN")));
        }

        Pengguna cari(String nama) {
            return tabel.get(nama);
        }
    }

    // Padanan AuthenticationManager + DaoAuthenticationProvider.
    static class PengelolaAutentikasi {

        private final LayananPengguna layanan;

        PengelolaAutentikasi(LayananPengguna layanan) {
            this.layanan = layanan;
        }

        // null = gagal (401); non-null = identitas terbentuk.
        Autentikasi autentikasi(String nama, String sandi) {
            Pengguna p = layanan.cari(nama);
            if (p == null || !p.sandiTersimpan().equals(hash(sandi))) {
                return null;
            }
            List<String> otoritas = new ArrayList<>();
            for (String peran : p.peran()) {
                otoritas.add("ROLE_" + peran);
            }
            return new Autentikasi(nama, otoritas);
        }
    }

    // Padanan authorizeHttpRequests: aturan akses per pola path.
    static class KebijakanAkses {

        String periksa(Autentikasi a, String path) {
            if (path.startsWith("/api/admin/")) {
                if (a == null) return "401";
                return a.punyaPeran("ADMIN") ? "200" : "403";
            }
            if (path.startsWith("/api/artikel/sunting/")) {
                if (a == null) return "401";
                return (a.punyaPeran("ADMIN") || a.punyaOtoritas("ROLE_EDITOR")) ? "200" : "403";
            }
            return "200"; // path publik
        }
    }

    // Versi RENTAN: keputusan akses memercayai header dari klien.
    static String periksaRentan(String headerPeran, String path) {
        return "ADMIN".equals(headerPeran) || !path.startsWith("/api/admin/") ? "200" : "403";
    }

    public static void main(String[] args) {
        PengelolaAutentikasi pengelola = new PengelolaAutentikasi(new LayananPengguna());
        KebijakanAkses kebijakan = new KebijakanAkses();

        System.out.println("== Tahap 1: Authentication (siapa Anda?) ==");
        Autentikasi budi = pengelola.autentikasi("budi", "kopiGayo123");
        Autentikasi salahSandi = pengelola.autentikasi("budi", "tebakan-salah");
        Autentikasi takDikenal = pengelola.autentikasi("hacker", "apa-pun");
        System.out.println("budi + sandi benar        -> " + ringkas(budi));
        System.out.println("budi + sandi salah        -> " + ringkas(salahSandi));
        System.out.println("pengguna tak dikenal      -> " + ringkas(takDikenal));
        System.out.println();

        System.out.println("== Tahap 2: Authorization (apa yang boleh?) ==");
        uji(kebijakan, budi, "/api/produk");
        uji(kebijakan, budi, "/api/admin/log");
        Autentikasi admin = pengelola.autentikasi("admin", "rahasiaAdmin789");
        uji(kebijakan, admin, "/api/admin/log");
        Autentikasi siti = pengelola.autentikasi("siti", "santiSiti456");
        uji(kebijakan, siti, "/api/artikel/sunting/12");
        uji(kebijakan, budi, "/api/artikel/sunting/12");
        uji(kebijakan, null, "/api/produk");
        uji(kebijakan, null, "/api/admin/log");
        System.out.println();

        System.out.println("== Tahap 3: jebakan peran dari klien ==");
        System.out.println("Request budi menyertakan header X-Role: ADMIN ke /api/admin/log");
        System.out.println("Versi rentan (percaya header) -> respons " + periksaRentan("ADMIN", "/api/admin/log"));
        System.out.println("Versi aman (peran dari server) -> respons " + kebijakan.periksa(budi, "/api/admin/log"));
    }

    // Fungsi hash tiruan agar sandi polos tidak pernah disimpan/dibandingkan langsung.
    // Chapter Password Encoding membedah algoritma yang tepat untuk kata sandi.
    static String hash(String sandi) {
        try {
            byte[] cerna = MessageDigest.getInstance("SHA-256")
                    .digest(sandi.getBytes(StandardCharsets.UTF_8));
            return "sha256$" + HexFormat.of().formatHex(cerna);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 wajib tersedia di JVM", e);
        }
    }

    static void uji(KebijakanAkses k, Autentikasi a, String path) {
        String siapa = a == null ? "anonim" : a.nama() + " " + a.otoritas();
        System.out.printf("%-40s %-26s -> %s%n", siapa, path, k.periksa(a, path));
    }

    static String ringkas(Autentikasi a) {
        return a == null ? "GAGAL (lempar AuthenticationException -> 401)"
                : "BERHASIL: nama=" + a.nama() + ", otoritas=" + a.otoritas();
    }
}
