package com.erp.service;

import com.erp.common.BaseService;
import com.erp.util.PasswordEncryption;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

/**
 * 외부 DB 연결 관리 서비스.
 * - CRUD: MyBatis Mapper (ExternalDbConnectionMapper)
 * - 연결 테스트 / 쿼리 실행: JDBC DriverManager (런타임 동적 연결)
 */
@Service
@Slf4j
public class ExternalDbConnectionService extends BaseService {

    @Autowired private ObjectMapper objectMapper;

    @Value("${encryption.secret-key:ilshin-erp-encryption-key-2024}")
    private String encryptionSecretKey;

    private PasswordEncryption passwordEncryption;

    @PostConstruct
    public void init() {
        // scrypt 기반 키 유도는 CPU-intensive → 애플리케이션 시작 시 1회만 생성
        this.passwordEncryption = new PasswordEncryption(encryptionSecretKey);
        log.info("PasswordEncryption initialized");
    }

    // ── 지원 DB 타입 (하드코딩) ────────────────────────────────────────────────

    public Map<String, Object> getSupportedTypes() {
        List<Map<String, Object>> types = List.of(
            typeOf("postgresql", "PostgreSQL", 5432),
            typeOf("mysql", "MySQL", 3306),
            typeOf("mariadb", "MariaDB", 3306),
            typeOf("oracle", "Oracle", 1521),
            typeOf("mssql", "SQL Server", 1433),
            typeOf("sqlite", "SQLite", null)
        );
        Map<String, Object> defaults = new LinkedHashMap<>();
        defaults.put("postgresql", Map.of("port", 5432, "ssl_enabled", "N"));
        defaults.put("mysql", Map.of("port", 3306, "ssl_enabled", "N"));
        defaults.put("mariadb", Map.of("port", 3306, "ssl_enabled", "N"));
        defaults.put("oracle", Map.of("port", 1521, "ssl_enabled", "N"));
        defaults.put("mssql", Map.of("port", 1433, "ssl_enabled", "N"));
        Map<String, Object> sqliteDefaults = new LinkedHashMap<>();
        sqliteDefaults.put("port", null);
        sqliteDefaults.put("ssl_enabled", "N");
        defaults.put("sqlite", sqliteDefaults);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("types", types);
        result.put("defaults", defaults);
        return result;
    }

