package com.erp.batch;

import com.erp.service.ExternalDbConnectionService;
import com.erp.service.ExternalRestApiConnectionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
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.util.*;
import java.util.regex.Pattern;

/**
 * 배치 ETL 실행기 — vexplor_rps batchSchedulerService.executeBatchMappings 의 1:1 이식.
 *
 * 흐름:
 *   1. 매핑을 (fixed | non-fixed) 로 partition
 *   2. non-fixed 매핑을 (from_connection_type, from_connection_id, from_table_name) 키로 그룹화
 *   3. 그룹별로 FROM 데이터 읽기 → MappingTransformer 로 행 변환 → TO 저장
 *   4. (totalRecords, successRecords, failedRecords) 집계
 *
 * FROM 소스 지원:
 *   - internal     : 현 tenant DB 의 테이블 (JDBC 직접 SELECT, LIMIT 1000)
 *   - external_db  : ExternalDbConnectionService.executeQuery (SELECT-only 보안 정책)
 *   - restapi      : ExternalRestApiConnectionService.fetchData (등록된 연결 + dataArrayPath)
 *
 * TO 대상 지원:
 *   - internal     : 현 tenant DB INSERT / UPSERT (save_mode + conflict_key)
 *   - restapi      : 행 단위 POST/PUT/DELETE — testConnection 으로 호출
 *   - external_db  : 미지원 (ExternalDbConnectionService 가 SELECT-only 라 의도적으로 차단)
 *
 * 미지원 (vexplor_rps 대비 단순화):
 *   - to_api_body 템플릿 기반 일괄 전송
 *   - URL_PATH_PARAM 컬럼 처리
 *   - auth_tokens 자동 조회 (inline-mode REST API)
 *   - row_filter_config
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class BatchExecutor {

    private final SqlSession sqlSession;
    private final ExternalDbConnectionService externalDb;
    private final ExternalRestApiConnectionService externalRest;

    /** PostgreSQL 식별자 화이트리스트 (영문/숫자/언더스코어만). SQL injection 방어용. */
    private static final Pattern SAFE_IDENT = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
    private static final int FROM_LIMIT = 1000;

    public ExecutionResult execute(Map<String, Object> config) {
        ExecutionResult r = new ExecutionResult();
        Object mappingsRaw = config.get("batch_mappings");
        if (!(mappingsRaw instanceof List)) {
            log.warn("배치 매핑이 없습니다: {}", config.get("batch_name"));
            return r;
        }
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> mappings = (List<Map<String, Object>>) mappingsRaw;
        if (mappings.isEmpty()) {
            log.warn("배치 매핑이 없습니다: {}", config.get("batch_name"));
            return r;
        }

        // 1. fixed 분리
        MappingTransformer.Partition partition = MappingTransformer.partitionFixed(mappings);

        // 2. non-fixed 그룹화 (from_connection 기준)
        Map<String, List<Map<String, Object>>> tableGroups = new LinkedHashMap<>();
        for (Map<String, Object> m : partition.nonFixed) {
            String key = str(m.get("from_connection_type")) + ":"
                       + (m.get("from_connection_id") == null ? "internal" : m.get("from_connection_id"))
                       + ":" + str(m.get("from_table_name"));
            tableGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(m);
        }
        if (tableGroups.isEmpty() && !partition.fixed.isEmpty()) {
            log.warn("일반 매핑이 없고 고정값 매핑만 있어 실행 불가");
            return r;
        }

        String companyCode = str(config.get("company_code"));
        String saveMode = strOr(config.get("save_mode"), "INSERT");
        String conflictKey = str(config.get("conflict_key"));
        String dataArrayPath = str(config.get("data_array_path"));

        // 3. 그룹별 처리
        for (Map.Entry<String, List<Map<String, Object>>> e : tableGroups.entrySet()) {
            String key = e.getKey();
            List<Map<String, Object>> groupMappings = e.getValue();
            Map<String, Object> first = groupMappings.get(0);
            try {
                log.info("테이블 처리 시작: {} → {} 컬럼 매핑", key, groupMappings.size());

                // FROM 읽기
                List<Map<String, Object>> fromData = readFrom(first, groupMappings, dataArrayPath, companyCode);

                r.totalRecords += fromData.size();

                // Transform
                String toConnType = str(first.get("to_connection_type"));
                List<Map<String, Object>> mappedRows = new ArrayList<>(fromData.size());
                for (Map<String, Object> row : fromData) {
                    mappedRows.add(MappingTransformer.transformRow(
                            row, groupMappings, partition.fixed, toConnType, companyCode));
                }

                // TO 저장
                WriteResult wr = writeTo(first, mappedRows, saveMode, conflictKey, companyCode);
                r.successRecords += wr.success;
                r.failedRecords += wr.failed;

            } catch (Exception ex) {
                log.error("테이블 처리 중 오류: {} — {}", key, ex.getMessage(), ex);
                r.errorMessages.add(key + ": " + ex.getMessage());
            }
        }

        return r;
    }

    // ── FROM 읽기 ───────────────────────────────────────────────────────────

    private List<Map<String, Object>> readFrom(
            Map<String, Object> firstMapping,
            List<Map<String, Object>> groupMappings,
            String dataArrayPath,
            String companyCode
    ) {
        String type = str(firstMapping.get("from_connection_type"));
        String tableName = str(firstMapping.get("from_table_name"));
        List<String> columns = new ArrayList<>();
        for (Map<String, Object> m : groupMappings) {
            String col = str(m.get("from_column_name"));
            if (col != null && !col.isEmpty() && !columns.contains(col)) columns.add(col);
        }

        if ("restapi".equals(type)) {
            return readFromRestApi(firstMapping, dataArrayPath, companyCode);
        }
        if ("external".equals(type) || "external_db".equals(type)) {
            return readFromExternalDb(firstMapping, columns);
        }
        // internal (기본)
        return readFromInternal(tableName, columns);
    }

    /** Internal DB 의 동적 SELECT. sqlSession 의 현 tenant connection 사용. */
    private List<Map<String, Object>> readFromInternal(String tableName, List<String> columns) {
        if (tableName == null) throw new IllegalArgumentException("from_table_name 누락");
        if (columns.isEmpty()) throw new IllegalArgumentException("from_column_name 매핑 없음");
        StringBuilder sql = new StringBuilder("SELECT ");
        for (int i = 0; i < columns.size(); i++) {
            if (i > 0) sql.append(", ");
            sql.append(safeIdent(columns.get(i)));
        }
        sql.append(" FROM ").append(safeIdent(tableName));
        sql.append(" LIMIT ").append(FROM_LIMIT);

        try (Connection c = sqlSession.getConnection();
             PreparedStatement ps = c.prepareStatement(sql.toString());
             ResultSet rs = ps.executeQuery()) {
            return materialize(rs);
        } catch (SQLException e) {
            throw new RuntimeException("internal SELECT 실패: " + e.getMessage(), e);
        }
    }

    /** External DB SELECT — ExternalDbConnectionService.executeQuery 경유 (SELECT-only). */
    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> readFromExternalDb(Map<String, Object> firstMapping, List<String> columns) {
        Object connIdObj = firstMapping.get("from_connection_id");
        if (connIdObj == null) throw new IllegalArgumentException("external_db 인데 from_connection_id 가 비어있음");
        long connId = Long.parseLong(connIdObj.toString());
        String tableName = str(firstMapping.get("from_table_name"));
        StringBuilder sql = new StringBuilder("SELECT ");
        for (int i = 0; i < columns.size(); i++) {
            if (i > 0) sql.append(", ");
            sql.append(safeIdent(columns.get(i)));
        }
        sql.append(" FROM ").append(safeIdent(tableName)).append(" LIMIT ").append(FROM_LIMIT);

        Map<String, Object> result = externalDb.executeQuery(connId, sql.toString());
        Object data = result.get("data");
        return data instanceof List ? (List<Map<String, Object>>) data : List.of();
    }

    /** REST API → ExternalRestApiConnectionService.fetchData. dataArrayPath 로 배열 추출. */
    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> readFromRestApi(
            Map<String, Object> firstMapping, String dataArrayPath, String companyCode
    ) {
        Object connIdObj = firstMapping.get("from_connection_id");
        if (connIdObj == null) {
            throw new UnsupportedOperationException(
                "REST API 등록 연결 없는 inline-mode (from_api_url 직접 호출) 는 현재 미지원");
        }
        int connId = Integer.parseInt(connIdObj.toString());
        String endpoint = str(firstMapping.get("from_table_name"));

        Map<String, Object> params = new HashMap<>();
        if (companyCode != null) params.put("company_code", companyCode);
        Map<String, Object> result = externalRest.fetchData(connId, endpoint, dataArrayPath, params);

        if (!Boolean.TRUE.equals(result.get("success"))) {
            throw new RuntimeException("REST API 호출 실패: " + result.getOrDefault("message", ""));
        }
        Object data = result.get("data");
        if (!(data instanceof Map)) return List.of();
        Object rows = ((Map<String, Object>) data).get("rows");
        if (!(rows instanceof List)) return List.of();
        List<Object> raw = (List<Object>) rows;
        List<Map<String, Object>> out = new ArrayList<>(raw.size());
        for (Object o : raw) if (o instanceof Map) out.add((Map<String, Object>) o);
        return out;
    }

    // ── TO 저장 ────────────────────────────────────────────────────────────

    // 트랜잭션은 의도적으로 걸지 않음 — batch 의 정상 동작은 row 단위 독립 commit.
    // 일부 row 가 실패해도 다른 row 는 살아야 successCount/failedCount 집계가 의미 있음.
    public WriteResult writeTo(
            Map<String, Object> firstMapping,
            List<Map<String, Object>> rows,
            String saveMode,
            String conflictKey,
            String companyCode
    ) {
        if (rows == null || rows.isEmpty()) return new WriteResult();
        String type = str(firstMapping.get("to_connection_type"));
        String tableName = str(firstMapping.get("to_table_name"));

        if ("restapi".equals(type)) {
            return writeToRestApi(firstMapping, rows, companyCode);
        }
        if ("external".equals(type) || "external_db".equals(type)) {
            throw new UnsupportedOperationException(
                "external_db TO 쓰기는 현재 미지원 (ExternalDbConnectionService 가 SELECT-only)");
        }
        return writeToInternal(tableName, rows, saveMode, conflictKey);
    }

    /** Internal DB INSERT / UPSERT — 행 단위 PreparedStatement. */
    private WriteResult writeToInternal(String tableName, List<Map<String, Object>> rows,
                                        String saveMode, String conflictKey) {
        WriteResult r = new WriteResult();
        if (tableName == null) throw new IllegalArgumentException("to_table_name 누락");
        safeIdent(tableName);

        try (Connection c = sqlSession.getConnection()) {
            for (Map<String, Object> row : rows) {
                try {
                    String sql = buildInsertSql(tableName, row, saveMode, conflictKey);
                    try (PreparedStatement ps = c.prepareStatement(sql)) {
                        int idx = 1;
                        for (Object v : row.values()) {
                            ps.setObject(idx++, v);
                        }
                        ps.executeUpdate();
                        r.success++;
                    }
                } catch (SQLException e) {
                    log.error("INSERT 실패 row={} — {}", row, e.getMessage());
                    r.failed++;
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("internal write 실패: " + e.getMessage(), e);
        }
        return r;
    }

    /** INSERT (또는 UPSERT) SQL 생성. row 의 key 순서로 컬럼/플레이스홀더 배열. */
    private String buildInsertSql(String tableName, Map<String, Object> row,
                                  String saveMode, String conflictKey) {
        List<String> cols = new ArrayList<>(row.keySet());
        StringBuilder sql = new StringBuilder("INSERT INTO ").append(safeIdent(tableName)).append(" (");
        for (int i = 0; i < cols.size(); i++) {
            if (i > 0) sql.append(", ");
            sql.append(safeIdent(cols.get(i)));
        }
        sql.append(") VALUES (");
        for (int i = 0; i < cols.size(); i++) {
            if (i > 0) sql.append(", ");
            sql.append("?");
        }
        sql.append(")");

        if ("UPSERT".equalsIgnoreCase(saveMode) && conflictKey != null && !conflictKey.isEmpty()) {
            safeIdent(conflictKey);
            List<String> updateCols = new ArrayList<>();
            for (String col : cols) if (!col.equalsIgnoreCase(conflictKey)) updateCols.add(col);
            sql.append(" ON CONFLICT (").append(conflictKey).append(") ");
            if (updateCols.isEmpty()) {
                sql.append("DO NOTHING");
            } else {
                sql.append("DO UPDATE SET ");
                for (int i = 0; i < updateCols.size(); i++) {
                    if (i > 0) sql.append(", ");
                    String c = safeIdent(updateCols.get(i));
                    sql.append(c).append(" = EXCLUDED.").append(c);
                }
                if (cols.stream().anyMatch(c -> c.equalsIgnoreCase("updated_date"))) {
                    sql.append(", updated_date = NOW()");
                }
            }
        }
        return sql.toString();
    }

    /** REST API TO — 행 단위로 testConnection 호출 (POST/PUT/DELETE). */
    private WriteResult writeToRestApi(Map<String, Object> firstMapping,
                                       List<Map<String, Object>> rows, String companyCode) {
        WriteResult r = new WriteResult();
        String baseUrl = str(firstMapping.get("to_api_url"));
        String endpoint = str(firstMapping.get("to_table_name"));
        String method = strOr(firstMapping.get("to_api_method"), "POST");

        for (Map<String, Object> row : rows) {
            try {
                Map<String, Object> testReq = new LinkedHashMap<>();
                testReq.put("base_url", baseUrl);
                testReq.put("endpoint", endpoint);
                testReq.put("method", method);
                testReq.put("body", row);
                testReq.put("auth_type", "none");
                testReq.put("timeout", 30000);
                Map<String, Object> result = externalRest.testConnection(testReq, companyCode);
                if (Boolean.TRUE.equals(result.get("success"))) r.success++; else r.failed++;
            } catch (Exception e) {
                log.error("REST API 전송 실패 row={} — {}", row, e.getMessage());
                r.failed++;
            }
        }
        return r;
    }

    // ── 유틸 ────────────────────────────────────────────────────────────────

    private static List<Map<String, Object>> materialize(ResultSet rs) throws SQLException {
        ResultSetMetaData md = rs.getMetaData();
        int n = md.getColumnCount();
        List<Map<String, Object>> rows = new ArrayList<>();
        while (rs.next()) {
            Map<String, Object> row = new LinkedHashMap<>();
            for (int i = 1; i <= n; i++) row.put(md.getColumnLabel(i), rs.getObject(i));
            rows.add(row);
        }
        return rows;
    }

    private static String safeIdent(String s) {
        if (s == null || !SAFE_IDENT.matcher(s).matches()) {
            throw new IllegalArgumentException("Unsafe identifier: " + s);
        }
        return s;
    }

    private static String str(Object v) { return v == null ? null : v.toString(); }
    private static String strOr(Object v, String fallback) {
        String s = str(v);
        return (s == null || s.isEmpty()) ? fallback : s;
    }

    // ── 결과 클래스 ────────────────────────────────────────────────────────

    public static final class ExecutionResult {
        public int totalRecords = 0;
        public int successRecords = 0;
        public int failedRecords = 0;
        public final List<String> errorMessages = new ArrayList<>();

        public Map<String, Object> toMap() {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("total_records", totalRecords);
            m.put("success_records", successRecords);
            m.put("failed_records", failedRecords);
            m.put("error_message", errorMessages.isEmpty() ? null : String.join("\n", errorMessages));
            return m;
        }
    }

    public static final class WriteResult {
        public int success = 0;
        public int failed = 0;
    }
}
