package study.saga;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Comparator;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/** 每次测试仅创建带随机标识的两个库与一个独占 TC，不触碰已有服务。 */
final class SagaFixture implements AutoCloseable {
    private static final String HOST = "127.0.0.1";
    private static final int TC_PORT = 18091;
    private final String token = UUID.randomUUID().toString().replace("-", "");
    private final String tc = "study_u38_sagatc_" + token;
    private final String biz = "study_u38_sagabiz_" + token;
    private final Path root;
    private Process server;
    private boolean tcCreated;
    private boolean bizCreated;

    private SagaFixture(Path root) { this.root = root; }
    String user() { return "root"; }
    String password() { return ""; }
    String bizUrl() { return jdbc(biz); }
    private String jdbc(String db) {
        return "jdbc:mysql://" + HOST + ":3306/" + db
                + "?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8&serverTimezone=UTC";
    }
    private Connection connect(String db) throws Exception { return DriverManager.getConnection(jdbc(db), user(), password()); }

    static SagaFixture start() throws Exception {
        String work = System.getenv("U38_SAGA_WORK_DIR");
        if (work == null || work.isBlank()) throw new IllegalStateException("请使用 run.py 创建独占实验目录");
        if (!"yes".equals(System.getenv("U38_SAGA_ALLOW_TC")))
            throw new IllegalStateException("启动 TC 前需获用户授权并显式启用");
        Path root = Paths.get(work).toRealPath();
        if (!Files.isDirectory(root.resolve("ownership"))) throw new IllegalStateException("缺少所有权记录目录");
        SagaFixture fixture = new SagaFixture(root);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try { fixture.close(); } catch (Exception e) { System.err.println("U38_SAGA shutdown cleanup failed: " + e); }
        }));
        try { fixture.initialize(); return fixture; }
        catch (Exception | Error e) { try { fixture.close(); } catch (Exception cleanup) { e.addSuppressed(cleanup); } throw e; }
    }

    private void createDb(String db) throws Exception {
        Path owner = root.resolve("ownership").resolve(db);
        Files.writeString(owner, token, StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE_NEW);
        try (Connection connection = connect(""); Statement statement = connection.createStatement()) {
            statement.execute("CREATE DATABASE `" + db + "` DEFAULT CHARSET utf8mb4");
            if (db.equals(tc)) tcCreated = true; else bizCreated = true;
            statement.execute("CREATE TABLE `" + db + "`.u38_saga_owner (token CHAR(32) PRIMARY KEY)");
            statement.execute("INSERT INTO `" + db + "`.u38_saga_owner VALUES ('" + token + "')");
        }
    }

    private void initialize() throws Exception {
        if (portOpen()) throw new IllegalStateException("TC 端口被已有进程占用: " + TC_PORT);
        createDb(tc);
        createDb(biz);
        initializeSql(tc, "/sql/seata-server-mysql.sql");
        initializeSql(biz, "/sql/saga-mysql.sql");
        try (Connection c = connect(biz); Statement s = c.createStatement()) {
            s.execute("CREATE TABLE u38_saga_order (id VARCHAR(40) PRIMARY KEY, state VARCHAR(20) NOT NULL) ENGINE=InnoDB");
        }
        String defaultHome = Paths.get(System.getProperty("user.home"), ".cache/software-engineering-study/java-depth-impl/u38/dist/extracted-bin/apache-seata-2.5.0-incubating-bin/seata-server").toString();
        Path home = Paths.get(env("U38_SEATA_SERVER_DIR", defaultHome));
        Path driver = Paths.get(env("U38_MYSQL_DRIVER", Paths.get(System.getProperty("user.home"), ".m2/repository/com/mysql/mysql-connector-j/9.7.0/mysql-connector-j-9.7.0.jar").toString()));
        if (!Files.isRegularFile(home.resolve("target/seata-server.jar")) || !Files.isRegularFile(driver))
            throw new IllegalStateException("缺少既有 Seata 2.5.0 发行包或 MySQL 驱动；请先按项目规则征得下载许可");
        Path agent = root.resolve("loopback-agent.jar");
        Path buildScript = Paths.get("../seata/loopback/build.sh").toAbsolutePath().normalize();
        runChecked(new ProcessBuilder("bash", buildScript.toString(), home.toString(), agent.toString())
                .redirectErrorStream(true).redirectOutput(root.resolve("agent-build.log").toFile()), 60);
        Path drivers = Files.createDirectory(root.resolve("drivers"));
        Files.copy(driver, drivers.resolve("mysql-connector-j-9.7.0.jar"));
        String java = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
        ProcessBuilder command = new ProcessBuilder(java, "-Xms64m", "-Xmx128m", "-XX:ActiveProcessorCount=2",
                "-javaagent:" + agent, "-Dio.netty.eventLoopThreads=2",
                "-Dloader.path=" + home.resolve("lib") + "," + drivers,
                "-Dlog.home=" + root.resolve("server-logs"), "-Dlogging.config=" + home.resolve("conf/logback-spring.xml"),
                "-Dseata.store.mode=db", "-Dseata.store.db.datasource=hikari", "-Dseata.store.db.db-type=mysql",
                "-Dseata.store.db.driver-class-name=com.mysql.cj.jdbc.Driver", "-Dseata.store.db.url=" + jdbc(tc),
                "-Dseata.store.db.user=root", "-Dseata.store.db.password=", "-Dseata.store.db.min-conn=1",
                "-Dseata.store.db.max-conn=4", "-jar", home.resolve("target/seata-server.jar").toString(), "-p", String.valueOf(TC_PORT));
        command.directory(home.toFile()).redirectErrorStream(true).redirectOutput(root.resolve("tc.log").toFile());
        server = command.start();
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(90);
        while (!portOpen() && System.nanoTime() < deadline) {
            if (!server.isAlive()) throw new IllegalStateException("TC 启动失败，见 " + root.resolve("tc.log"));
            Thread.sleep(100);
        }
        if (!portOpen()) throw new IllegalStateException("TC 启动超时");
        Process check = new ProcessBuilder("lsof", "-nP", "-a", "-p", Long.toString(server.pid()), "-iTCP:" + TC_PORT, "-sTCP:LISTEN").redirectErrorStream(true).start();
        String output = new String(check.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
        if (check.waitFor() != 0 || !output.contains(HOST + ":" + TC_PORT) || output.contains("*:" + TC_PORT))
            throw new IllegalStateException("TC 未限定本机回环: " + output);
        Files.writeString(root.resolve("listener.txt"), output, StandardCharsets.UTF_8);
    }

    private void initializeSql(String db, String resource) throws Exception {
        try (var input = SagaFixture.class.getResourceAsStream(resource); Connection c = connect(db); Statement s = c.createStatement()) {
            if (input == null) throw new IOException("缺少 SQL: " + resource);
            StringBuilder sql = new StringBuilder();
            for (String line : new String(input.readAllBytes(), StandardCharsets.UTF_8).split("\\R")) {
                if (!line.stripLeading().startsWith("--")) sql.append(line).append('\n');
            }
            boolean quoted = false;
            StringBuilder part = new StringBuilder();
            for (char ch : sql.toString().toCharArray()) {
                if (ch == '\'') quoted = !quoted;
                if (ch == ';' && !quoted) { if (!part.toString().isBlank()) s.execute(part.toString()); part.setLength(0); }
                else part.append(ch);
            }
            if (!part.toString().isBlank()) s.execute(part.toString());
        }
    }

    private static String env(String key, String fallback) {
        String value = System.getenv(key); return value == null || value.isBlank() ? fallback : value;
    }
    private static boolean portOpen() {
        try (Socket s = new Socket()) { s.connect(new InetSocketAddress(HOST, TC_PORT), 500); return true; }
        catch (IOException e) { return false; }
    }
    private static void runChecked(ProcessBuilder builder, int seconds) throws Exception {
        Process p = builder.start();
        if (!p.waitFor(seconds, TimeUnit.SECONDS)) { p.destroyForcibly(); throw new IOException("构建 agent 超时"); }
        if (p.exitValue() != 0) throw new IOException("构建 agent 失败，检查 agent-build.log");
    }

    private void dropIfOwned(String db) throws Exception {
        try (Connection c = connect(""); Statement s = c.createStatement()) {
            try (ResultSet rs = s.executeQuery("SELECT token FROM `" + db + "`.u38_saga_owner")) {
                if (!rs.next() || !token.equals(rs.getString(1)) || rs.next()) throw new IllegalStateException("所有权不匹配: " + db);
            }
            s.execute("DROP DATABASE `" + db + "`");
        }
    }

    @Override public synchronized void close() throws Exception {
        if (server != null) {
            server.destroy();
            if (!server.waitFor(10, TimeUnit.SECONDS)) { server.destroyForcibly(); server.waitFor(); }
            server = null;
        }
        Exception error = null;
        if (tcCreated) try { dropIfOwned(tc); tcCreated = false; } catch (Exception e) { error = e; }
        if (bizCreated) try { dropIfOwned(biz); bizCreated = false; } catch (Exception e) { if (error == null) error = e; else error.addSuppressed(e); }
        if (error != null) throw error;
    }
}
