import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
 * Companion for "Top 40 Microservices Interview Questions". Seven resilience and data-consistency
 * patterns in one file, no frameworks, a fake clock so the output is deterministic.
 * Run on JDK 25 (uses ScopedValue, final in 25):  java MicroservicesPatternsLab.java
 */
public class MicroservicesPatternsLab {

    // ---- fake clock so timeouts and refills need no sleeping ------------------------------
    static long now = 0;

    public static void main(String[] args) {
        retryWithBackoff();
        circuitBreaker();
        idempotency();
        saga();
        outbox();
        tokenBucket();
        correlationId();
    }

    // 1. Retry: exponential backoff with full jitter, capped -----------------------------------
    static void retryWithBackoff() {
        System.out.println("[1] retry with exponential backoff + full jitter (seeded Random)");
        Random rnd = new Random(7);
        int[] failuresLeft = {3};
        Supplier<String> flaky = () -> {
            if (failuresLeft[0]-- > 0) throw new IllegalStateException("503 from inventory");
            return "200 OK";
        };
        long base = 100, cap = 1_000;
        for (int attempt = 1; attempt <= 5; attempt++) {
            try {
                System.out.println("    attempt " + attempt + " -> " + flaky.get());
                break;
            } catch (IllegalStateException e) {
                long ceiling = Math.min(cap, base << (attempt - 1));
                long wait = (long) (rnd.nextDouble() * ceiling);
                System.out.println("    attempt " + attempt + " failed (" + e.getMessage()
                        + "), ceiling=" + ceiling + "ms, sleeping " + wait + "ms");
            }
        }
    }

    // 2. Circuit breaker: CLOSED -> OPEN -> HALF_OPEN -> CLOSED -----------------------------------
    enum State { CLOSED, OPEN, HALF_OPEN }

    static class CircuitBreaker {
        State state = State.CLOSED;
        int failures = 0;
        final int threshold;
        final long openMillis;
        long openedAt;
        CircuitBreaker(int threshold, long openMillis) { this.threshold = threshold; this.openMillis = openMillis; }

        String call(Supplier<String> remote) {
            if (state == State.OPEN) {
                if (now - openedAt < openMillis) return "REJECTED (circuit open, no call made)";
                state = State.HALF_OPEN;
            }
            try {
                String r = remote.get();
                failures = 0;
                state = State.CLOSED;
                return r;
            } catch (RuntimeException e) {
                failures++;
                if (state == State.HALF_OPEN || failures >= threshold) { state = State.OPEN; openedAt = now; }
                return "FAILED (" + e.getMessage() + ")";
            }
        }
    }

    static void circuitBreaker() {
        System.out.println("[2] circuit breaker (threshold=3, open for 5000ms)");
        CircuitBreaker cb = new CircuitBreaker(3, 5_000);
        boolean[] healthy = {false};
        Supplier<String> payments = () -> {
            if (!healthy[0]) throw new RuntimeException("timeout");
            return "OK";
        };
        for (int i = 1; i <= 5; i++) {
            String r = cb.call(payments);
            System.out.println("    t=" + now + " call " + i + " -> " + r + "  [state=" + cb.state + "]");
            now += 100;
        }
        now += 5_000;
        healthy[0] = true;
        System.out.println("    ... 5s pass, service recovers ...");
        System.out.println("    t=" + now + " call 6 -> " + cb.call(payments) + "  [state=" + cb.state + "]");
    }

    // 3. Idempotency key: a retried POST must not charge twice ---------------------------------------
    static void idempotency() {
        System.out.println("[3] idempotency key");
        Map<String, String> processed = new ConcurrentHashMap<>();
        int[] charges = {0};
        for (String key : List.of("pay-9f2", "pay-9f2", "pay-1c7", "pay-9f2")) {
            String result = processed.computeIfAbsent(key, k -> { charges[0]++; return "charged#" + charges[0]; });
            System.out.println("    key=" + key + " -> " + result);
        }
        System.out.println("    4 requests, " + charges[0] + " actual charges");
    }

