package com.erp.service;

import com.erp.batch.BatchExecutor;
import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
public class BatchManagementService extends BaseService {

    @Autowired
    private BatchService batchService;
    @Autowired
    private ExternalDbConnectionService externalDbConnectionService;
    @Autowired
    private CommonService commonService;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private BatchExecutor batchExecutor;

    private static final String NS = "batchManagement.";
    private static final String EXEC_LOG_NS = "batchExecutionLog.";

    // ── Stats ─────────────────────────────────────────────────────────────────

    public Map<String, Object> getBatchStats(Map<String, Object> params) {
        commonService.applyCompanyCodeFilter(params);
        Map<String, Object> stats = sqlSession.selectOne(NS + "getBatchManagementStats", params);
        return stats != null ? stats : Collections.emptyMap();
    }

    // ── Node Flows ────────────────────────────────────────────────────────────

    public List<Map<String, Object>> getNodeFlows(Map<String, Object> params) {
        commonService.applyCompanyCodeFilter(params);
        return sqlSession.selectList(NS + "getBatchManagementNodeFlowList", params);
    }

    // ── Connections / Tables / Columns ────────────────────────────────────────

    public List<Map<String, Object>> getAvailableConnections(String companyCode) {
        return batchService.getConnections(companyCode);
    }

    public List<Map<String, Object>> getTablesFromConnection(String type, Long connectionId, String companyCode) {
        if ("internal".equals(type)) {
            return batchService.getTables("internal", null);
        }
        if ("external".equals(type) && connectionId != null) {
            try {
                return externalDbConnectionService.getTables(connectionId);
            } catch (Exception e) {
                log.warn("외부 DB 테이블 목록 조회 실패: connectionId={}, {}", connectionId, e.getMessage());
                throw new RuntimeException("테이블 목록 조회 중 오류가 발생했습니다: " + e.getMessage());
            }
        }
        throw new IllegalArgumentException("올바른 연결 타입을 지정해주세요. (internal 또는 external)");
    }

    public List<Map<String, Object>> getTableColumns(String type, Long connectionId, String tableName, String companyCode) {
        if ("internal".equals(type)) {
            return batchService.getColumns("internal", null, tableName);
        }
        if ("external".equals(type) && connectionId != null) {
            try {
                return externalDbConnectionService.getTableColumns(connectionId, tableName);
            } catch (Exception e) {
                log.warn("외부 DB 컬럼 조회 실패: connectionId={}, table={}, {}", connectionId, tableName, e.getMessage());
                throw new RuntimeException("컬럼 정보 조회 중 오류가 발생했습니다: " + e.getMessage());
            }
        }
        throw new IllegalArgumentException("올바른 연결 타입을 지정해주세요. (internal 또는 external)");
    }

    // ── Batch Configs CRUD ────────────────────────────────────────────────────

    public Map<String, Object> getBatchConfigList(Map<String, Object> params) {
        commonService.applyCompanyCodeFilter(params);
        return batchService.getBatchList(params);
    }

    public Map<String, Object> getBatchConfigInfo(Map<String, Object> params) {
        return batchService.getBatchInfo(params);
    }

    @Transactional
    public Map<String, Object> insertBatchConfig(Map<String, Object> params) {
        convertCamelToSnake(params);
        if (params.get("created_by") == null) params.put("created_by", "system");
        if (params.get("is_active") == null)  params.put("is_active", "Y");
        return batchService.insertBatch(params);
    }

    @Transactional
    public Map<String, Object> updateBatchConfig(Map<String, Object> params) {
        convertCamelToSnake(params);
        if (params.get("updated_by") == null) params.put("updated_by", "system");
        return batchService.updateBatch(params);
    }

    // ── Execute ───────────────────────────────────────────────────────────────