    private Map<String, Object> typeOf(String value, String label, Integer defaultPort) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("value", value);
        m.put("label", label);
        m.put("default_port", defaultPort);
        return m;
    }

    // ── 풀 상태 (외부 연결 풀은 관리하지 않음 → 빈 목록 반환) ──────────────────

    public Map<String, Object> getPoolStatus() {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total_pools", 0);
        result.put("active_pools", 0);
        result.put("pools", Collections.emptyList());
        return result;
    }

    // ── 목록 조회 ──────────────────────────────────────────────────────────────

    public List<Map<String, Object>> getConnections(Map<String, Object> params) {
        List<Map<String, Object>> list = sqlSession.selectList("externalDbConnection.getExternalDbConnectionList", params);
        list.forEach(this::parseConnectionOptions);
        return list;
    }

    // ── DB 타입별 그룹화 ────────────────────────────────────────────────────────

    public Map<String, Object> getConnectionsGroupedByType(Map<String, Object> params) {
        List<Map<String, Object>> connections = getConnections(params);
        List<Map<String, Object>> categories = new ArrayList<>();
        try {
            categories = sqlSession.selectList("externalDbConnection.getExternalDbTypeCategories", null);
        } catch (Exception e) {
            log.warn("db_type_categories 조회 실패 (테이블 없을 수 있음): {}", e.getMessage());
        }

        // 카테고리 초기화
        Map<String, Map<String, Object>> grouped = new LinkedHashMap<>();
        for (Map<String, Object> cat : categories) {
            String typeCode = str(cat, "type_code");
            if (typeCode == null) continue;
            Map<String, Object> entry = new LinkedHashMap<>();
            entry.put("category", cat);
            entry.put("connections", new ArrayList<>());
            grouped.put(typeCode, entry);
        }

        // 연결을 타입 그룹에 배치
        for (Map<String, Object> conn : connections) {
            String dbType = str(conn, "db_type");
            if (dbType == null) continue;

            if (grouped.containsKey(dbType)) {
                ((List<Map<String, Object>>) grouped.get(dbType).get("connections")).add(conn);
            } else {
                // 기타 그룹
                grouped.computeIfAbsent("other", k -> {
                    Map<String, Object> otherCat = new LinkedHashMap<>();
                    otherCat.put("type_code", "other");
                    otherCat.put("display_name", "기타");
                    otherCat.put("icon", "database");
                    otherCat.put("color", "#6B7280");
                    otherCat.put("sort_order", 999);
                    Map<String, Object> entry = new LinkedHashMap<>();
                    entry.put("category", otherCat);
                    entry.put("connections", new ArrayList<>());
                    return entry;
                });
                ((List<Map<String, Object>>) grouped.get("other").get("connections")).add(conn);
            }
        }

        // 빈 그룹 제거
        grouped.entrySet().removeIf(e ->
            ((List<?>) e.getValue().get("connections")).isEmpty());

        Map<String, Object> result = new LinkedHashMap<>(grouped);
        return result;
    }

    // ── 단건 조회 ──────────────────────────────────────────────────────────────

    public Map<String, Object> getConnectionById(long id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        Map<String, Object> conn = sqlSession.selectOne("externalDbConnection.getExternalDbConnectionInfo", params);
        if (conn != null) parseConnectionOptions(conn);
        return conn;
    }

    private Map<String, Object> getConnectionByIdWithPassword(long id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        Map<String, Object> conn = sqlSession.selectOne("externalDbConnection.getExternalDbConnectionInfoWithPassword", params);
        if (conn != null) parseConnectionOptions(conn);
        return conn;
    }

    // ── 생성 ───────────────────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> createConnection(Map<String, Object> body) {
        String name = str(body, "connection_name");
        String companyCode = str(body, "company_code");

        // 필수 필드 검증
        for (String f : List.of("connection_name", "db_type", "host", "port",
                "database_name", "username", "password", "company_code")) {
            if (body.get(f) == null || body.get(f).toString().isBlank()) {
                throw new IllegalArgumentException("필수 필드가 누락되었습니다: " + f);
            }
        }

        // 이름 중복 확인
        Map<String, Object> dupCheck = new HashMap<>();
        dupCheck.put("connection_name", name);
        dupCheck.put("company_code", companyCode);
        if (sqlSession.selectOne("externalDbConnection.getExternalDbConnectionByName", dupCheck) != null) {
            throw new IllegalArgumentException("이미 존재하는 연결명입니다.");
        }

        // 비밀번호 암호화
        String rawPassword = str(body, "password");
        body.put("password", passwordEncryption.encrypt(rawPassword));

        // connection_options 직렬화
        serializeConnectionOptions(body);

        sqlSession.insert("externalDbConnection.insertExternalDbConnection", body);
        Long id = body.get("id") != null ? Long.parseLong(body.get("id").toString()) : null;

        // 비밀번호 마스킹 후 반환
        Map<String, Object> result = new LinkedHashMap<>(body);
        result.put("password", "***ENCRYPTED***");
        if (id != null) result.put("id", id);
        parseConnectionOptions(result);
        return result;
    }

    // ── 수정 ───────────────────────────────────────────────────────────────────

    @Transactional
    public Map<String, Object> updateConnection(long id, Map<String, Object> body) {
        // 존재 확인
        Map<String, Object> existing = getConnectionByIdWithPassword(id);
        if (existing == null) {
            throw new NoSuchElementException("해당 연결 설정을 찾을 수 없습니다.");
        }

        // 이름 중복 확인 (자신 제외)
        String name = str(body, "connection_name");
        String companyCode = str(body, "company_code");
        if (name != null && companyCode != null) {
            Map<String, Object> dupCheck = new HashMap<>();
            dupCheck.put("connection_name", name);
            dupCheck.put("company_code", companyCode);
            dupCheck.put("exclude_id", id);
            if (sqlSession.selectOne("externalDbConnection.getExternalDbConnectionByNameExclude", dupCheck) != null) {
                throw new IllegalArgumentException("이미 존재하는 연결명입니다.");
            }
        }

        // 비밀번호 변경 시 암호화
        String rawPassword = str(body, "password");
        if (rawPassword != null && !rawPassword.equals("***ENCRYPTED***")) {
            body.put("password", passwordEncryption.encrypt(rawPassword));
        } else {
            body.remove("password"); // 변경 없으면 UPDATE 제외
        }

        // connection_options 직렬화
        if (body.containsKey("connection_options")) {
            serializeConnectionOptions(body);
        }

        body.put("id", id);
        sqlSession.update("externalDbConnection.updateExternalDbConnection", body);

        Map<String, Object> updated = getConnectionById(id);
        return updated;
    }

    // ── 삭제 ───────────────────────────────────────────────────────────────────

    @Transactional
    public void deleteConnection(long id, String userCompanyCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        // SUPER_ADMIN("*")은 모든 연결 삭제 가능, 일반 사용자는 자사만
        if (!"*".equals(userCompanyCode)) {
            params.put("company_code", userCompanyCode);
        }
        int deleted = sqlSession.delete("externalDbConnection.deleteExternalDbConnection", params);
        if (deleted == 0) {
            throw new NoSuchElementException("해당 연결 설정을 찾을 수 없거나 권한이 없습니다.");
        }
    }

    // ── 연결 테스트 ────────────────────────────────────────────────────────────

    public Map<String, Object> testConnectionById(long id, String testPassword) {
        Map<String, Object> conn = getConnectionByIdWithPassword(id);
        if (conn == null) {
            return errorResult("CONNECTION_NOT_FOUND",
                "ID " + id + "에 해당하는 연결 정보가 없습니다.");
        }

        String password;
        if (testPassword != null && !testPassword.isBlank()) {
            password = testPassword;
        } else {
            String encrypted = str(conn, "password");
            if (encrypted == null) {
                return errorResult("DECRYPTION_FAILED", "비밀번호가 없습니다.");
            }
            try {
                password = passwordEncryption.decrypt(encrypted);
            } catch (Exception e) {
                log.warn("비밀번호 복호화 실패 (id={}): {}", id, e.getMessage());
                return errorResult("DECRYPTION_FAILED", "비밀번호 복호화에 실패했습니다.");
            }
        }

        String dbType = str(conn, "db_type");

        return executeConnectionTest(dbType, conn, password);
    }

    private Map<String, Object> executeConnectionTest(
            String dbType, Map<String, Object> conn, String password) {

        String type = dbType == null ? "" : dbType.toLowerCase();
        String host = str(conn, "host");
        int port = toInt(conn, "port", defaultPort(type));
        String database = str(conn, "database_name");
        String username = str(conn, "username");
        String sslEnabled = str(conn, "ssl_enabled");
        int connTimeout = toInt(conn, "connection_timeout", 30);
        boolean ssl = "Y".equalsIgnoreCase(sslEnabled);

        String url;
        Properties props = new Properties();
        if (username != null) props.setProperty("user", username);
        if (password != null) props.setProperty("password", password);

        switch (type) {
            case "postgresql" -> {
                url = String.format("jdbc:postgresql://%s:%d/%s", host, port, database);
                props.setProperty("connect_timeout", String.valueOf(connTimeout));
                props.setProperty("socket_timeout", "30");
                if (ssl) {
                    props.setProperty("ssl", "true");
                    props.setProperty("sslmode", "require");
                }
            }
            case "mysql" -> {
                url = String.format("jdbc:mysql://%s:%d/%s", host, port, database);
                props.setProperty("connectTimeout", String.valueOf(connTimeout * 1000));
                props.setProperty("socketTimeout", "30000");
                props.setProperty("useSSL", String.valueOf(ssl));
                props.setProperty("allowPublicKeyRetrieval", "true");
            }
            case "mariadb" -> {
                url = String.format("jdbc:mariadb://%s:%d/%s", host, port, database);
                props.setProperty("connectTimeout", String.valueOf(connTimeout * 1000));
                props.setProperty("socketTimeout", "30000");
                if (ssl) props.setProperty("useSsl", "true");
            }
            case "mssql", "sqlserver" -> {
                StringBuilder sb = new StringBuilder()
                    .append("jdbc:sqlserver://").append(host).append(':').append(port)
                    .append(";databaseName=").append(database)
                    .append(";loginTimeout=").append(connTimeout)
                    .append(";encrypt=").append(ssl ? "true;trustServerCertificate=true" : "false");
                url = sb.toString();
            }
            case "sqlite" -> {
                // SQLite: host/port 무의미. database_name 을 파일 경로로 사용 (비면 in-memory)
                url = "jdbc:sqlite:" + (database != null && !database.isBlank() ? database : ":memory:");
            }
            default -> {
                Map<String, Object> result = new LinkedHashMap<>();
                result.put("success", false);
                result.put("message", "지원하지 않는 DB 타입입니다: " + dbType);
                return result;
            }
        }

        try (Connection c = DriverManager.getConnection(url, props);
             Statement stmt = c.createStatement()) {
            stmt.executeQuery("SELECT 1");
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("success", true);
            result.put("message", "연결 성공");
            Map<String, Object> details = new LinkedHashMap<>();
            details.put("host", host == null ? "" : host);
            details.put("database", database == null ? "" : database);
            details.put("port", port);
            result.put("details", details);
            return result;
        } catch (SQLException e) {
            log.warn("DB 연결 테스트 실패 ({}): {}", url, e.getMessage());
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("success", false);
            result.put("message", "연결 실패: " + e.getMessage());
            result.put("error", Map.of(
                "code", "CONNECTION_FAILED",
                "details", e.getMessage()));
            return result;
        }
    }

    private int defaultPort(String dbType) {
        if (dbType == null) return 5432;
        return switch (dbType.toLowerCase()) {
            case "mysql", "mariadb" -> 3306;
            case "mssql", "sqlserver" -> 1433;
            case "sqlite" -> 0;
            default -> 5432;
        };
    }

    // ── SQL 쿼리 실행 (SELECT only) ────────────────────────────────────────────

    public Map<String, Object> executeQuery(long id, String sql) {
        // 보안: SELECT만 허용
        String trimmed = sql.trim().toUpperCase();
        if (!trimmed.startsWith("SELECT")) {
            throw new IllegalArgumentException(
                "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다.");
        }
        for (String kw : List.of("INSERT", "UPDATE", "DELETE", "DROP", "CREATE",
                "ALTER", "TRUNCATE", "EXEC", "EXECUTE", "CALL", "MERGE")) {
            if (trimmed.contains(kw)) {
                throw new IllegalArgumentException(
                    "데이터를 변경하거나 삭제하는 쿼리는 허용되지 않습니다.");
            }
        }

        Map<String, Object> conn = getConnectionByIdWithPassword(id);
        if (conn == null) {
            throw new NoSuchElementException("연결 정보를 찾을 수 없습니다.");
        }

        String dbType = str(conn, "db_type");

        if (!"postgresql".equalsIgnoreCase(dbType)) {
            throw new UnsupportedOperationException(
                "이 버전에서는 PostgreSQL 쿼리 실행만 지원됩니다.");
        }

        String decrypted = decryptPassword(conn);
        String url = buildJdbcUrl(conn);
        Properties props = buildJdbcProps(conn, decrypted);

        try (Connection c = DriverManager.getConnection(url, props);
             Statement stmt = c.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            ResultSetMetaData meta = rs.getMetaData();
            int colCount = meta.getColumnCount();
            List<Map<String, Object>> rows = new ArrayList<>();
            while (rs.next()) {
                Map<String, Object> row = new LinkedHashMap<>();
                for (int i = 1; i <= colCount; i++) {
                    row.put(meta.getColumnLabel(i), rs.getObject(i));
                }
                rows.add(row);
            }
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("success", true);
            result.put("message", "쿼리가 성공적으로 실행되었습니다.");
            result.put("data", rows);
            return result;
        } catch (SQLException e) {
            log.error("쿼리 실행 오류: {}", e.getMessage());
            throw new RuntimeException("쿼리 실행 중 오류가 발생했습니다: " + e.getMessage(), e);
        }
    }

    // ── 테이블 목록 조회 ────────────────────────────────────────────────────────

    public List<Map<String, Object>> getTables(long id) {
        Map<String, Object> conn = getConnectionByIdWithPassword(id);
        if (conn == null) {
            throw new NoSuchElementException("연결 정보를 찾을 수 없습니다.");
        }

        String dbType = str(conn, "db_type");
        if (!"postgresql".equalsIgnoreCase(dbType)) {
            return Collections.emptyList();
        }

        String decrypted = decryptPassword(conn);
        String url = buildJdbcUrl(conn);
        Properties props = buildJdbcProps(conn, decrypted);

        try (Connection c = DriverManager.getConnection(url, props);
             Statement stmt = c.createStatement();
             ResultSet rs = stmt.executeQuery(
                 "SELECT table_name, obj_description(" +
                 "quote_ident(table_name)::regclass::oid, 'pg_class') AS description " +
                 "FROM information_schema.tables " +
                 "WHERE table_schema = 'public' AND table_type = 'BASE TABLE' " +
                 "ORDER BY table_name")) {

            List<Map<String, Object>> tables = new ArrayList<>();
            while (rs.next()) {
                Map<String, Object> t = new LinkedHashMap<>();
                t.put("table_name", rs.getString("table_name"));
                t.put("description", rs.getString("description"));
                tables.add(t);
            }
            return tables;
        } catch (SQLException e) {
            log.error("테이블 목록 조회 오류: {}", e.getMessage());
            throw new RuntimeException("테이블 목록 조회 중 오류가 발생했습니다: " + e.getMessage(), e);
        }
    }

    // ── 테이블 컬럼 목록 조회 ───────────────────────────────────────────────────

    public List<Map<String, Object>> getTableColumns(long id, String tableName) {
        Map<String, Object> conn = getConnectionByIdWithPassword(id);
        if (conn == null) {
            throw new NoSuchElementException("연결 정보를 찾을 수 없습니다.");
        }

        String dbType = str(conn, "db_type");
        if (!"postgresql".equalsIgnoreCase(dbType)) {
            return Collections.emptyList();
        }

        String decrypted = decryptPassword(conn);
        String url = buildJdbcUrl(conn);
        Properties props = buildJdbcProps(conn, decrypted);

        String query =
            "SELECT column_name, data_type, is_nullable, column_default, " +
            "character_maximum_length " +
            "FROM information_schema.columns " +
            "WHERE table_schema = 'public' AND table_name = '" +
            tableName.replace("'", "''") + "' " +
            "ORDER BY ordinal_position";

        try (Connection c = DriverManager.getConnection(url, props);
             Statement stmt = c.createStatement();
             ResultSet rs = stmt.executeQuery(query)) {

            List<Map<String, Object>> columns = new ArrayList<>();
            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"));
                col.put("is_nullable", rs.getString("is_nullable"));
                col.put("column_default", rs.getString("column_default"));
                col.put("character_maximum_length", rs.getObject("character_maximum_length"));
                columns.add(col);
            }
            return columns;
        } catch (SQLException e) {
            log.error("컬럼 목록 조회 오류: {}", e.getMessage());
            throw new RuntimeException("컬럼 목록 조회 중 오류가 발생했습니다: " + e.getMessage(), e);
        }
    }

    // ── 제어관리용 활성 연결 목록 ─────────────────────────────────────────────

    public List<Map<String, Object>> getActiveConnectionsForControl(
            Map<String, Object> params) {

        params.put("is_active", "Y");
        List<Map<String, Object>> connections = getConnections(params);

        // 각 연결 테스트 (병렬 처리는 복잡성 증가 → 순차 처리, 타임아웃 5초)
        List<Map<String, Object>> valid = new ArrayList<>();
        for (Map<String, Object> conn : connections) {
            try {
                Long connId = toLong(conn, "id");
                if (connId == null) continue;
                Map<String, Object> testResult = testConnectionById(connId, null);
                if (Boolean.TRUE.equals(testResult.get("success"))) {
                    valid.add(conn);
                }
            } catch (Exception e) {
                log.debug("활성 연결 테스트 실패 (id={}): {}", conn.get("id"), e.getMessage());
            }
        }

        // 메인 DB를 첫 번째로 추가
        Map<String, Object> mainDb = new LinkedHashMap<>();
        mainDb.put("id", 0);
        mainDb.put("connection_name", "메인 데이터베이스 (현재 시스템)");
        mainDb.put("description", "현재 시스템의 PostgreSQL 데이터베이스");
        mainDb.put("db_type", "postgresql");
        mainDb.put("host", "localhost");
        mainDb.put("port", 5432);
        mainDb.put("database_name", "erp_database");
        mainDb.put("username", "system");
        mainDb.put("password", "***");
        mainDb.put("is_active", "Y");
        mainDb.put("company_code", "*");

        List<Map<String, Object>> result = new ArrayList<>();
        result.add(mainDb);
        result.addAll(valid);
        return result;
    }

    // ── 내부 유틸 ──────────────────────────────────────────────────────────────

    private String decryptPassword(Map<String, Object> conn) {
        String encrypted = str(conn, "password");
        if (encrypted == null || encrypted.equals("***ENCRYPTED***")) {
            throw new RuntimeException("비밀번호를 복호화할 수 없습니다.");
        }
        try {
            return passwordEncryption.decrypt(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("비밀번호 복호화 실패: " + e.getMessage(), e);
        }
    }

    private String buildJdbcUrl(Map<String, Object> conn) {
        String host = str(conn, "host");
        int port = toInt(conn, "port", 5432);
        String database = str(conn, "database_name");
        return String.format("jdbc:postgresql://%s:%d/%s", host, port, database);
    }

    private Properties buildJdbcProps(Map<String, Object> conn, String password) {
        Properties props = new Properties();
        String username = str(conn, "username");
        props.setProperty("user", username != null ? username : "");
        props.setProperty("password", password != null ? password : "");
        int connTimeout = toInt(conn, "connection_timeout", 30);
        props.setProperty("connect_timeout", String.valueOf(connTimeout));
        props.setProperty("socket_timeout", "60");
        String ssl = str(conn, "ssl_enabled");
        if ("Y".equalsIgnoreCase(ssl)) {
            props.setProperty("ssl", "true");
            props.setProperty("sslmode", "require");
        }
        return props;
    }

    @SuppressWarnings("unchecked")
    private void parseConnectionOptions(Map<String, Object> conn) {
        // connection_options 가 JSON 문자열이면 Map으로 파싱
        String key = "connection_options";
        Object raw = conn.get(key);
        if (raw instanceof String s && !s.isBlank()) {
            try {
                conn.put(key, objectMapper.readValue(s, new TypeReference<Map<String, Object>>() {}));
            } catch (Exception e) {
                // 파싱 실패 시 원본 문자열 유지
            }
        }
    }

    private void serializeConnectionOptions(Map<String, Object> params) {
        Object opts = params.get("connection_options");
        if (opts == null) {
            params.put("connection_options", "{}");
        } else if (opts instanceof Map || opts instanceof List) {
            try {
                params.put("connection_options", objectMapper.writeValueAsString(opts));
            } catch (Exception e) {
                params.put("connection_options", "{}");
            }
        }
        // 이미 문자열이면 그대로 사용
    }

    private Map<String, Object> errorResult(String code, String details) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", false);
        result.put("message", details);
        result.put("error", Map.of("code", code, "details", details));
        return result;
    }

    private String str(Map<String, Object> m, String key) {
        Object v = m.get(key);
        return v != null ? v.toString() : null;
    }

    private int toInt(Map<String, Object> m, String key, int defaultVal) {
        Object v = m.get(key);
        if (v == null) return defaultVal;
        try { return Integer.parseInt(v.toString()); } catch (Exception e) { return defaultVal; }
    }

    private Long toLong(Map<String, Object> m, String key) {
        Object v = m.get(key);
        if (v == null) return null;
        try { return Long.parseLong(v.toString()); } catch (Exception e) { return null; }
    }
}
