package com.ga.batch.settlement.transform;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.product.ContractMapper;
import com.ga.core.mapper.receive.ReceiveMapper;
import com.ga.core.vo.ledger.LedgerVO;
import com.ga.core.vo.product.ContractSearchParam;
import com.ga.core.vo.product.ContractVO;
import com.ga.core.vo.receive.CompanyFieldMappingVO;
import com.ga.core.vo.receive.ParseErrorLogVO;
import com.ga.core.vo.receive.RawCommissionDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * raw_commission_data → 표준 원장(recruit_ledger / maintain_ledger) 변환.
 *
 * 동작:
 *  1. company_field_mapping 의 (source_field → target_field) 규칙 로드
 *  2. raw_content (JSON) 에서 source_field 값을 추출
 *  3. transform_rule 적용 (TRIM/UPPER/LOWER/DATE/DIVIDE/MULTIPLY/CODE_MAP)
 *  4. policy_no 로 contract 매칭 → contract_id, agent_id 채움
 *  5. data_type 에 따라 recruit_ledger / maintain_ledger 에 INSERT
 *
 * 실패 시 parse_error_log 에 기록.
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MappingEngine {

    private final ReceiveMapper receiveMapper;
    private final ContractMapper contractMapper;
    private final RecruitLedgerMapper recruitMapper;
    private final MaintainLedgerMapper maintainMapper;
    private final ObjectMapper objectMapper;

    @Transactional
    public Long transform(RawCommissionDataVO raw) {
        if (raw.getCompanyCode() == null || raw.getRawContent() == null) {
            saveError(raw, "INVALID_INPUT", null, null, "company_code 또는 raw_content 누락");
            return null;
        }

        String targetTable = resolveTargetTable(raw.getDataType());
        if (targetTable == null) {
            saveError(raw, "UNKNOWN_TYPE", "data_type", raw.getDataType(),
                    "지원하지 않는 data_type: " + raw.getDataType());
            return null;
        }

        List<CompanyFieldMappingVO> mappings = receiveMapper.selectFieldMappings(
                raw.getCompanyCode(), targetTable);
        if (mappings.isEmpty()) {
            saveError(raw, "NO_MAPPING", null, null,
                    "매핑 정의 없음: " + raw.getCompanyCode() + "." + targetTable);
            return null;
        }

        JsonNode root;
        try {
            root = objectMapper.readTree(raw.getRawContent());
        } catch (Exception e) {
            saveError(raw, "JSON_PARSE", null, null, "raw_content JSON 파싱 실패: " + e.getMessage());
            return null;
        }

        Map<String, Object> mapped = new HashMap<>();
        for (CompanyFieldMappingVO m : mappings) {
            JsonNode v = root.get(m.getSourceField());
            String src = (v == null || v.isNull()) ? null : v.asText();
            try {
                Object converted = applyTransform(src, m, raw.getCompanyCode());
                if (converted == null && "Y".equals(m.getIsRequired())) {
                    saveError(raw, "REQUIRED", m.getTargetField(), src,
                            "필수 항목 누락: " + m.getSourceField());
                    return null;
                }
                mapped.put(m.getTargetField(), converted);
            } catch (Exception e) {
                saveError(raw, "TRANSFORM_FAIL", m.getTargetField(), src, e.getMessage());
                return null;
            }
        }

        String policyNo = (String) mapped.get("policy_no");
        if (policyNo == null) {
            saveError(raw, "NO_POLICY", "policy_no", null, "policy_no 누락");
            return null;
        }
        ContractVO contract = findContractByPolicyNo(policyNo);
        if (contract == null) {
            saveError(raw, "CONTRACT_NOT_FOUND", "policy_no", policyNo,
                    "계약을 찾을 수 없습니다: " + policyNo);
            return null;
        }

        LedgerVO led = buildLedger(mapped, contract, raw);
        if ("recruit_ledger".equals(targetTable)) {
            recruitMapper.insert(led);
        } else {
            maintainMapper.insert(led);
        }
        return led.getLedgerId();
    }

    private String resolveTargetTable(String dataType) {
        if (dataType == null) return null;
        return switch (dataType.toUpperCase()) {
            case "RECRUIT"  -> "recruit_ledger";
            case "MAINTAIN" -> "maintain_ledger";
            default         -> null;
        };
    }

    private Object applyTransform(String value, CompanyFieldMappingVO m, String companyCode) {
        if (value == null) {
            return convertType(m.getDefaultValue(), m.getDataType());
        }
        String trimmed = value.trim();
        String rule = m.getTransformRule();
        if (rule == null || rule.isBlank()) {
            return convertType(trimmed, m.getDataType());
        }
        return switch (rule.toUpperCase()) {
            case "TRIM"     -> convertType(trimmed, m.getDataType());
            case "UPPER"    -> trimmed.toUpperCase();
            case "LOWER"    -> trimmed.toLowerCase();
            case "DATE"     -> parseDate(trimmed, m.getDefaultValue());
            case "DIVIDE"   -> divideOrMultiply(trimmed, m.getDefaultValue(), true);
            case "MULTIPLY" -> divideOrMultiply(trimmed, m.getDefaultValue(), false);
            case "CODE_MAP" -> {
                String mappedCode = receiveMapper.findTargetCode(companyCode, m.getDefaultValue(), trimmed);
                yield mappedCode != null ? mappedCode : trimmed;
            }
            default -> convertType(trimmed, m.getDataType());
        };
    }

    private Object convertType(String value, String dataType) {
        if (value == null) return null;
        if (dataType == null) return value;
        return switch (dataType.toUpperCase()) {
            case "STRING"            -> value;
            case "NUMBER", "DECIMAL" -> new BigDecimal(value.replace(",", ""));
            case "INT", "INTEGER"    -> Integer.parseInt(value.replace(",", ""));
            case "LONG"              -> Long.parseLong(value.replace(",", ""));
            case "DATE"              -> parseDate(value, null);
            default                  -> value;
        };
    }

    private LocalDate parseDate(String s, String pattern) {
        if (s == null || s.isBlank()) return null;
        try {
            DateTimeFormatter fmt = (pattern != null && !pattern.isBlank())
                    ? DateTimeFormatter.ofPattern(pattern)
                    : DateTimeFormatter.ofPattern("yyyyMMdd");
            return LocalDate.parse(s, fmt);
        } catch (Exception ignored) {
            try { return LocalDate.parse(s); } catch (Exception e) { return null; }
        }
    }

    private BigDecimal divideOrMultiply(String value, String factor, boolean divide) {
        BigDecimal num = new BigDecimal(value.replace(",", ""));
        BigDecimal f = new BigDecimal(factor != null && !factor.isBlank() ? factor : "1");
        return divide ? num.divide(f, 2, RoundingMode.HALF_UP) : num.multiply(f);
    }

    private ContractVO findContractByPolicyNo(String policyNo) {
        ContractSearchParam p = new ContractSearchParam();
        p.setPolicyNo(policyNo);
        p.setPageSize(1);
        var list = contractMapper.selectList(p);
        if (list.isEmpty()) return null;
        return contractMapper.selectById(list.get(0).getContractId());
    }

    private LedgerVO buildLedger(Map<String, Object> mapped, ContractVO contract, RawCommissionDataVO raw) {
        LedgerVO led = new LedgerVO();
        led.setContractId(contract.getContractId());
        led.setAgentId(contract.getAgentId());
        led.setPolicyNo(contract.getPolicyNo());
        led.setCompanyCode(raw.getCompanyCode());
        led.setProductCode((String) mapped.get("product_code"));
        led.setInsuranceType((String) mapped.getOrDefault("insurance_type", "WHOLE_LIFE"));
        led.setCommissionYear(toInt(mapped.get("commission_year"), 1));
        led.setPremium(toBd(mapped.get("premium"), contract.getPremium()));
        led.setPayMethod((String) mapped.getOrDefault("pay_method", contract.getPayCycle()));
        led.setCompanyRate(toBd(mapped.get("company_rate"), BigDecimal.ZERO));
        led.setCompanyAmount(toBd(mapped.get("company_amount"), BigDecimal.ZERO));
        led.setPayoutRate(toBd(mapped.get("payout_rate"), BigDecimal.ZERO));
        led.setAgentAmount(toBd(mapped.get("agent_amount"), BigDecimal.ZERO));
        led.setTaxAmount(MoneyUtil.tax(led.getAgentAmount()));
        led.setSettleMonth(raw.getSettleMonth());
        led.setStatus("CALCULATED");
        led.setReconcileStatus("PENDING");
        return led;
    }

    private BigDecimal toBd(Object v, BigDecimal fallback) {
        if (v == null) return fallback;
        if (v instanceof BigDecimal bd) return bd;
        try { return new BigDecimal(v.toString()); } catch (Exception e) { return fallback; }
    }

    private int toInt(Object v, int fallback) {
        if (v == null) return fallback;
        if (v instanceof Integer i) return i;
        try { return Integer.parseInt(v.toString()); } catch (Exception e) { return fallback; }
    }

    private void saveError(RawCommissionDataVO raw, String type, String field, String value, String msg) {
        log.warn("MappingEngine error rawId={} type={} field={} value={}: {}",
                raw.getRawId(), type, field, value, msg);
        ParseErrorLogVO err = new ParseErrorLogVO();
        err.setRawId(raw.getRawId());
        err.setCompanyCode(raw.getCompanyCode());
        err.setErrorType(type);
        err.setErrorField(field);
        err.setErrorValue(value);
        err.setErrorMessage(msg);
        err.setResolveStatus("OPEN");
        receiveMapper.insertError(err);
    }
}
