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.LinkedHashMap;
import java.util.Map;

/**
 * Lapisan web inti telanjang: satu HttpHandler yang menyimpan peta endpoint
 * "METODE /pola" -> kode, lengkap dengan pencocokan pola {id}. Struktur ini
 * adalah miniatur DispatcherServlet Spring MVC: peta handler menggantikan
 * anotasi @GetMapping/@PostMapping, dan satu blok catch di batas menggantikan
 * @ExceptionHandler.
 */
public class RouterWeb implements HttpHandler {

    /** Satu endpoint terdaftar: pola "METODE /api/tugas/{id}". */
    @FunctionalInterface
    public interface Endpoint {

        /** Memproses satu permintaan; parameter berisi nilai {id} hasil cocok pola. */
        ResponsWeb tangani(Map<String, String> parameter, String body);
    }

    /** Respons siap kirim: status HTTP, body JSON, dan Location untuk 201. */
    public record ResponsWeb(int status, String json, String lokasi) {

        static ResponsWeb ok(String json) {
            return new ResponsWeb(200, json, null);
        }

        static ResponsWeb dibuat(TugasWeb tugas) {
            return new ResponsWeb(201, JsonWeb.tugas(tugas), "/api/tugas/" + tugas.id());
        }

        static ResponsWeb tanpaIsi() {
            return new ResponsWeb(204, null, null);
        }
    }

    private final LayananTugasWeb layanan;
    private final Map<String, Endpoint> rute = new LinkedHashMap<>();

    public RouterWeb(LayananTugasWeb layanan) {
        this.layanan = layanan;

        rute.put("GET /api/tugas", (p, b) -> ResponsWeb.ok(JsonWeb.daftar(layanan.daftar())));
        rute.put("POST /api/tugas", (p, b) -> ResponsWeb.dibuat(layanan.buat(bacaPermintaan(b))));
        rute.put("GET /api/tugas/{id}", (p, b) ->
                ResponsWeb.ok(JsonWeb.tugas(layanan.cari(id(p)))));
        rute.put("PUT /api/tugas/{id}", (p, b) ->
                ResponsWeb.ok(JsonWeb.tugas(layanan.ubah(id(p), bacaPermintaan(b)))));
        rute.put("POST /api/tugas/{id}/selesai", (p, b) ->
                ResponsWeb.ok(JsonWeb.tugas(layanan.tandaiSelesai(id(p)))));
        rute.put("DELETE /api/tugas/{id}", (p, b) -> {
            layanan.hapus(id(p));
            return ResponsWeb.tanpaIsi();
        });
    }

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // Body dibaca selalu agar koneksi sehat walau permintaan ditolak.
        String body = new String(exchange.getRequestBody().readAllBytes(),
                StandardCharsets.UTF_8);
        String metode = exchange.getRequestMethod();
        String path = exchange.getRequestURI().getPath();
        try {
            kirim(exchange, cocokkan(metode, path, body));
        } catch (ValidasiGagalException e) {
            kirim(exchange, new ResponsWeb(400,
                    JsonWeb.galat(400, "validasi gagal", e.galat()), null));
        } catch (ApiException e) {
            kirim(exchange, new ResponsWeb(e.status(),
                    JsonWeb.galat(e.status(), e.getMessage()), null));
        } catch (IllegalArgumentException e) {
            kirim(exchange, new ResponsWeb(400,
                    JsonWeb.galat(400, e.getMessage()), null));
        } catch (RuntimeException e) {
            kirim(exchange, new ResponsWeb(500,
                    JsonWeb.galat(500, "kegagalan tak terduga: " + e), null));
        }
    }

    /** Mencocokkan metode dan path dengan peta rute; 405 bila path dikenal
     *  dengan metode lain, 404 bila path memang tidak terdaftar. */
    private ResponsWeb cocokkan(String metode, String path, String body) {
        for (Map.Entry<String, Endpoint> rute : this.rute.entrySet()) {
            String[] kunci = rute.getKey().split(" ", 2);
            Map<String, String> parameter = cocokkanPola(kunci[1], path);
            if (parameter != null && kunci[0].equals(metode)) {
                return rute.getValue().tangani(parameter, body);
            }
        }
        if (pathTerdaftar(path)) {
            throw new ApiException(405, "metode " + metode + " tidak didukung untuk " + path);
        }
        throw new ApiException(404, "endpoint tidak dikenal: " + metode + " " + path);
    }

    /** Mencocokkan pola bersegmen dengan path; {id} menyerap satu segmen angka. */
    private static Map<String, String> cocokkanPola(String pola, String path) {
        String[] segPola = pola.split("/");
        String[] segPath = path.split("/");
        if (segPola.length != segPath.length) {
            return null;
        }
        Map<String, String> parameter = new LinkedHashMap<>();
        for (int i = 0; i < segPola.length; i++) {
            String p = segPola[i];
            if (p.startsWith("{") && p.endsWith("}")) {
                if (!segPath[i].matches("\\d+")) {
                    return null;
                }
                parameter.put(p.substring(1, p.length() - 1), segPath[i]);
            } else if (!p.equals(segPath[i])) {
                return null;
            }
        }
        return parameter;
    }

    /** Apakah path cocok dengan pola rute mana pun (metode apa pun). */
    private boolean pathTerdaftar(String path) {
        for (String kunci : rute.keySet()) {
            if (cocokkanPola(kunci.split(" ", 2)[1], path) != null) {
                return true;
            }
        }
        return false;
    }

    /** Id numerik dari parameter hasil cocok pola. */
    private static int id(Map<String, String> parameter) {
        return Integer.parseInt(parameter.get("id"));
    }

    /** Membaca body JSON menjadi PermintaanTugas. */
    private static PermintaanTugas bacaPermintaan(String body) {
        if (body == null || body.isBlank()) {
            throw new ValidasiGagalException("body permintaan wajib berisi JSON objek");
        }
        Map<String, Object> data = JsonWeb.parseObjek(body);
        return new PermintaanTugas(teks(data, "judul"), teks(data, "pemilik"),
                teks(data, "tenggat"));
    }

    /** Nilai field teks; null bila field hilang, 400 bila bukan teks. */
    private static String teks(Map<String, Object> data, String field) {
        Object nilai = data.get(field);
        if (nilai == null) {
            return null;
        }
        if (nilai instanceof String s) {
            return s;
        }
        throw new ApiException(400, "field " + field + " harus berupa teks");
    }

    /** Menulis respons: body null berarti 204 tanpa isi. */
    private static void kirim(HttpExchange exchange, ResponsWeb respons) throws IOException {
        if (respons.json() == null) {
            exchange.sendResponseHeaders(respons.status(), -1);
            exchange.close();
            return;
        }
        byte[] isi = respons.json().getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
        if (respons.lokasi() != null) {
            exchange.getResponseHeaders().set("Location", respons.lokasi());
        }
        exchange.sendResponseHeaders(respons.status(), isi.length);
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(isi);
        } finally {
            exchange.close();
        }
    }
}
