package com.goldenchart.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.dto.UpbitApiCredentials;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.repository.GcAppSettingsRepository;
import com.goldenchart.security.SecretCryptoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * 앱 전역 차트 기본 설정 서비스.
 *
 * <p>프론트엔드에서 하드코딩된 기본값들(DEFAULT_STATE, DEFAULT_MAIN_CHART_STYLE,
 * DEFAULT_SYNC 등)을 DB 로 대체하여 사용자별 맞춤 기본값을 관리한다.</p>
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class AppSettingsService {

    private final GcAppSettingsRepository repo;
    private final ObjectMapper mapper;
    private final SecretCryptoService secretCrypto;

    // ── 공개 API ─────────────────────────────────────────────────────────────

    /**
     * 현재 설정값을 Map 으로 반환.
     * DB 에 없으면 엔티티 기본값이 반영된 빈 엔티티를 기반으로 Map 생성.
     */
    @Transactional(readOnly = true)
    public Map<String, Object> get(Long userId, String deviceId) {
        GcAppSettings s = findEntity(userId, deviceId)
                .orElse(new GcAppSettings());
        return toMap(s);
    }

    /**
     * 설정값을 Map 으로 받아 저장.
     * DB 에 레코드가 없으면 신규 생성.
     */
    public Map<String, Object> save(Long userId, String deviceId,
                                    Map<String, Object> data) {
        GcAppSettings s = findOrCreate(userId, deviceId);
        apply(s, data);
        repo.save(s);
        log.debug("[AppSettings] saved for device={}", deviceId);
        return toMap(s);
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    private Optional<GcAppSettings> findEntity(Long userId, String deviceId) {
        if (userId != null) return repo.findByUserId(userId);
        if (deviceId != null && !deviceId.isBlank()) return repo.findByDeviceId(deviceId);
        return Optional.empty();
    }

    private GcAppSettings findOrCreate(Long userId, String deviceId) {
        return findEntity(userId, deviceId)
                .orElseGet(() -> GcAppSettings.builder()
                        .userId(userId)
                        .deviceId(deviceId)
                        .build());
    }

    private void apply(GcAppSettings s, Map<String, Object> d) {
        if (d.containsKey("defaultSymbol"))    s.setDefaultSymbol((String) d.get("defaultSymbol"));
        if (d.containsKey("defaultTimeframe")) s.setDefaultTimeframe((String) d.get("defaultTimeframe"));
        if (d.containsKey("defaultChartType")) s.setDefaultChartType((String) d.get("defaultChartType"));
        if (d.containsKey("defaultTheme"))     s.setDefaultTheme((String) d.get("defaultTheme"));
        if (d.containsKey("defaultLogScale"))  s.setDefaultLogScale(
                Boolean.parseBoolean(d.get("defaultLogScale").toString()));
        if (d.containsKey("defaultLayoutId"))  s.setDefaultLayoutId((String) d.get("defaultLayoutId"));
        if (d.containsKey("displayTimezone"))  s.setDisplayTimezone((String) d.get("displayTimezone"));
        if (d.containsKey("mainChartStyle"))   s.setMainChartStyleJson(toJson(d.get("mainChartStyle")));
        if (d.containsKey("syncOptions"))      s.setSyncOptionsJson(toJson(d.get("syncOptions")));
        if (d.containsKey("btAutoPopup"))       s.setBtAutoPopup(
                Boolean.parseBoolean(d.get("btAutoPopup").toString()));
        if (d.containsKey("btShowPrice"))       s.setBtShowPrice(
                Boolean.parseBoolean(d.get("btShowPrice").toString()));
        if (d.containsKey("chartSeriesPriceLabels")) s.setChartSeriesPriceLabels(
                Boolean.parseBoolean(d.get("chartSeriesPriceLabels").toString()));
        if (d.containsKey("chartVolumeVisible")) s.setChartVolumeVisible(
                Boolean.parseBoolean(d.get("chartVolumeVisible").toString()));
        if (d.containsKey("chartLegendOptions")) s.setChartLegendOptionsJson(toJson(d.get("chartLegendOptions")));
        if (d.containsKey("tradeAlertPopup"))   s.setTradeAlertPopup(
                Boolean.parseBoolean(d.get("tradeAlertPopup").toString()));
        if (d.containsKey("tradeAlertSoundEnabled")) s.setTradeAlertSoundEnabled(
                Boolean.parseBoolean(d.get("tradeAlertSoundEnabled").toString()));
        if (d.containsKey("tradeAlertSound"))   s.setTradeAlertSound(
                d.get("tradeAlertSound").toString());
        if (d.containsKey("tradeAlertPopupPosition")) {
            String pos = d.get("tradeAlertPopupPosition").toString().toLowerCase();
            s.setTradeAlertPopupPosition("left".equals(pos) || "bottom".equals(pos) ? pos : "right");
        }
        if (d.containsKey("tradeAlertPopupLayout")) {
            String lay = d.get("tradeAlertPopupLayout").toString().toLowerCase();
            s.setTradeAlertPopupLayout(
                    "grid".equals(lay) || "strip".equals(lay) || "single".equals(lay) ? lay : "stack");
        }
        if (d.containsKey("tradeAlertPopupGridCols")) {
            int cols = Integer.parseInt(d.get("tradeAlertPopupGridCols").toString());
            s.setTradeAlertPopupGridCols(Math.min(4, Math.max(2, cols)));
        }
        if (d.containsKey("liveStrategyCheck")) s.setLiveStrategyCheck(
                Boolean.parseBoolean(d.get("liveStrategyCheck").toString()));
        if (d.containsKey("liveStrategyId")) {
            Object v = d.get("liveStrategyId");
            s.setLiveStrategyId(v == null || "".equals(v.toString()) ? null : Long.parseLong(v.toString()));
        }
        if (d.containsKey("liveExecutionType")) s.setLiveExecutionType(
                "REALTIME_TICK".equals(d.get("liveExecutionType")) ? "REALTIME_TICK" : "CANDLE_CLOSE");
        if (d.containsKey("livePositionMode"))  s.setLivePositionMode(
                "SIGNAL_ONLY".equals(d.get("livePositionMode")) ? "SIGNAL_ONLY" : "LONG_ONLY");
        if (d.containsKey("paperTradingEnabled")) s.setPaperTradingEnabled(
                Boolean.parseBoolean(d.get("paperTradingEnabled").toString()));
        if (d.containsKey("paperInitialCapital")) s.setPaperInitialCapital(
                new java.math.BigDecimal(d.get("paperInitialCapital").toString()));
        if (d.containsKey("paperFeeRatePct")) s.setPaperFeeRatePct(
                new java.math.BigDecimal(d.get("paperFeeRatePct").toString()));
        if (d.containsKey("paperSlippagePct")) s.setPaperSlippagePct(
                new java.math.BigDecimal(d.get("paperSlippagePct").toString()));
        if (d.containsKey("paperMinOrderKrw")) s.setPaperMinOrderKrw(
                new java.math.BigDecimal(d.get("paperMinOrderKrw").toString()));
        if (d.containsKey("paperAutoTradeEnabled")) s.setPaperAutoTradeEnabled(
                Boolean.parseBoolean(d.get("paperAutoTradeEnabled").toString()));
        if (d.containsKey("paperAutoTradeBudgetPct")) s.setPaperAutoTradeBudgetPct(
                new java.math.BigDecimal(d.get("paperAutoTradeBudgetPct").toString()));
        if (d.containsKey("tradingMode")) {
            String mode = d.get("tradingMode").toString().toUpperCase();
            s.setTradingMode("LIVE".equals(mode) || "BOTH".equals(mode) ? mode : "PAPER");
        }
        if (d.containsKey("liveAutoTradeEnabled")) s.setLiveAutoTradeEnabled(
                Boolean.parseBoolean(d.get("liveAutoTradeEnabled").toString()));
        if (d.containsKey("upbitAccessKey")) {
            String v = d.get("upbitAccessKey").toString().trim();
            if (!v.isEmpty() && !v.startsWith("····") && !v.startsWith("****")) {
                s.setUpbitAccessKey(secretCrypto.encrypt(v));
            }
        }
        if (d.containsKey("upbitSecretKey")) {
            String v = d.get("upbitSecretKey").toString().trim();
            if (!v.isEmpty() && !"__UNCHANGED__".equals(v)) {
                s.setUpbitSecretKey(secretCrypto.encrypt(v));
            }
        }
        if (d.containsKey("chartRealtimeSource")) {
            String src = d.get("chartRealtimeSource").toString().toUpperCase();
            s.setChartRealtimeSource("UPBIT_DIRECT".equals(src) ? "UPBIT_DIRECT" : "BACKEND_STOMP");
        }
        if (d.containsKey("liveAutoTradeBudgetPct")) s.setLiveAutoTradeBudgetPct(
                new java.math.BigDecimal(d.get("liveAutoTradeBudgetPct").toString()));
        if (d.containsKey("fcmPushEnabled")) s.setFcmPushEnabled(
                Boolean.parseBoolean(d.get("fcmPushEnabled").toString()));
    }

    private Map<String, Object> toMap(GcAppSettings s) {
        Map<String, Object> m = new HashMap<>();
        m.put("defaultSymbol",    s.getDefaultSymbol()    != null ? s.getDefaultSymbol()    : "KRW-BTC");
        m.put("defaultTimeframe", s.getDefaultTimeframe() != null ? s.getDefaultTimeframe() : "1D");
        m.put("defaultChartType", s.getDefaultChartType() != null ? s.getDefaultChartType() : "candlestick");
        m.put("defaultTheme",     s.getDefaultTheme()     != null ? s.getDefaultTheme()     : "dark");
        m.put("defaultLogScale",  s.getDefaultLogScale()  != null ? s.getDefaultLogScale()  : false);
        m.put("defaultLayoutId",  s.getDefaultLayoutId()  != null ? s.getDefaultLayoutId()  : "1");
        m.put("displayTimezone",  s.getDisplayTimezone()  != null ? s.getDisplayTimezone()  : "Asia/Seoul");
        m.put("mainChartStyle",   parseJson(s.getMainChartStyleJson()));
        m.put("syncOptions",      parseJson(s.getSyncOptionsJson()));
        m.put("btAutoPopup",      s.getBtAutoPopup()  != null ? s.getBtAutoPopup()  : true);
        m.put("btShowPrice",        s.getBtShowPrice()        != null ? s.getBtShowPrice()        : true);
        m.put("chartSeriesPriceLabels", s.getChartSeriesPriceLabels() != null ? s.getChartSeriesPriceLabels() : true);
        m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true);
        m.put("chartLegendOptions", parseJson(s.getChartLegendOptionsJson()));
        m.put("tradeAlertPopup",    s.getTradeAlertPopup()    != null ? s.getTradeAlertPopup()    : true);
        m.put("tradeAlertSoundEnabled", s.getTradeAlertSoundEnabled() != null ? s.getTradeAlertSoundEnabled() : true);
        m.put("tradeAlertSound",    s.getTradeAlertSound()    != null ? s.getTradeAlertSound()    : "bell");
        m.put("tradeAlertPopupPosition", s.getTradeAlertPopupPosition() != null ? s.getTradeAlertPopupPosition() : "right");
        m.put("tradeAlertPopupLayout",   s.getTradeAlertPopupLayout()   != null ? s.getTradeAlertPopupLayout()   : "stack");
        m.put("tradeAlertPopupGridCols", s.getTradeAlertPopupGridCols() != null ? s.getTradeAlertPopupGridCols() : 2);
        m.put("liveStrategyCheck",  s.getLiveStrategyCheck()  != null ? s.getLiveStrategyCheck()  : false);
        m.put("liveStrategyId",     s.getLiveStrategyId());
        m.put("liveExecutionType",  s.getLiveExecutionType()  != null ? s.getLiveExecutionType()  : "CANDLE_CLOSE");
        m.put("livePositionMode",   s.getLivePositionMode()   != null ? s.getLivePositionMode()   : "LONG_ONLY");
        m.put("paperTradingEnabled", s.getPaperTradingEnabled() != null ? s.getPaperTradingEnabled() : true);
        m.put("paperInitialCapital", s.getPaperInitialCapital() != null
                ? s.getPaperInitialCapital().doubleValue() : 10_000_000);
        m.put("paperFeeRatePct",    s.getPaperFeeRatePct()    != null ? s.getPaperFeeRatePct().doubleValue() : 0.05);
        m.put("paperSlippagePct",   s.getPaperSlippagePct()   != null ? s.getPaperSlippagePct().doubleValue() : 0);
        m.put("paperMinOrderKrw",   s.getPaperMinOrderKrw()   != null ? s.getPaperMinOrderKrw().doubleValue() : 5000);
        m.put("paperAutoTradeEnabled", s.getPaperAutoTradeEnabled() != null ? s.getPaperAutoTradeEnabled() : false);
        m.put("paperAutoTradeBudgetPct", s.getPaperAutoTradeBudgetPct() != null
                ? s.getPaperAutoTradeBudgetPct().doubleValue() : 95);
        m.put("tradingMode", s.getTradingMode() != null ? s.getTradingMode() : "PAPER");
        m.put("liveAutoTradeEnabled", s.getLiveAutoTradeEnabled() != null ? s.getLiveAutoTradeEnabled() : false);
        UpbitApiCredentials creds = resolveUpbitCredentials(s);
        m.put("upbitAccessKeyMasked", SecretCryptoService.maskForDisplay(creds.accessKey()));
        m.put("hasUpbitKeys", creds.isComplete());
        m.put("chartRealtimeSource", s.getChartRealtimeSource() != null ? s.getChartRealtimeSource() : "BACKEND_STOMP");
        m.put("liveAutoTradeBudgetPct", s.getLiveAutoTradeBudgetPct() != null
                ? s.getLiveAutoTradeBudgetPct().doubleValue() : 95);
        m.put("fcmPushEnabled", s.getFcmPushEnabled() != null ? s.getFcmPushEnabled() : false);
        return m;
    }

    /** 업비트 API 키 복호화 (실거래 API 호출 전용, 외부 노출 금지) */
    @Transactional(readOnly = true)
    public UpbitApiCredentials resolveUpbitCredentials(GcAppSettings s) {
        if (s == null) {
            return UpbitApiCredentials.builder().accessKey("").secretKey("").build();
        }
        String access = safeDecrypt(s.getUpbitAccessKey());
        String secret = safeDecrypt(s.getUpbitSecretKey());
        return UpbitApiCredentials.builder()
                .accessKey(access != null ? access : "")
                .secretKey(secret != null ? secret : "")
                .build();
    }

    /** encryption-key 불일치 시 API 500 대신 키 미등록으로 처리 */
    private String safeDecrypt(String stored) {
        try {
            return secretCrypto.decrypt(stored);
        } catch (Exception e) {
            log.warn("[AppSettings] 업비트 키 복호화 실패 — hasUpbitKeys=false 로 처리: {}", e.getMessage());
            return "";
        }
    }

    @Transactional(readOnly = true)
    public boolean hasUpbitApiKeys(GcAppSettings s) {
        return resolveUpbitCredentials(s).isComplete();
    }

    /** 실시간 전략 전역 템플릿 (관심종목 자동 연동용) */
    @Transactional(readOnly = true)
    public GcAppSettings getEntity(Long userId, String deviceId) {
        return findEntity(userId, deviceId).orElse(new GcAppSettings());
    }

    private Object parseJson(String json) {
        if (json == null || json.isBlank()) return null;
        try { return mapper.readValue(json, Object.class); }
        catch (Exception e) { return null; }
    }

    private String toJson(Object obj) {
        if (obj == null) return null;
        try { return mapper.writeValueAsString(obj); }
        catch (JsonProcessingException e) {
            log.warn("[AppSettings] JSON 직렬화 실패", e);
            return null;
        }
    }
}
