package com.erp.provisioning;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 선택된 테이블을 원본 DB → 신규 DB 로 복사.
 *
 * 정책:
 *   - company_code 컬럼이 있는 테이블: ('*','TEMPLATE') + NULL 만 복사 (공통 템플릿).
 *   - 없는 테이블: 전체 복사.
 *
 * ★ 실패 시: 테이블 하나라도 예외가 터지면 호출자에게 그대로 throw.
 *   CompanyProvisioningService 가 dst.rollback() + DROP DATABASE 로 원자성 보장.
 *
 * ★ 2026-04-24 추가: resetSequences() — 데이터 복사 후 모든 시퀀스의 current value 를
 *   max(column)+1 로 재설정. pg_dump --schema-only 는 시퀀스 값을 안 복제하므로 필수.
 */
@Service
@Slf4j
public class DataCopier {

    private static final int BATCH = 500;

    public int copyTable(Connection src, Connection dst, String table) throws SQLException {
        boolean hasCompanyCode = columnExists(src, table, "company_code");
        List<String> cols = listColumns(src, table);
        if (cols.isEmpty()) {
            log.warn("[Provisioning] {} has no columns in source — skip", table);
            return 0;
        }

        String where = hasCompanyCode ? " WHERE company_code IS NULL OR company_code IN ('*', 'TEMPLATE')" : "";
        String quotedCols = cols.stream().map(c -> "\"" + c + "\"").collect(Collectors.joining(","));
        String selectSql = "SELECT " + quotedCols + " FROM \"" + table + "\"" + where;
        String placeholders = cols.stream().map(c -> "?").collect(Collectors.joining(","));
        String insertSql = "INSERT INTO \"" + table + "\" (" + quotedCols + ") VALUES (" + placeholders + ")";

        int rows = 0;
        try (PreparedStatement selectStmt = src.prepareStatement(selectSql);
             ResultSet rs = selectStmt.executeQuery();
             PreparedStatement insertStmt = dst.prepareStatement(insertSql)) {
            ResultSetMetaData meta = rs.getMetaData();
            int colCount = meta.getColumnCount();
            while (rs.next()) {
                for (int i = 1; i <= colCount; i++) {
                    insertStmt.setObject(i, rs.getObject(i));
                }
                insertStmt.addBatch();
                rows++;
                if (rows % BATCH == 0) insertStmt.executeBatch();
            }
            insertStmt.executeBatch();
        }
        log.info("[Provisioning] COPY DATA {}: {} rows (filter={})", table, rows, hasCompanyCode);
        return rows;
    }

    /**
     * 모든 시퀀스의 current value 를 연결된 컬럼의 MAX()+1 로 재설정.
     * pg_depend 조회로 seq↔table↔column 관계를 얻으므로 휴리스틱 없음.
     *
     * 이 쿼리의 테이블/컬럼/시퀀스 이름은 pg_class 에서 직접 나온 값이라 신뢰 가능 → SQL injection 위험 없음.
     * 빈 테이블은 setval(seq, 1) 로 안전하게 초기화.
     */
    public int resetSequences(Connection dst) throws SQLException {
        // format_type 으로 컬럼 실제 타입까지 조회 → integer 계열만 setval.
        // 레거시 DB 에선 SERIAL 이었다가 나중에 TEXT 로 타입 변경된 컬럼이 있을 수 있음 (시퀀스 의존성만 남음).
        // 이런 컬럼에 setval 을 호출하면 "COALESCE types text and integer cannot be matched" 예외 발생.
        String findSql = """
                SELECT s.relname AS seq, t.relname AS tbl, a.attname AS col,
                       format_type(a.atttypid, a.atttypmod) AS coltype
                FROM pg_class s
                JOIN pg_depend d ON d.objid = s.oid
                JOIN pg_class t ON t.oid = d.refobjid
                JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
                WHERE s.relkind = 'S' AND d.deptype = 'a'
                """;
        List<String[]> rows = new ArrayList<>();
        try (Statement s = dst.createStatement();
             ResultSet rs = s.executeQuery(findSql)) {
            while (rs.next()) {
                rows.add(new String[]{
                        rs.getString("seq"), rs.getString("tbl"),
                        rs.getString("col"), rs.getString("coltype")});
            }
        }

        int updated = 0, skippedType = 0, skippedErr = 0;
        try (Statement us = dst.createStatement()) {
            for (String[] r : rows) {
                String seq = r[0], tbl = r[1], col = r[2], coltype = r[3];
                String sql;
                if (isIntegerLike(coltype)) {
                    sql = String.format(
                            "SELECT setval('%s', GREATEST(COALESCE((SELECT MAX(\"%s\") FROM \"%s\"), 0), 1))",
                            seq.replace("'", "''"), col, tbl);
                } else if (isVarcharLike(coltype)) {
                    // V001 마이그레이션으로 INT → VARCHAR 로 바뀐 PK 컬럼도 시퀀스가 연결되어 있고,
                    // INSERT 시 DEFAULT nextval 이 호출되므로 max 재설정 필요. 비숫자 PK(UUID 등) 가
                    // 섞여 있어도 정규식으로 거르고 숫자 PK 만 max 계산.
                    sql = String.format(
                            "SELECT setval('%s', GREATEST(COALESCE((SELECT MAX(\"%s\"::bigint) FROM \"%s\" WHERE \"%s\" ~ '^[0-9]+$'), 0), 1))",
                            seq.replace("'", "''"), col, tbl, col);
                } else {
                    skippedType++;
                    continue;
                }
                try {
                    us.execute(sql);
                    updated++;
                } catch (SQLException e) {
                    skippedErr++;
                    log.warn("[Provisioning] setval failed seq={} tbl={} col={} type={}: {}",
                            seq, tbl, col, coltype, e.getMessage());
                }
            }
        }
        log.info("[Provisioning] RESET SEQUENCES: updated={} skipped_non_numeric={} skipped_error={} total={}",
                updated, skippedType, skippedErr, rows.size());
        return updated;
    }

    private static boolean isIntegerLike(String coltype) {
        if (coltype == null) return false;
        String t = coltype.toLowerCase();
        return t.startsWith("integer") || t.startsWith("bigint") || t.startsWith("smallint")
                || t.startsWith("int4") || t.startsWith("int8") || t.startsWith("int2");
    }

    private static boolean isVarcharLike(String coltype) {
        if (coltype == null) return false;
        String t = coltype.toLowerCase();
        return t.startsWith("character varying") || t.startsWith("varchar") || t.startsWith("text");
    }

    private List<String> listColumns(Connection conn, String table) throws SQLException {
        List<String> cols = new ArrayList<>();
        try (PreparedStatement ps = conn.prepareStatement(
                "SELECT column_name FROM information_schema.columns " +
                        "WHERE table_schema='public' AND lower(table_name)=lower(?) " +
                        "ORDER BY ordinal_position")) {
            ps.setString(1, table);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) cols.add(rs.getString(1));
            }
        }
        return cols;
    }

    private boolean columnExists(Connection conn, String table, String col) throws SQLException {
        try (PreparedStatement ps = conn.prepareStatement(
                "SELECT 1 FROM information_schema.columns " +
                        "WHERE table_schema='public' AND lower(table_name)=lower(?) AND lower(column_name)=lower(?)")) {
            ps.setString(1, table);
            ps.setString(2, col);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next();
            }
        }
    }
}