    public Map<String, Object> executeBatchConfig(Map<String, Object> params) {
        Long id = toLong(params.get("id"));
        if (id == null) throw new IllegalArgumentException("배치 설정 ID가 필요합니다.");

        Map<String, Object> batchConfig = batchService.getBatchInfo(params);
        if (batchConfig == null) throw new RuntimeException("배치 설정을 찾을 수 없습니다.");

        long startMs = System.currentTimeMillis();
        String batchName = str(batchConfig.get("batch_name"));
        String companyCode = str(batchConfig.get("company_code"));
        log.info("배치 수동 실행: id={}, name={}", id, batchName);

        // 1. 실행 로그 INSERT — RUNNING 상태로 먼저 박아두면 도중 비정상 종료해도 추적 가능
        Map<String, Object> logRow = new LinkedHashMap<>();
        logRow.put("batch_config_id", id);
        logRow.put("company_code", companyCode);
        logRow.put("execution_status", "RUNNING");
        logRow.put("server_name", safeHostName());
        logRow.put("process_id", String.valueOf(ProcessHandle.current().pid()));
        try {
            sqlSession.insert(EXEC_LOG_NS + "insertBatchExecutionLog", logRow);
        } catch (Exception e) {
            log.warn("실행 로그 INSERT 실패 (실행은 계속 진행): {}", e.getMessage());
        }
        Object logId = logRow.get("id");

        // 2. 실제 ETL 실행 — 예외는 로그에 기록 후 다시 throw (controller 의 에러 응답 위해)
        BatchExecutor.ExecutionResult execResult = null;
        String status = "SUCCESS";
        String errorMessage = null;
        try {
            execResult = batchExecutor.execute(batchConfig);
            if (execResult.failedRecords > 0) {
                status = execResult.successRecords > 0 ? "PARTIAL" : "FAILED";
            }
            if (!execResult.errorMessages.isEmpty()) {
                errorMessage = String.join("\n", execResult.errorMessages);
            }
        } catch (Exception e) {
            status = "FAILED";
            errorMessage = e.getMessage();
            log.error("배치 실행 중 예외: id={} — {}", id, e.getMessage(), e);
        }

        long duration = System.currentTimeMillis() - startMs;

        // 3. 실행 로그 UPDATE — 최종 상태/카운트/duration 마무리
        // 주의: batch_execution_logs 의 duration_ms / *_records 컬럼은 운영 DB 에서 VARCHAR
        // (V001 legacy 마이그레이션 흔적). PgJDBC 가 Long/Integer 를 VARCHAR 로 자동 변환하지 못할 수 있어
        // 명시적으로 String 으로 보낸다. mapper 의 COALESCE default 도 '0' (문자열) 이라 일관됨.
        if (logId != null) {
            Map<String, Object> updateLog = new LinkedHashMap<>();
            updateLog.put("id", logId);
            updateLog.put("execution_status", status);
            updateLog.put("end_time", new java.sql.Timestamp(System.currentTimeMillis()));
            updateLog.put("duration_ms", String.valueOf(duration));
            updateLog.put("total_records",   String.valueOf(execResult != null ? execResult.totalRecords   : 0));
            updateLog.put("success_records", String.valueOf(execResult != null ? execResult.successRecords : 0));
            updateLog.put("failed_records",  String.valueOf(execResult != null ? execResult.failedRecords  : 0));
            if (errorMessage != null) updateLog.put("error_message", errorMessage);
            try {
                sqlSession.update(EXEC_LOG_NS + "updateBatchExecutionLog", updateLog);
            } catch (Exception e) {
                log.warn("실행 로그 UPDATE 실패: {}", e.getMessage());
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("batch_name",      batchName);
        result.put("execution_status", status);
        result.put("total_records",   execResult != null ? execResult.totalRecords   : 0);
        result.put("success_records", execResult != null ? execResult.successRecords : 0);
        result.put("failed_records",  execResult != null ? execResult.failedRecords  : 0);
        result.put("execution_time",  duration);
        if (errorMessage != null) result.put("error_message", errorMessage);
        return result;
    }

    /** 실행 로그 server_name 컬럼용 — hostname resolve 실패 시 "unknown". */
    private static String safeHostName() {
        try {
            return java.net.InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            return "unknown";
        }
    }

    // ── REST API Preview / Save ───────────────────────────────────────────────

    public Map<String, Object> previewRestApiData(Map<String, Object> body) {
        // 프론트(batchManagement.ts)는 camelCase 로 키를 보내고 백엔드는 snake_case 로 읽음.
        // 기존 convertCamelToSnake() 는 batch_configs 전용 remap 이라 여기엔 효과 없음.
        // → previewRestApiData 전용으로 사용하는 키만 직접 remap.
        remap(body, "apiUrl",          "api_url");
        remap(body, "apiKey",          "api_key");
        remap(body, "requestBody",     "request_body");
        remap(body, "dataArrayPath",   "data_array_path");
        remap(body, "paramType",       "param_type");
        remap(body, "paramName",       "param_name");
        remap(body, "paramValue",      "param_value");
        remap(body, "paramSource",     "param_source");
        remap(body, "authServiceName", "auth_service_name");

        String apiUrl     = str(body.get("api_url"));
        String endpoint   = str(body.get("endpoint"));
        String method     = body.get("method") != null ? str(body.get("method")) : "GET";
        String apiKey     = str(body.get("api_key"));
        String dataPath   = str(body.get("data_array_path"));
        String paramType  = str(body.get("param_type"));
        String paramName  = str(body.get("param_name"));
        String paramValue = str(body.get("param_value"));

        if (isBlank(apiUrl) || isBlank(endpoint)) {
            throw new IllegalArgumentException("API URL과 엔드포인트는 필수입니다.");
        }

        // URL 파라미터 처리
        String finalEndpoint = endpoint;
        if ("url".equals(paramType) && !isBlank(paramName) && !isBlank(paramValue)) {
            if (finalEndpoint.contains("{" + paramName + "}")) {
                finalEndpoint = finalEndpoint.replace("{" + paramName + "}", paramValue);
            } else {
                finalEndpoint = finalEndpoint + "/" + paramValue;
            }
        } else if ("query".equals(paramType) && !isBlank(paramName) && !isBlank(paramValue)) {
            String sep = finalEndpoint.contains("?") ? "&" : "?";
            finalEndpoint = finalEndpoint + sep + paramName + "=" + paramValue;
        }

        String fullUrl = apiUrl + finalEndpoint;
        log.info("REST API 미리보기: {} {}", method, fullUrl);

        try {
            RestTemplate restTemplate = new RestTemplate();
            HttpHeaders headers = new HttpHeaders();
            if (!isBlank(apiKey)) {
                headers.set("Authorization", "Bearer " + apiKey);
            }
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity<String> entity = new HttpEntity<>(headers);
            ResponseEntity<String> response = restTemplate.exchange(
                fullUrl, HttpMethod.valueOf(method.toUpperCase()), entity, String.class);

            String responseBody = response.getBody();
            if (responseBody == null || responseBody.isBlank()) {
                return buildPreviewResult(Collections.emptyList());
            }

            Object parsed = objectMapper.readValue(responseBody, Object.class);
            List<Map<String, Object>> dataList = extractDataArray(parsed, dataPath);
            return buildPreviewResult(dataList);

        } catch (Exception e) {
            log.error("REST API 미리보기 오류: {}", e.getMessage());
            throw new RuntimeException("REST API 데이터 미리보기 중 오류가 발생했습니다: " + e.getMessage());
        }
    }

    @Transactional
    public Map<String, Object> saveRestApiBatch(Map<String, Object> body) {
        if (isBlank(str(body.get("batch_name"))) || isBlank(str(body.get("cron_schedule")))) {
            throw new IllegalArgumentException("필수 필드가 누락되었습니다. (batchName, cronSchedule)");
        }
        convertCamelToSnake(body);
        if (body.get("created_by") == null) body.put("created_by", "system");
        if (body.get("is_active")  == null) body.put("is_active",  "Y");
        if (body.get("execution_type") == null) {
            body.put("execution_type", body.getOrDefault("batch_type", "mapping"));
        }
        return batchService.insertBatch(body);
    }

    // ── Auth Services ─────────────────────────────────────────────────────────

    public List<String> getAuthServiceNames(Map<String, Object> params) {
        commonService.applyCompanyCodeFilter(params);
        return sqlSession.selectList(NS + "getBatchManagementAuthServiceList", params);
    }

    // ── Sparkline / Recent Logs ───────────────────────────────────────────────

    public List<Map<String, Object>> getBatchSparkline(Map<String, Object> params) {
        return sqlSession.selectList(NS + "getBatchManagementSparklineData", params);
    }

    public List<Map<String, Object>> getGlobalSparkline(Map<String, Object> params) {
        commonService.applyCompanyCodeFilter(params);
        return sqlSession.selectList(NS + "getBatchManagementGlobalSparklineData", params);
    }

    public List<Map<String, Object>> getBatchRecentLogs(Map<String, Object> params) {
        return sqlSession.selectList(NS + "getBatchManagementRecentLogList", params);
    }

    // ── Private Helpers ───────────────────────────────────────────────────────

    /** camelCase 요청 필드 → batch_configs 테이블 컬럼명(snake_case) 변환 */
    private void convertCamelToSnake(Map<String, Object> params) {
        remap(params, "batch_name",       "batch_name");
        remap(params, "cron_schedule",    "cron_schedule");
        remap(params, "is_active",        "is_active");
        remap(params, "save_mode",        "save_mode");
        remap(params, "conflict_key",     "conflict_key");
        remap(params, "auth_service_name", "auth_service_name");
        remap(params, "data_array_path",   "data_array_path");
        remap(params, "execution_type",   "execution_type");
        remap(params, "node_flow_id",      "node_flow_id");
        remap(params, "node_flow_context", "node_flow_context");
        remap(params, "batch_type",       "execution_type");
        remap(params, "api_mappings",     "mappings");
        remap(params, "description",     "description");
    }

    private void remap(Map<String, Object> params, String from, String to) {
        if (params.containsKey(from) && !params.containsKey(to)) {
            params.put(to, params.get(from));
        }
    }

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

    private boolean isBlank(String s) {
        return s == null || s.isBlank();
    }

    private Long toLong(Object val) {
        if (val == null) return null;
        try { return Long.parseLong(val.toString()); }
        catch (NumberFormatException e) { return null; }
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> extractDataArray(Object parsed, String dataPath) {
        if (parsed == null) return Collections.emptyList();
        Object target = parsed;
        if (!isBlank(dataPath)) {
            for (String key : dataPath.split("\\.")) {
                if (target instanceof Map) {
                    target = ((Map<?, ?>) target).get(key);
                } else {
                    return Collections.emptyList();
                }
            }
        }
        if (target instanceof List) {
            return (List<Map<String, Object>>) target;
        }
        if (target instanceof Map) {
            List<Map<String, Object>> single = new ArrayList<>();
            single.add((Map<String, Object>) target);
            return single;
        }
        return Collections.emptyList();
    }

    private Map<String, Object> buildPreviewResult(List<Map<String, Object>> dataList) {
        List<Map<String, Object>> samples = dataList.stream().limit(5).collect(Collectors.toList());
        List<String> fields = samples.isEmpty() ? Collections.emptyList() : new ArrayList<>(samples.get(0).keySet());

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("fields",     fields);
        result.put("samples",    samples);
        result.put("total_count", dataList.size());
        return result;
    }
}
