package example;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public final class ExceptionSemanticsDemo {
    private static final long WAIT_SECONDS = 2L;

    private ExceptionSemanticsDemo() {
    }

    static final class StorageFailure extends Exception {
        private static final long serialVersionUID = 1L;

        StorageFailure(String message) {
            super(message);
        }
    }

    static final class ApplicationFailure extends RuntimeException {
        private static final long serialVersionUID = 1L;

        ApplicationFailure(String message, Throwable cause) {
            super(message, cause);
        }

        ApplicationFailure(String message) {
            super(message);
        }
    }

    static final class FailingResource implements AutoCloseable {
        private boolean closed;

        void operate() throws StorageFailure {
            throw new StorageFailure("body failed");
        }

        boolean isClosed() {
            return closed;
        }

        @Override
        public void close() throws IOException {
            closed = true;
            throw new IOException("close failed");
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println(
                    "usage=preserved|lost-cause|restored-interrupt|swallowed-interrupt");
            System.exit(64);
        }

        switch (args[0]) {
            case "preserved" -> runPreserved();
            case "lost-cause" -> runLostCause();
            case "restored-interrupt" -> runRestoredInterrupt();
            case "swallowed-interrupt" -> runSwallowedInterrupt();
            default -> {
                System.err.println("unknown-mode=" + args[0]);
                System.exit(64);
            }
        }
    }

    private static void runPreserved() {
        FailingResource resource = new FailingResource();
        try {
            translatePreservingEvidence(resource);
            throw new AssertionError("translated failure was not thrown");
        } catch (ApplicationFailure translated) {
            Throwable cause = translated.getCause();
            if (!(cause instanceof StorageFailure)) {
                throw new AssertionError("StorageFailure cause was not preserved");
            }
            Throwable[] suppressed = cause.getSuppressed();
            if (suppressed.length != 1 || !(suppressed[0] instanceof IOException)) {
                throw new AssertionError("IOException was not preserved as suppressed");
            }
            if (!resource.isClosed()) {
                throw new AssertionError("resource was not closed");
            }

            System.out.println("mode=preserved");
            System.out.println("cause=" + cause.getClass().getSimpleName());
            System.out.println("suppressed="
                    + suppressed[0].getClass().getSimpleName());
            System.out.println("resource-closed=" + resource.isClosed());
        }
    }

    private static void runLostCause() {
        StorageFailure original = new StorageFailure("body failed");
        ApplicationFailure broken = new ApplicationFailure(original.getMessage());
        if (broken.getCause() != null) {
            throw new AssertionError("lost-cause probe unexpectedly retained a cause");
        }
        System.err.println("mode=lost-cause");
        System.err.println("cause=null");
        System.err.println("rejected=missing-cause");
        System.exit(2);
    }

    private static void runRestoredInterrupt() throws InterruptedException {
        CountDownLatch reachedBlockingPoint = new CountDownLatch(1);
        AtomicBoolean restored = new AtomicBoolean();
        Thread worker = new Thread(() -> {
            reachedBlockingPoint.countDown();
            try {
                Thread.sleep(TimeUnit.SECONDS.toMillis(10));
                throw new AssertionError("blocking call was not interrupted");
            } catch (InterruptedException expected) {
                Thread.currentThread().interrupt();
                restored.set(Thread.currentThread().isInterrupted());
            }
        }, "restored-interrupt-worker");
        worker.setDaemon(true);
        worker.start();

        if (!reachedBlockingPoint.await(WAIT_SECONDS, TimeUnit.SECONDS)) {
            worker.interrupt();
            throw new AssertionError("worker did not reach the blocking point");
        }
        worker.interrupt();
        worker.join(TimeUnit.SECONDS.toMillis(WAIT_SECONDS));
        if (worker.isAlive()) {
            worker.interrupt();
            throw new AssertionError("restored-interrupt worker did not exit");
        }
        if (!restored.get()) {
            throw new AssertionError("interrupt status was not restored");
        }

        System.out.println("mode=restored-interrupt");
        System.out.println("reached-blocking-point=true");
        System.out.println("restored-interrupt=" + restored.get());
        System.out.println("worker-exited=true");
    }

    private static void runSwallowedInterrupt() throws InterruptedException {
        CountDownLatch reachedBlockingPoint = new CountDownLatch(1);
        AtomicBoolean statusAfterCatch = new AtomicBoolean(true);
        Thread worker = new Thread(() -> {
            reachedBlockingPoint.countDown();
            try {
                Thread.sleep(TimeUnit.SECONDS.toMillis(10));
                throw new AssertionError("blocking call was not interrupted");
            } catch (InterruptedException ignored) {
                statusAfterCatch.set(Thread.currentThread().isInterrupted());
            }
        }, "swallowed-interrupt-worker");
        worker.setDaemon(true);
        worker.start();

        if (!reachedBlockingPoint.await(WAIT_SECONDS, TimeUnit.SECONDS)) {
            worker.interrupt();
            throw new AssertionError("worker did not reach the blocking point");
        }
        worker.interrupt();
        worker.join(TimeUnit.SECONDS.toMillis(WAIT_SECONDS));
        if (worker.isAlive()) {
            worker.interrupt();
            throw new AssertionError("swallowed-interrupt worker did not exit");
        }
        if (statusAfterCatch.get()) {
            throw new AssertionError("swallowed interrupt unexpectedly remained set");
        }

        System.err.println("mode=swallowed-interrupt");
        System.err.println("interrupt-after-catch=false");
        System.err.println("worker-exited=true");
        System.err.println("rejected=interrupt-lost");
        System.exit(3);
    }

    private static void translatePreservingEvidence(FailingResource resource) {
        try (resource) {
            resource.operate();
        } catch (StorageFailure primary) {
            throw new ApplicationFailure("reservation unavailable", primary);
        } catch (IOException closeOnly) {
            throw new AssertionError("body failure was unexpectedly absent", closeOnly);
        }
    }
}
