package com.erp.service;

import com.erp.common.BaseService;
import com.erp.util.PasswordEncryption;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.sql.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * Node 플로우 전용 외부 DB 연결 서비스
 *
 * Node.js node-external-connections.ts / externalDbConnectionService.ts 포팅.
 *
 * 1. getTestedConnections — is_active='Y' 커넥션 중 실제로 연결되는 것만 반환
 *    (청크 3개씩 병렬 테스트, 3초 타임아웃)
 * 2. getTablesFromConnection  — 외부 DB 테이블 목록 조회
 * 3. getColumnsFromConnection — 외부 DB 테이블 컬럼 목록 조회
 */
@Service
@Slf4j
public class NodeExternalConnectionService extends BaseService {

    private final PasswordEncryption encryption;

    public NodeExternalConnectionService(
            @Value("${app.ext-db-password-secret:default-fallback-key-change-in-production}") String secretKey) {
        this.encryption = new PasswordEncryption(secretKey);
    }

    // ══════════════════════════════════════════════════════════════
    // 1. 테스트 완료된 커넥션 목록
    // Node.js GET /tested 동일 로직
    // ══════════════════════════════════════════════════════════════

    /**
     * 활성 외부 DB 커넥션 중 실제로 연결 가능한 것만 반환.
     * 청크 3개씩, 각 커넥션 3초 타임아웃.
     */
    public List<Map<String, Object>> getTestedConnections(String companyCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("company_code", companyCode);
        List<Map<String, Object>> connections = sqlSession.selectList("nodeExternalConnection.selectActiveConnections", params);

        List<Map<String, Object>> valid = new ArrayList<>();
        final int MAX_CONCURRENT = 3;
        final int TIMEOUT_SEC    = 3;

        for (int i = 0; i < connections.size(); i += MAX_CONCURRENT) {
            List<Map<String, Object>> chunk =
                    connections.subList(i, Math.min(i + MAX_CONCURRENT, connections.size()));

            List<CompletableFuture<Map<String, Object>>> futures = chunk.stream()
                    .map(conn -> CompletableFuture
                            .supplyAsync(() -> testAndBuildEntry(conn))
                            .orTimeout(TIMEOUT_SEC, TimeUnit.SECONDS)
                            .exceptionally(e -> {
                                log.warn("커넥션 테스트 타임아웃/오류 ({}): {}",
                                        conn.get("connection_name"), e.getMessage());
                                return null;
                            }))
                    .toList();

            for (CompletableFuture<Map<String, Object>> future : futures) {
                try {
                    Map<String, Object> result = future.join();
                    if (result != null) valid.add(result);
                } catch (Exception e) {
                    // null result from exceptionally — skip
                }
            }
        }

        log.info("테스트 성공 커넥션: {}/{}개", valid.size(), connections.size());
        return valid;
    }

