package com.example.support;

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Service virtualization (L3git): a tiny in-process HTTP stub server built on the JDK's
 * {@code com.sun.net.httpserver} - no extra dependency. Register canned responses per method+path,
 * point the API base URI at {@link #baseUri()}, and run tests against a dependency that isn't built
 * or available yet. Bind to port 0 for an OS-assigned free port so parallel scenarios never clash.
 *
 * Advanced tier adds STATEFUL virtualization:
 *   - {@link #stubSequence} returns a different response on each call (call 1 -> 202, call 2 -> 200 ...)
 *     then repeats the last - for polling / eventual-consistency / retry flows.
 *   - {@link #stubStateful} makes a path behave like a real resource: POST/PUT stores the request body,
 *     GET returns it (404 until created), DELETE clears it - for create-then-read round trips.
 * Generated / shipped by L3git.
 */
public class MockService {

    private HttpServer server;
    private int port;
    private final Map<String, Stub> stubs = new ConcurrentHashMap<>();
    private final Set<String> statefulPaths = ConcurrentHashMap.newKeySet();       // paths handled as live resources
    private final Map<String, String> state = new ConcurrentHashMap<>();            // path -> last stored body

    private static final class Resp {
        final int status;
        final String body;
        Resp(int status, String body) { this.status = status; this.body = body == null ? "" : body; }
    }

    private static final class Stub {
        final int status;
        final String body;
        final Map<String, String> headers = new LinkedHashMap<>();
        int delayMs = 0;         // simulate a slow dependency
        int failPercent = 0;     // inject faults: this % of calls return failStatus
        int failStatus = 500;
        List<Resp> sequence;                       // non-null = stateful sequence
        final AtomicInteger seqIx = new AtomicInteger(0);

        Stub(int status, String body) {
            this.status = status;
            this.body = body == null ? "" : body;
        }
    }

    private static String key(String method, String path) {
        return method.toUpperCase() + " " + path;
    }

    private static byte[] readBody(com.sun.net.httpserver.HttpExchange exchange) throws IOException {
        java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
        byte[] buf = new byte[4096]; int n;
        while ((n = exchange.getRequestBody().read(buf)) != -1) { bos.write(buf, 0, n); }
        return bos.toByteArray();
    }

    /** Start on {@code requestedPort} (0 = OS picks a free port). Returns the actual bound port. */
    public int start(int requestedPort) throws IOException {
        server = HttpServer.create(new InetSocketAddress("127.0.0.1", requestedPort), 0);
        this.port = server.getAddress().getPort();
        server.createContext("/", exchange -> {
            String method = exchange.getRequestMethod();
            String path = exchange.getRequestURI().getPath();
            int code;
            byte[] out;
            String contentType = "application/json";

            if (statefulPaths.contains(path)) {
                // Stateful resource: POST/PUT store, GET read, DELETE clear.
                String m = method.toUpperCase();
                if (m.equals("POST") || m.equals("PUT")) {
                    String body = new String(readBody(exchange), StandardCharsets.UTF_8);
                    state.put(path, body);
                    code = m.equals("POST") ? 201 : 200;
                    out = body.getBytes(StandardCharsets.UTF_8);
                } else if (m.equals("GET")) {
                    String body = state.get(path);
                    if (body == null) { code = 404; out = "{\"error\":\"not_found\"}".getBytes(StandardCharsets.UTF_8); }
                    else { code = 200; out = body.getBytes(StandardCharsets.UTF_8); }
                } else if (m.equals("DELETE")) {
                    state.remove(path);
                    code = 204; out = new byte[0];
                } else {
                    code = 405; out = "{\"error\":\"method_not_allowed\"}".getBytes(StandardCharsets.UTF_8);
                }
                exchange.getResponseHeaders().add("Content-Type", contentType);
                exchange.sendResponseHeaders(code, out.length == 0 ? -1 : out.length);
                try (OutputStream os = exchange.getResponseBody()) { if (out.length > 0) os.write(out); }
                return;
            }

            Stub stub = stubs.get(key(method, path));
            if (stub == null) { stub = stubs.get(key("ANY", path)); }
            if (stub == null) {
                code = 404;
                out = ("no stub for " + method + " " + path).getBytes(StandardCharsets.UTF_8);
                exchange.getResponseHeaders().add("Content-Type", "text/plain");
            } else if (stub.sequence != null) {
                // Stateful sequence: advance per call, clamp at the last entry (which then repeats).
                int i = stub.seqIx.getAndIncrement();
                if (i >= stub.sequence.size()) { i = stub.sequence.size() - 1; }
                Resp r = stub.sequence.get(i);
                code = r.status;
                out = r.body.getBytes(StandardCharsets.UTF_8);
                exchange.getResponseHeaders().add("Content-Type", contentType);
            } else {
                if (stub.delayMs > 0) {
                    try { Thread.sleep(stub.delayMs); }
                    catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
                }
                if (stub.failPercent > 0 && ThreadLocalRandom.current().nextInt(100) < stub.failPercent) {
                    code = stub.failStatus;
                    out = "{\"error\":\"injected fault\"}".getBytes(StandardCharsets.UTF_8);
                    exchange.getResponseHeaders().add("Content-Type", "application/json");
                } else {
                    code = stub.status;
                    out = stub.body.getBytes(StandardCharsets.UTF_8);
                    for (Map.Entry<String, String> h : stub.headers.entrySet()) {
                        exchange.getResponseHeaders().add(h.getKey(), h.getValue());
                    }
                    if (!stub.headers.containsKey("Content-Type")) {
                        exchange.getResponseHeaders().add("Content-Type", "application/json");
                    }
                }
            }
            exchange.sendResponseHeaders(code, out.length);
            try (OutputStream os = exchange.getResponseBody()) { os.write(out); }
        });
        server.setExecutor(null);
        server.start();
        return this.port;
    }

    /** Register a canned response. Use method "ANY" to match any verb on the path. */
    public void stub(String method, String path, int status, String body) {
        stubs.put(key(method, path), new Stub(status, body));
    }

    /**
     * Register a canned response with fault injection: {@code delayMs} adds latency (a slow
     * dependency) and {@code failPercent} of calls return {@code failStatus} instead - for
     * resilience testing (retries, timeouts, circuit breakers).
     */
    public void stub(String method, String path, int status, String body,
                     int delayMs, int failPercent, int failStatus) {
        Stub s = new Stub(status, body);
        s.delayMs = Math.max(0, delayMs);
        s.failPercent = Math.max(0, Math.min(100, failPercent));
        s.failStatus = failStatus <= 0 ? 500 : failStatus;
        stubs.put(key(method, path), s);
    }

    /**
     * ADVANCED - stateful sequence: each call to method+path returns the NEXT response; the last one
     * repeats. e.g. statuses {202,202,200} models "still processing... done" for a polling client.
     */
    public void stubSequence(String method, String path, int[] statuses, String[] bodies) {
        Stub s = new Stub(statuses.length > 0 ? statuses[0] : 200, "");
        s.sequence = new ArrayList<>();
        for (int i = 0; i < statuses.length; i++) {
            s.sequence.add(new Resp(statuses[i], i < bodies.length ? bodies[i] : ""));
        }
        stubs.put(key(method, path), s);
    }

    /** Parse a "status body" spec (one response per line) into a {@link #stubSequence}. */
    public void stubSequenceSpec(String method, String path, String spec) {
        List<Integer> statuses = new ArrayList<>();
        List<String> bodies = new ArrayList<>();
        if (spec != null) {
            for (String raw : spec.split("\\r?\\n")) {
                String line = raw.trim();
                if (line.isEmpty()) { continue; }
                int sp = line.indexOf(' ');
                String st = sp < 0 ? line : line.substring(0, sp);
                String body = sp < 0 ? "" : line.substring(sp + 1).trim();
                try { statuses.add(Integer.parseInt(st.trim())); bodies.add(body); } catch (NumberFormatException ignore) { }
            }
        }
        if (statuses.isEmpty()) { statuses.add(200); bodies.add(""); }
        int[] sarr = new int[statuses.size()];
        String[] barr = new String[bodies.size()];
        for (int i = 0; i < statuses.size(); i++) { sarr[i] = statuses.get(i); barr[i] = bodies.get(i); }
        stubSequence(method, path, sarr, barr);
    }

    /**
     * ADVANCED - stateful resource: {@code path} behaves like a real record. POST/PUT stores the
     * request body, GET returns it (404 until created), DELETE clears it. Models a create-then-read
     * round trip against a virtualized backend.
     */
    public void stubStateful(String path) {
        statefulPaths.add(path);
    }

    public int port() {
        return port;
    }

    public String baseUri() {
        return "http://127.0.0.1:" + port;
    }

    public void stop() {
        if (server != null) {
            server.stop(0);
            server = null;
        }
    }
}