    // 4. Orchestrated saga with compensation --------------------------------------------------------------
    interface Step { String name(); void run(); void undo(); }

    static void saga() {
        System.out.println("[4] orchestrated saga: payment fails, earlier steps are compensated in reverse");
        List<String> log = new ArrayList<>();
        List<Step> steps = List.of(
                step("createOrder", log, false), step("reserveStock", log, false), step("chargePayment", log, true));
        Deque<Step> done = new ArrayDeque<>();
        try {
            for (Step s : steps) { s.run(); done.push(s); }
        } catch (RuntimeException e) {
            log.add("!! " + e.getMessage());
            while (!done.isEmpty()) done.pop().undo();
        }
        log.forEach(l -> System.out.println("    " + l));
    }

    static Step step(String name, List<String> log, boolean fails) {
        return new Step() {
            public String name() { return name; }
            public void run() {
                if (fails) throw new IllegalStateException(name + " declined");
                log.add("do   " + name);
            }
            public void undo() { log.add("undo " + name); }
        };
    }

    // 5. Transactional outbox + idempotent consumer ----------------------------------------------------------------
    record OutboxRow(long id, String payload, boolean published) {}

    static void outbox() {
        System.out.println("[5] outbox: state change and event are ONE local transaction; relay is at-least-once");
        List<String> orders = new ArrayList<>();
        List<OutboxRow> outbox = new ArrayList<>();
        // one "transaction": both writes or neither
        orders.add("order-42");
        outbox.add(new OutboxRow(1, "OrderCreated(order-42)", false));
        System.out.println("    committed: orders=" + orders + " outbox=" + outbox.size() + " row(s)");

        List<String> broker = new ArrayList<>();
        // relay publishes, then crashes BEFORE marking the row published
        for (OutboxRow r : outbox) broker.add(r.id() + ":" + r.payload());
        System.out.println("    relay published, then crashed before marking the row sent");
        // after restart the row is still unpublished, so it is published again
        for (OutboxRow r : outbox) if (!r.published()) broker.add(r.id() + ":" + r.payload());
        System.out.println("    broker now holds " + broker.size() + " copies of the same event");

        Set<Long> seen = new HashSet<>();
        int applied = 0;
        for (String msg : broker) {
            long eventId = Long.parseLong(msg.substring(0, msg.indexOf(':')));
            if (seen.add(eventId)) applied++;
        }
        System.out.println("    consumer dedupes by event id: applied " + applied + " time(s)");
    }

    // 6. Token bucket rate limiter at the gateway -----------------------------------------------------------------------
    static void tokenBucket() {
        System.out.println("[6] token bucket: capacity 5, refill 2 tokens/second");
        double capacity = 5, tokens = 5, refillPerMs = 2.0 / 1000;
        long last = 0;
        long[] arrivals = {0, 0, 0, 0, 0, 0, 0, 500, 1000, 1500, 3000};
        for (long t : arrivals) {
            tokens = Math.min(capacity, tokens + (t - last) * refillPerMs);
            last = t;
            boolean ok = tokens >= 1;
            if (ok) tokens -= 1;
            System.out.printf("    t=%4dms %s  tokens left=%.1f%n", t, ok ? "ALLOW" : "429  ", tokens);
        }
    }

    // 7. Correlation id travels through every hop without being passed as a parameter ------------------------
    static final ScopedValue<String> TRACE = ScopedValue.newInstance();

    static void correlationId() {
        System.out.println("[7] correlation id via ScopedValue across three hops");
        ScopedValue.where(TRACE, "trace-7f3a").run(() -> gateway());
    }
    static void gateway() { log("gateway   received request"); orders(); }
    static void orders() { log("orders    calling inventory"); inventory(); }
    static void inventory() { log("inventory reserved 2 units"); }
    static void log(String m) { System.out.println("    [" + TRACE.get() + "] " + m); }
}
