package com.erp.provisioning;

import com.erp.tenant.TenantDbSettings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

/**
 * pg_dump → psql 파이프로 스키마 복제.
 *
 * ★ 보안 강화 (2026-04-24 Codex 리뷰 반영):
 *   - 이전 버전: "sh -c 'pg_dump ... | psql ...'" 로 쉘 문자열 구성 → command injection 표면.
 *   - 현재: ProcessBuilder 에 각 인자를 배열로 전달 + JVM 내부에서 stdout→stdin 파이프.
 *   - DB 이름은 ^[a-z][a-z0-9_]{{2,40}}$ 화이트리스트 재검증.
 *   - PGPASSWORD 는 여전히 env 로 전달. (.pgpass 전환은 별도 작업)
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class SchemaCopier {

    private static final Pattern SAFE_DB_NAME = Pattern.compile("^[a-z][a-z0-9_]{2,40}$");
    private static final int BUFFER_SIZE = 64 * 1024;
    private static final long TIMEOUT_MILLIS = 5 * 60 * 1000; // 5분

    private final TenantDbSettings settings;

    public void copySchema(String srcDb, String dstDb) throws IOException, InterruptedException {
        requireSafeDbName(srcDb);
        requireSafeDbName(dstDb);

        ProcessBuilder dumpPb = new ProcessBuilder(
                "pg_dump",
                "-h", settings.host(),
                "-p", String.valueOf(settings.port()),
                "-U", settings.username(),
                "-s",                    // schema only
                "--no-owner",
                "--no-privileges",
                srcDb
        );
        dumpPb.environment().put("PGPASSWORD", settings.password());
        dumpPb.redirectErrorStream(false);

        ProcessBuilder restorePb = new ProcessBuilder(
                "psql",
                "-h", settings.host(),
                "-p", String.valueOf(settings.port()),
                "-U", settings.username(),
                "-d", dstDb,
                "-v", "ON_ERROR_STOP=1"
        );
        restorePb.environment().put("PGPASSWORD", settings.password());
        restorePb.redirectErrorStream(false);

        Process dump = dumpPb.start();
        Process restore = restorePb.start();

        // dump stdout -> restore stdin
        Thread pipe = new Thread(() -> {
            try (InputStream in = dump.getInputStream();
                 OutputStream out = restore.getOutputStream()) {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
            } catch (IOException e) {
                log.warn("[Provisioning] pipe error (dump→psql): {}", e.getMessage());
            }
        }, "schema-copy-pipe");
        pipe.setDaemon(true);

        // stderr 캡처 (비동기)
        StringBuilder dumpErr = new StringBuilder();
        StringBuilder restoreErr = new StringBuilder();
        Thread dumpErrT = asyncCollect(dump.getErrorStream(), dumpErr, "pg_dump-stderr");
        Thread restoreErrT = asyncCollect(restore.getErrorStream(), restoreErr, "psql-stderr");

        pipe.start();
        dumpErrT.start();
        restoreErrT.start();

        long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS;
        boolean dumpDone = dump.waitFor(TIMEOUT_MILLIS, java.util.concurrent.TimeUnit.MILLISECONDS);
        if (!dumpDone) { dump.destroyForcibly(); restore.destroyForcibly(); throw new IOException("pg_dump timed out"); }
        long remaining = Math.max(1, deadline - System.currentTimeMillis());
        boolean restoreDone = restore.waitFor(remaining, java.util.concurrent.TimeUnit.MILLISECONDS);
        if (!restoreDone) { restore.destroyForcibly(); throw new IOException("psql timed out"); }

        pipe.join(5_000);
        dumpErrT.join(5_000);
        restoreErrT.join(5_000);

        int dumpExit = dump.exitValue();
        int restoreExit = restore.exitValue();

        if (dumpExit != 0 || restoreExit != 0) {
            String dumpLog = dumpErr.length() > 2000 ? dumpErr.substring(0, 2000) + "…" : dumpErr.toString();
            String restoreLog = restoreErr.length() > 2000 ? restoreErr.substring(0, 2000) + "…" : restoreErr.toString();
            throw new IOException(String.format(
                    "schema copy failed (dumpExit=%d, restoreExit=%d)\n--pg_dump stderr--\n%s\n--psql stderr--\n%s",
                    dumpExit, restoreExit, dumpLog, restoreLog));
        }
        log.info("[Provisioning] COPY SCHEMA: {} -> {}", srcDb, dstDb);
    }

    private static void requireSafeDbName(String name) {
        if (name == null || !SAFE_DB_NAME.matcher(name).matches()) {
            throw new IllegalArgumentException("unsafe db name: " + name);
        }
    }

    private static Thread asyncCollect(InputStream in, StringBuilder sink, String threadName) {
        Thread t = new Thread(() -> {
            try (InputStream is = in) {
                byte[] buf = new byte[4096];
                int n;
                while ((n = is.read(buf)) != -1) {
                    sink.append(new String(buf, 0, n, StandardCharsets.UTF_8));
                    if (sink.length() > 10_000) break; // runaway 방어
                }
            } catch (IOException ignored) { }
        }, threadName);
        t.setDaemon(true);
        return t;
    }
}