    // ══════════════════════════════════════════════════════════════
    // 2. 외부 DB 테이블 목록
    // Node.js getTablesFromConnection 동일 로직
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getTablesFromConnection(int id) {
        Map<String, Object> conn = loadConnection(id);
        if (conn == null) return fail("연결 ID " + id + "를 찾을 수 없습니다.");

        String dbType = str(conn.get("db_type")).toLowerCase();
        String sql;
        switch (dbType) {
            case "postgresql":
                sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
                break;
            case "mysql":
            case "mariadb":
                sql = "SELECT table_name AS tablename FROM information_schema.tables " +
                      "WHERE table_schema = DATABASE() ORDER BY table_name";
                break;
            default:
                return fail("지원하지 않는 DB 타입: " + dbType);
        }

        try (Connection jdbc = openJdbc(conn);
             PreparedStatement ps = jdbc.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {

            List<String> tables = new ArrayList<>();
            while (rs.next()) tables.add(rs.getString(1));

            Map<String, Object> out = new LinkedHashMap<>();
            out.put("success", true);
            out.put("data",    tables);
            out.put("message", tables.size() + "개의 테이블을 조회했습니다.");
            return out;
        } catch (Exception e) {
            log.error("테이블 목록 조회 실패: id={}", id, e);
            return fail("테이블 목록 조회 중 오류가 발생했습니다: " + e.getMessage());
        }
    }

    // ══════════════════════════════════════════════════════════════
    // 3. 외부 DB 컬럼 목록
    // Node.js getColumnsFromConnection 동일 로직
    // ══════════════════════════════════════════════════════════════

    public Map<String, Object> getColumnsFromConnection(int id, String tableName) {
        Map<String, Object> conn = loadConnection(id);
        if (conn == null) return fail("연결 ID " + id + "를 찾을 수 없습니다.");

        String dbType = str(conn.get("db_type")).toLowerCase();
        String sql;
        switch (dbType) {
            case "postgresql":
                sql = "SELECT column_name, data_type FROM information_schema.columns " +
                      "WHERE table_schema = 'public' AND table_name = ? ORDER BY ordinal_position";
                break;
            case "mysql":
            case "mariadb":
                sql = "SELECT column_name, data_type FROM information_schema.columns " +
                      "WHERE table_schema = DATABASE() AND table_name = ? ORDER BY ordinal_position";
                break;
            default:
                return fail("지원하지 않는 DB 타입: " + dbType);
        }

        try (Connection jdbc = openJdbc(conn);
             PreparedStatement ps = jdbc.prepareStatement(sql)) {

            ps.setString(1, tableName);
            List<Map<String, Object>> cols = new ArrayList<>();
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    Map<String, Object> col = new LinkedHashMap<>();
                    col.put("column_name", rs.getString("column_name"));
                    col.put("data_type",   rs.getString("data_type"));
                    cols.add(col);
                }
            }

            Map<String, Object> out = new LinkedHashMap<>();
            out.put("success", true);
            out.put("data",    cols);
            out.put("message", cols.size() + "개의 컬럼을 조회했습니다.");
            return out;
        } catch (Exception e) {
            log.error("컬럼 목록 조회 실패: id={}, table={}", id, tableName, e);
            return fail("컬럼 목록 조회 중 오류가 발생했습니다: " + e.getMessage());
        }
    }

    // ══ private helpers ═══════════════════════════════════════════

    /** 연결 테스트 후 응답용 맵 구성 (실패 시 null 반환) */
    private Map<String, Object> testAndBuildEntry(Map<String, Object> conn) {
        try (Connection jdbc = openJdbc(conn)) {
            jdbc.prepareStatement("SELECT 1").execute();

            Map<String, Object> entry = new LinkedHashMap<>();
            entry.put("id",              conn.get("id"));
            entry.put("connection_name", conn.get("connection_name"));
            entry.put("description",     conn.get("description"));
            entry.put("db_type",         conn.get("db_type"));
            entry.put("host",            conn.get("host"));
            entry.put("port",            conn.get("port"));
            entry.put("database_name",   conn.get("database_name"));
            return entry;
        } catch (Exception e) {
            log.warn("연결 테스트 실패 ({}): {}", conn.get("connection_name"), e.getMessage());
            return null;
        }
    }

    private Map<String, Object> loadConnection(int id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        return sqlSession.selectOne("nodeExternalConnection.selectConnectionById", params);
    }

    /**
     * external_db_connections 행에서 JDBC 커넥션을 열어 반환.
     * ssl_enabled = 'Y'/'N' 문자열 처리 (Node.js 저장 형식과 동일).
     */
    private Connection openJdbc(Map<String, Object> conn) throws Exception {
        String dbType  = str(conn.get("db_type")).toLowerCase();
        String host    = str(conn.get("host"));
        int    port    = ((Number) conn.get("port")).intValue();
        String dbName  = str(conn.get("database_name"));
        String user    = str(conn.get("username"));
        String encPwd  = str(conn.get("password"));
        boolean ssl    = "Y".equalsIgnoreCase(str(conn.get("ssl_enabled")));

        String password;
        try {
            password = encryption.decrypt(encPwd);
        } catch (Exception e) {
            log.warn("비밀번호 복호화 실패, 원본 사용: {}", e.getMessage());
            password = encPwd;
        }

        String jdbcUrl;
        switch (dbType) {
            case "postgresql":
                jdbcUrl = String.format("jdbc:postgresql://%s:%d/%s?sslmode=%s",
                        host, port, dbName, ssl ? "require" : "disable");
                Class.forName("org.postgresql.Driver");
                break;
            case "mysql":
            case "mariadb":
                jdbcUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=%s&serverTimezone=UTC",
                        host, port, dbName, ssl);
                Class.forName("com.mysql.cj.jdbc.Driver");
                break;
            default:
                throw new UnsupportedOperationException("지원하지 않는 DB 타입: " + dbType);
        }

        return DriverManager.getConnection(jdbcUrl, user, password);
    }

    private Map<String, Object> fail(String message) {
        Map<String, Object> r = new LinkedHashMap<>();
        r.put("success", false);
        r.put("message", message);
        return r;
    }

    private String str(Object o) { return o == null ? "" : o.toString(); }
}
