package com.goldenchart.service;

import com.goldenchart.dto.*;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcPaperAccount;
import com.goldenchart.entity.GcPaperPosition;
import com.goldenchart.entity.GcPaperTrade;
import com.goldenchart.repository.GcPaperAccountRepository;
import com.goldenchart.repository.GcPaperPositionRepository;
import com.goldenchart.repository.GcPaperTradeRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 모의투자(페이퍼 트레이딩) 체결·잔고·포지션 관리.
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class PaperTradingService {

    private static final int    SCALE_QTY   = 12;
    private static final int    SCALE_KRW   = 2;
    private static final RoundingMode RM   = RoundingMode.HALF_UP;

    private final GcPaperAccountRepository   accountRepo;
    private final GcPaperPositionRepository  positionRepo;
    private final GcPaperTradeRepository     tradeRepo;
    private final AppSettingsService         appSettingsService;

    // ── 조회 ─────────────────────────────────────────────────────────────────

    public PaperSummaryDto getSummary(Long userId, String deviceId, Map<String, Double> markPrices) {
        GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
        GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);

        List<GcPaperPosition> positions = positionRepo.findByAccountIdOrderBySymbolAsc(account.getId());
        double stockEval = 0;
        double unrealized = 0;
        List<PaperPositionDto> posDtos = new ArrayList<>();

        for (GcPaperPosition p : positions) {
            double qty = p.getQuantity().doubleValue();
            if (qty <= 0) continue;
            double avg = p.getAvgPrice().doubleValue();
            Double mark = markPrices != null ? markPrices.get(p.getSymbol()) : null;
            double eval = mark != null ? mark * qty : avg * qty;
            double pl = mark != null ? (mark - avg) * qty : 0;
            double plPct = avg > 0 && mark != null ? (mark - avg) / avg * 100 : 0;
            stockEval += eval;
            unrealized += pl;
            posDtos.add(PaperPositionDto.builder()
                    .id(p.getId())
                    .symbol(p.getSymbol())
                    .koreanName(p.getKoreanName())
                    .quantity(qty)
                    .avgPrice(avg)
                    .markPrice(mark)
                    .evalAmount(eval)
                    .profitLoss(pl)
                    .profitLossPct(plPct)
                    .build());
        }

        double cash = account.getCashBalance().doubleValue();
        double initial = app.getPaperInitialCapital() != null
                ? app.getPaperInitialCapital().doubleValue() : 10_000_000;
        double total = cash + stockEval;
        double returnPct = initial > 0 ? (total - initial) / initial * 100 : 0;

        return PaperSummaryDto.builder()
                .enabled(Boolean.TRUE.equals(app.getPaperTradingEnabled()))
                .initialCapital(initial)
                .cashBalance(cash)
                .stockEvalAmount(stockEval)
                .totalAsset(total)
                .unrealizedPnl(unrealized)
                .realizedPnl(account.getRealizedPnl().doubleValue())
                .totalReturnPct(returnPct)
                .feeRatePct(app.getPaperFeeRatePct() != null ? app.getPaperFeeRatePct().doubleValue() : 0.05)
                .slippagePct(app.getPaperSlippagePct() != null ? app.getPaperSlippagePct().doubleValue() : 0)
                .minOrderKrw(app.getPaperMinOrderKrw() != null ? app.getPaperMinOrderKrw().doubleValue() : 5000)
                .autoTradeEnabled(Boolean.TRUE.equals(app.getPaperAutoTradeEnabled()))
                .autoTradeBudgetPct(app.getPaperAutoTradeBudgetPct() != null
                        ? app.getPaperAutoTradeBudgetPct().doubleValue() : 95)
                .positions(posDtos)
                .build();
    }

    public List<PaperTradeDto> listTrades(Long userId, String deviceId) {
        GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
        GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
        return tradeRepo.findTop100ByAccountIdOrderByCreatedAtDesc(account.getId())
                .stream().map(this::toTradeDto).toList();
    }

    // ── 주문 ─────────────────────────────────────────────────────────────────

    public PaperTradeDto placeOrder(Long userId, String deviceId, PaperOrderRequest req) {
        GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
        if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
            throw new IllegalStateException("모의투자가 비활성화되어 있습니다. 설정에서 모의투자를 켜 주세요.");
        }
        if (req.getMarket() == null || req.getSide() == null) {
            throw new IllegalArgumentException("종목과 매수/매도 구분이 필요합니다.");
        }
        String side = req.getSide().toUpperCase();
        if (!"BUY".equals(side) && !"SELL".equals(side)) {
            throw new IllegalArgumentException("side는 BUY 또는 SELL 이어야 합니다.");
        }
        double price = req.getPrice() != null ? req.getPrice() : 0;
        if (price <= 0) {
            throw new IllegalArgumentException("가격은 0보다 커야 합니다.");
        }

        String source = req.getSource() != null ? req.getSource() : "MANUAL";
        String orderKind = req.getOrderKind() != null ? req.getOrderKind() : "limit";
        double qty = req.getQuantity() != null ? req.getQuantity() : 0;
        if (qty <= 0) {
            GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
            qty = resolveAutoQuantity(account, app, req.getMarket(), side, price,
                    req.getBudgetPct(), req.getKrwAmount());
        }
        if (qty <= 0) {
            throw new IllegalArgumentException("주문 수량을 계산할 수 없습니다.");
        }

        return executeTrade(userId, deviceId, app, req.getMarket(), side, orderKind, source,
                req.getStrategyId(), price, qty, null);
    }

    /**
     * 실시간 전략 시그널 발생 시 자동 모의매매.
     * liveStrategyCheck + paperAutoTradeEnabled + paperTradingEnabled 일 때만 실행.
     */
    public void tryExecuteOnSignal(String deviceId, Long userId, String market,
                                   Long strategyId, String signalType, double price) {
        if (deviceId == null || deviceId.isBlank()) return;
        if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return;

        GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
        if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) return;
        if (!Boolean.TRUE.equals(app.getPaperAutoTradeEnabled())) return;
        if (!Boolean.TRUE.equals(app.getLiveStrategyCheck())) return;

        try {
            GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
            double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
            double slip = pct(app.getPaperSlippagePct(), 0);
            double minOrder = app.getPaperMinOrderKrw() != null
                    ? app.getPaperMinOrderKrw().doubleValue() : 5000;

            if ("BUY".equals(signalType)) {
                double budgetPct = pct(app.getPaperAutoTradeBudgetPct(), 95);
                double cash = account.getCashBalance().doubleValue();
                double budget = cash * budgetPct / 100.0;
                double execPrice = price * (1 + slip / 100.0);
                double denom = execPrice * (1 + feeRate / 100.0);
                if (denom <= 0 || budget < minOrder) {
                    log.debug("[Paper] auto BUY skip: budget={} min={}", budget, minOrder);
                    return;
                }
                double qty = budget / denom;
                if (qty * execPrice < minOrder) return;
                executeTrade(userId, deviceId, app, market, "BUY", "market", "STRATEGY",
                        strategyId, price, qty, null);
                log.info("[Paper] STRATEGY BUY {} qty≈{} @ {}", market, qty, price);
            } else {
                GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
                        .orElse(null);
                if (pos == null || pos.getQuantity().doubleValue() <= 0) {
                    log.debug("[Paper] auto SELL skip: no position {}", market);
                    return;
                }
                double qty = pos.getQuantity().doubleValue();
                executeTrade(userId, deviceId, app, market, "SELL", "market", "STRATEGY",
                        strategyId, price, qty, null);
                log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, qty, price);
            }
        } catch (Exception e) {
            log.warn("[Paper] auto trade failed {} {}: {}", market, signalType, e.getMessage());
        }
    }

    /** 계좌 초기화 — 설정의 초기 자본으로 리셋 */
    public PaperSummaryDto resetAccount(Long userId, String deviceId) {
        GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
        GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
        BigDecimal initial = app.getPaperInitialCapital() != null
                ? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);

        positionRepo.findByAccountIdOrderBySymbolAsc(account.getId())
                .forEach(positionRepo::delete);
        account.setCashBalance(initial);
        account.setRealizedPnl(BigDecimal.ZERO);
        accountRepo.save(account);
        log.info("[Paper] account reset device={} capital={}", deviceId, initial);
        return getSummary(userId, deviceId, null);
    }

    /**
     * 수량 미입력(0) 시 자동매매와 동일한 방식으로 수량 산출.
     * 매수: 가용현금 × budgetPct(또는 krwAmount) / (체결가 × (1+수수료))
     * 매도: 보유수량 × budgetPct
     */
    private double resolveAutoQuantity(GcPaperAccount account, GcAppSettings app,
                                       String market, String side,
                                       double price, Double budgetPct, Double krwAmount) {
        double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
        double slip = pct(app.getPaperSlippagePct(), 0);
        double pctVal = budgetPct != null ? budgetPct : pct(app.getPaperAutoTradeBudgetPct(), 95);

        if ("BUY".equals(side)) {
            double cash = account.getCashBalance().doubleValue();
            double budget = krwAmount != null && krwAmount > 0
                    ? krwAmount
                    : cash * pctVal / 100.0;
            double execPrice = price * (1 + slip / 100.0);
            double denom = execPrice * (1 + feeRate / 100.0);
            if (denom <= 0 || budget <= 0) {
                throw new IllegalStateException("주문 가능 금액이 부족합니다.");
            }
            return budget / denom;
        }

        GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
                .orElseThrow(() -> new IllegalStateException("보유 수량이 없습니다."));
        double held = pos.getQuantity().doubleValue();
        if (held <= 0) {
            throw new IllegalStateException("보유 수량이 없습니다.");
        }
        return held * pctVal / 100.0;
    }

    // ── 내부 체결 ───────────────────────────────────────────────────────────

    private PaperTradeDto executeTrade(Long userId, String deviceId, GcAppSettings app,
                                       String market, String side, String orderKind, String source,
                                       Long strategyId, double inputPrice, double inputQty,
                                       String koreanName) {
        GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
        double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
        double slip = pct(app.getPaperSlippagePct(), 0);
        double minOrder = app.getPaperMinOrderKrw() != null
                ? app.getPaperMinOrderKrw().doubleValue() : 5000;

        double execPrice = "BUY".equals(side)
                ? inputPrice * (1 + slip / 100.0)
                : inputPrice * (1 - slip / 100.0);
        BigDecimal qty = BigDecimal.valueOf(inputQty).setScale(SCALE_QTY, RM);
        BigDecimal price = BigDecimal.valueOf(execPrice).setScale(SCALE_KRW, RM);
        BigDecimal gross = price.multiply(qty).setScale(SCALE_KRW, RM);
        BigDecimal fee = gross.multiply(BigDecimal.valueOf(feeRate / 100.0)).setScale(SCALE_KRW, RM);

        if (gross.doubleValue() < minOrder) {
            throw new IllegalArgumentException("최소 주문 금액은 " + (long) minOrder + " KRW 입니다.");
        }

        if ("BUY".equals(side)) {
            BigDecimal totalCost = gross.add(fee);
            if (account.getCashBalance().compareTo(totalCost) < 0) {
                throw new IllegalStateException("주문가능 현금이 부족합니다.");
            }
            account.setCashBalance(account.getCashBalance().subtract(totalCost));
            upsertPositionBuy(account, market, koreanName, qty, price);
            accountRepo.save(account);

            GcPaperTrade trade = tradeRepo.save(GcPaperTrade.builder()
                    .accountId(account.getId())
                    .symbol(market)
                    .side("BUY")
                    .orderKind(orderKind)
                    .source(source)
                    .strategyId(strategyId)
                    .price(price)
                    .quantity(qty)
                    .grossAmount(gross)
                    .feeAmount(fee)
                    .netAmount(totalCost.negate())
                    .cashAfter(account.getCashBalance())
                    .build());
            return toTradeDto(trade);
        }

        // SELL
        GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
                .orElseThrow(() -> new IllegalStateException("보유 수량이 없습니다."));
        if (pos.getQuantity().compareTo(qty) < 0) {
            throw new IllegalStateException("매도 수량이 보유 수량을 초과합니다.");
        }

        BigDecimal netProceeds = gross.subtract(fee);
        BigDecimal costBasis = pos.getAvgPrice().multiply(qty).setScale(SCALE_KRW, RM);
        BigDecimal realizedDelta = netProceeds.subtract(costBasis);
        account.setCashBalance(account.getCashBalance().add(netProceeds));
        account.setRealizedPnl(account.getRealizedPnl().add(realizedDelta));

        BigDecimal remaining = pos.getQuantity().subtract(qty);
        if (remaining.compareTo(BigDecimal.valueOf(0.00000001)) <= 0) {
            positionRepo.delete(pos);
        } else {
            pos.setQuantity(remaining);
            positionRepo.save(pos);
        }
        accountRepo.save(account);

        GcPaperTrade trade = tradeRepo.save(GcPaperTrade.builder()
                .accountId(account.getId())
                .symbol(market)
                .side("SELL")
                .orderKind(orderKind)
                .source(source)
                .strategyId(strategyId)
                .price(price)
                .quantity(qty)
                .grossAmount(gross)
                .feeAmount(fee)
                .netAmount(netProceeds)
                .cashAfter(account.getCashBalance())
                .build());
        return toTradeDto(trade);
    }

    private void upsertPositionBuy(GcPaperAccount account, String market, String koreanName,
                                   BigDecimal qty, BigDecimal price) {
        GcPaperPosition pos = positionRepo.findByAccountIdAndSymbol(account.getId(), market)
                .orElse(null);
        if (pos == null) {
            positionRepo.save(GcPaperPosition.builder()
                    .accountId(account.getId())
                    .symbol(market)
                    .koreanName(koreanName)
                    .quantity(qty)
                    .avgPrice(price)
                    .build());
            return;
        }
        BigDecimal totalQty = pos.getQuantity().add(qty);
        BigDecimal totalCost = pos.getAvgPrice().multiply(pos.getQuantity()).add(price.multiply(qty));
        BigDecimal avg = totalCost.divide(totalQty, SCALE_KRW, RM);
        pos.setQuantity(totalQty);
        pos.setAvgPrice(avg);
        if (koreanName != null) pos.setKoreanName(koreanName);
        positionRepo.save(pos);
    }

    private GcPaperAccount getOrCreateAccount(Long userId, String deviceId, GcAppSettings app) {
        String dev = deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous";
        return accountRepo.findByDeviceId(dev).orElseGet(() -> {
            BigDecimal initial = app.getPaperInitialCapital() != null
                    ? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);
            GcPaperAccount a = GcPaperAccount.builder()
                    .userId(userId)
                    .deviceId(dev)
                    .cashBalance(initial)
                    .realizedPnl(BigDecimal.ZERO)
                    .build();
            return accountRepo.save(a);
        });
    }

    private static double pct(BigDecimal v, double def) {
        return v != null ? v.doubleValue() : def;
    }

    private PaperTradeDto toTradeDto(GcPaperTrade t) {
        return PaperTradeDto.builder()
                .id(t.getId())
                .symbol(t.getSymbol())
                .side(t.getSide())
                .orderKind(t.getOrderKind())
                .source(t.getSource())
                .strategyId(t.getStrategyId())
                .price(t.getPrice().doubleValue())
                .quantity(t.getQuantity().doubleValue())
                .grossAmount(t.getGrossAmount().doubleValue())
                .feeAmount(t.getFeeAmount().doubleValue())
                .netAmount(t.getNetAmount().doubleValue())
                .cashAfter(t.getCashAfter().doubleValue())
                .createdAt(t.getCreatedAt() != null ? t.getCreatedAt().toString() : null)
                .build();
    }
}
