import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class JadwalVirtual {

    static final DateTimeFormatter JAM =
            DateTimeFormatter.ofPattern("HH:mm:ss.SSS", Locale.ROOT);

    // Satu tugas terjadwal: jenis, periode, durasi eksekusi, dan jadwal berikutnya.
    static class Tugas {

        final String nama;
        final String jenis;   // "fixedRate" | "fixedDelay" | "cron"
        final long periodeMs;
        final long durasiMs;
        final String polaCron;   // hanya untuk jenis cron
        long berikutnya;         // waktu jadwal berikutnya (ms virtual)
        int eksekusi = 0;

        Tugas(String nama, String jenis, long periodeMs, long durasiMs, String polaCron) {
            this.nama = nama;
            this.jenis = jenis;
            this.periodeMs = periodeMs;
            this.durasiMs = durasiMs;
            this.polaCron = polaCron;
            this.berikutnya = 0;   // semua tugas dijadwalkan pada 08:00:00.000
        }
    }

    // Cron mini: hanya field detik pola "*/n" (lainnya wajib "*").
    static long cronBerikutnya(String pola, long kini) {
        String[] field = pola.split(" ");
        for (int i = 1; i < field.length; i++) {
            if (!field[i].equals("*")) {
                throw new IllegalArgumentException("contoh hanya mendukung */n pada detik");
            }
        }
        long n = Long.parseLong(field[0].substring(2)) * 1000;
        return (kini / n + 1) * n;   // kelipatan n BERIKUTNYA setelah kini
    }

    public static void main(String[] args) {
        List<Tugas> daftar = new ArrayList<>();
        daftar.add(new Tugas("sinkron-stok", "fixedRate", 10_000, 2_000, null));
        daftar.add(new Tugas("arsip-email", "fixedDelay", 10_000, 6_000, null));
        daftar.add(new Tugas("metrik-kirim", "cron", 0, 2_000, "*/15 * * * * *"));

        System.out.println("Scheduler virtual 1 thread, jendela 08:00:00 - 08:01:00");
        System.out.println("sinkron-stok : fixedRate 10s, durasi 2s (dihitung dari AWAL)");
        System.out.println("arsip-email  : fixedDelay 10s, durasi 6s (dihitung dari AKHIR)");
        System.out.println("metrik-kirim : cron \"*/15 * * * * *\", durasi 2s");
        System.out.println();

        long batas = 60_000;
        long jam = 0;   // jam virtual dalam milidetik sejak 08:00:00.000

        while (true) {
            // Pilih tugas dengan jadwal terkecil; seri mengikuti urutan pendaftaran.
            Tugas terpilih = null;
            for (Tugas t : daftar) {
                if (t.berikutnya >= batas) {
                    continue;
                }
                if (terpilih == null || t.berikutnya < terpilih.berikutnya) {
                    terpilih = t;
                }
            }
            if (terpilih == null) {
                break;
            }

            // Eksekusi berjalan saat thread bebas, bukan saat jadwal jatuh.
            long mulai = Math.max(jam, terpilih.berikutnya);
            long keterlambatan = mulai - terpilih.berikutnya;
            long selesai = mulai + terpilih.durasiMs;
            jam = selesai;

            System.out.printf("[%s] MULAI   %s%s%n", fmt(mulai), terpilih.nama,
                    keterlambatan > 0 ? "  (antre " + keterlambatan + " ms)" : "");
            System.out.printf("[%s] SELESAI %s%n", fmt(selesai), terpilih.nama);
            terpilih.eksekusi++;

            terpilih.berikutnya = switch (terpilih.jenis) {
                case "fixedRate" -> mulai + terpilih.periodeMs;
                case "fixedDelay" -> selesai + terpilih.periodeMs;
                case "cron" -> cronBerikutnya(terpilih.polaCron, selesai);
                default -> throw new IllegalStateException();
            };
        }

        System.out.println();
        System.out.println("Rekap dalam 60 detik:");
        for (Tugas t : daftar) {
            System.out.printf("  %-12s %2d eksekusi%n", t.nama, t.eksekusi);
        }
    }

    static String fmt(long ms) {
        // 08:00:00.000 + ms (jam virtual beku, bukan jam dinding).
        return LocalTime.ofNanoOfDay((8 * 3600_000L + ms) * 1_000_000).format(JAM);
    }
}
