import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.LongSupplier;

public final class CancellationLab {
    private CancellationLab() { }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) throw new IllegalArgumentException("choose a documented mode");
        switch (args[0]) {
            case "flags" -> flags();
            case "before" -> before();
            case "completed" -> completed();
            case "cooperative", "ignored", "cancel-false", "timeout" -> running(args[0]);
            case "wait-reacquire" -> waitReacquire();
            case "deadline" -> deadline();
            case "shutdown" -> shutdown();
            default -> throw new IllegalArgumentException("unknown mode: " + args[0]);
        }
    }

    private static void flags() throws InterruptedException {
        Thread self = Thread.currentThread();
        self.interrupt();
        require(self.isInterrupted(), "flag was not set");
        boolean consumed = Thread.interrupted();
        require(consumed && !self.isInterrupted(), "interrupted must clear");
        System.out.println("read=true consumed=true after-clear=false");
        self.interrupt();
        try {
            Thread.sleep(1);
            throw new IllegalStateException("sleep should throw");
        } catch (InterruptedException expected) {
            require(!self.isInterrupted(), "sleep should clear the flag");
            System.out.println("sleep=InterruptedException flag=false");
        }
        self.interrupt();
        try {
            LockSupport.park();
            require(self.isInterrupted(), "park must preserve the flag");
            System.out.println("park=returned flag=true");
        } finally {
            Thread.interrupted();
        }
    }

    private static void before() throws Exception {
        AtomicBoolean ran = new AtomicBoolean();
        FutureTask<Integer> task = new FutureTask<>(() -> {
            ran.set(true);
            return 42;
        });
        require(task.cancel(false), "cancel before run");
        task.run();
        require(!ran.get() && task.isCancelled() && task.isDone(), "body should not run");
        expectCancellation(task);
        System.out.println("accepted=true cancelled=true done=true body-ran=false");
    }

    private static void completed() throws Exception {
        FutureTask<Integer> task = new FutureTask<>(() -> 42);
        task.run();
        require(!task.cancel(true) && task.get() == 42, "completed result must survive");
        System.out.println("result=42 cancel-accepted=false cancelled=false");
    }

    private static void running(String mode) throws Exception {
        CountDownLatch entered = new CountDownLatch(1);
        CountDownLatch release = new CountDownLatch(1);
        CountDownLatch interrupted = new CountDownLatch(1);
        AtomicBoolean cleaned = new AtomicBoolean();
        FutureTask<Integer> task = new FutureTask<>(() -> {
            entered.countDown();
            try {
                while (true) {
                    try {
                        require(release.await(10, TimeUnit.SECONDS), "lab release timeout");
                        return 42;
                    } catch (InterruptedException cancelled) {
                        interrupted.countDown();
                        if (!mode.equals("ignored")) throw cancelled;
                        // Intentional negative example; release still gives the lab a safe exit.
                    }
                }
            } finally {
                cleaned.set(true);
            }
        });
        Thread runner = new Thread(task, "cancel-" + mode);
        runner.setDaemon(true); // Failure containment only; successful modes must join the thread.
        runner.start();
        try {
            await(entered);
            if (mode.equals("timeout")) {
                try {
                    task.get(20, TimeUnit.MILLISECONDS);
                    throw new IllegalStateException("get should time out");
                } catch (TimeoutException expected) {
                    require(!task.isDone() && runner.isAlive(), "get timeout changed work");
                    System.out.println("get=TimeoutException done=false alive=true");
                }
                release.countDown();
                require(task.get(2, TimeUnit.SECONDS) == 42, "task did not complete");
            } else {
                require(task.cancel(!mode.equals("cancel-false")), "running cancel rejected");
                expectCancellation(task);
                if (mode.equals("cooperative")) {
                    await(interrupted);
                    join(runner);
                    require(cleaned.get(), "cleanup missing");
                    System.out.println("cancelled=true alive=false cleaned=true");
                } else {
                    if (mode.equals("ignored")) await(interrupted);
                    require(runner.isAlive() && !cleaned.get(), "work stopped too early");
                    System.out.printf("cancelled=true alive=true cleaned=false interrupt-observed=%s%n",
                            interrupted.getCount() == 0);
                    release.countDown();
                }
            }
        } finally {
            release.countDown();
            runner.join(2000);
            if (runner.isAlive()) {
                runner.interrupt();
                join(runner);
            }
        }
        require(cleaned.get(), "final cleanup missing");
        System.out.println("after-release: alive=false cleaned=true");
    }

    private static void waitReacquire() throws Exception {
        Object monitor = new Object();
        CountDownLatch entered = new CountDownLatch(1);
        AtomicBoolean returned = new AtomicBoolean();
        boolean[] ready = {false}; // Accessed only under monitor.
        Thread waiter = new Thread(() -> {
            synchronized (monitor) {
                entered.countDown();
                try {
                    while (!ready[0]) monitor.wait();
                    returned.set(true);
                } catch (InterruptedException cancelled) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "wait-reacquire");
        waiter.setDaemon(true);
        waiter.start();
        try {
            await(entered);
            synchronized (monitor) {
                ready[0] = true;
                monitor.notifyAll();
                long start = System.nanoTime();
                while (waiter.getState() != Thread.State.BLOCKED
                        && System.nanoTime() - start < TimeUnit.SECONDS.toNanos(2)) {
                    Thread.sleep(1);
                }
                require(waiter.getState() == Thread.State.BLOCKED && !returned.get(),
                        "waiter must reacquire the held monitor");
                System.out.println("notified=true state=BLOCKED wait-returned=false");
            }
            join(waiter);
            require(returned.get(), "wait never returned");
            System.out.println("owner-released=true wait-returned=true");
        } finally {
            synchronized (monitor) {
                ready[0] = true;
                monitor.notifyAll();
            }
            waiter.interrupt();
            join(waiter);
        }
    }

    static final class Deadline {
        private final long start;
        private final long budget;
        private final LongSupplier clock;

        Deadline(long budgetNanos) {
            this(budgetNanos, System::nanoTime);
        }

        Deadline(long budgetNanos, LongSupplier clock) {
            if (budgetNanos <= 0 || budgetNanos > TimeUnit.DAYS.toNanos(1)) {
                throw new IllegalArgumentException("budget must be in (0, 1 day]");
            }
            this.clock = clock;
            this.start = clock.getAsLong();
            this.budget = budgetNanos;
        }

        long remainingNanos() throws TimeoutException {
            long elapsed = clock.getAsLong() - start;
            long remaining = budget - elapsed;
            if (remaining <= 0) throw new TimeoutException("deadline exceeded");
            return remaining;
        }
    }

    private static void deadline() throws Exception {
        long[] now = {0};
        Deadline d = new Deadline(TimeUnit.MILLISECONDS.toNanos(800), () -> now[0]);
        now[0] = TimeUnit.MILLISECONDS.toNanos(700);
        require(d.remainingNanos() == TimeUnit.MILLISECONDS.toNanos(100), "queue budget lost");
        System.out.println("budget=800ms queue=700ms remaining=100ms");
        now[0] = TimeUnit.MILLISECONDS.toNanos(800);
        try {
            d.remainingNanos();
            throw new IllegalStateException("deadline not enforced");
        } catch (TimeoutException expected) {
            System.out.println("remaining=0 action=TimeoutException");
        }
    }

    private static void shutdown() throws Exception {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        CountDownLatch entered = new CountDownLatch(1);
        CountDownLatch release = new CountDownLatch(1);
        AtomicBoolean cleaned = new AtomicBoolean();
        try {
            pool.submit(() -> {
                entered.countDown();
                try {
                    release.await();
                } catch (InterruptedException cancelled) {
                    Thread.currentThread().interrupt();
                } finally {
                    cleaned.set(true);
                }
            });
            await(entered);
            Future<Integer> queued = pool.submit(() -> 42);
            pool.shutdown();
            require(!pool.awaitTermination(20, TimeUnit.MILLISECONDS), "worker still blocked");
            List<Runnable> drained = pool.shutdownNow();
            require(drained.size() == 1 && !queued.isDone(), "unexpected queue state");
            for (Runnable r : drained) {
                if (r instanceof Future<?> future) future.cancel(false);
            }
            require(pool.awaitTermination(2, TimeUnit.SECONDS) && cleaned.get(), "not terminated");
            require(queued.isCancelled(), "drained result remained pending");
            System.out.println("drained=1 queued-cancelled=true worker-cleaned=true terminated=true");
        } finally {
            release.countDown();
            pool.shutdownNow();
            require(pool.awaitTermination(2, TimeUnit.SECONDS), "pool cleanup timeout");
        }
    }

    private static void expectCancellation(Future<?> future) throws Exception {
        try {
            future.get(1, TimeUnit.SECONDS);
            throw new IllegalStateException("expected cancellation");
        } catch (CancellationException expected) {
            require(future.isCancelled(), "not cancelled");
        }
    }

    private static void await(CountDownLatch latch) throws InterruptedException {
        require(latch.await(2, TimeUnit.SECONDS), "worker handshake timeout");
    }

    private static void join(Thread thread) throws InterruptedException {
        thread.join(2000);
        require(!thread.isAlive(), "worker did not stop");
    }

    private static void require(boolean value, String message) {
        if (!value) throw new IllegalStateException(message);
    }
}
