package com.zioinfo.kintex.marketing;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.marketing.dto.MarketingDtos.*;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * M12 EDM·캠페인 · 스폰서십 서비스.
 * <p>★ 캠페인 실 발송은 미구현 — 생성 시 status 전이만 결정한다(즉시=draft, 예약=scheduled). 실 발송 게이트웨이 연동은 후속 갭.
 */
@Service
public class MarketingService {

    private static final ObjectMapper JSON = new ObjectMapper();

    /** EDM 회당 발송 상한(가드) 및 배치 크기. */
    private static final int SEND_CAP = 500;
    private static final int BATCH_SIZE = 50;

    private final MarketingMapper mapper;
    private final com.zioinfo.kintex.mail.MailService mailService;
    private final com.zioinfo.kintex.mail.MailProperties mailProperties;

    public MarketingService(MarketingMapper mapper,
                            com.zioinfo.kintex.mail.MailService mailService,
                            com.zioinfo.kintex.mail.MailProperties mailProperties) {
        this.mapper = mapper;
        this.mailService = mailService;
        this.mailProperties = mailProperties;
    }

    // ── 캠페인 ──
    public List<CampaignDto> campaigns(String eventId, String status) {
        List<Map<String, Object>> rows = mapper.findCampaigns(eventId, blankToNull(status));
        List<CampaignDto> out = new ArrayList<>(rows == null ? 0 : rows.size());
        if (rows != null) {
            for (Map<String, Object> r : rows) {
                out.add(new CampaignDto(
                        str(r.get("id")), str(r.get("name")), str(r.get("status")), intVal(r.get("audience")),
                        str(r.get("meta")), str(r.get("openRate")), str(r.get("clickRate"))));
            }
        }
        return out;
    }

    public CampaignDto createCampaign(String eventId, CampaignCreateRequest req) {
        String name = trimOrNull(req.name());
        if (name == null) {
            throw new ApiException(ErrorCode.VALIDATION, "캠페인 이름을 입력해 주세요.");
        }
        if (mapper.eventExists(eventId) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "존재하지 않는 행사입니다.");
        }
        boolean later = "later".equalsIgnoreCase(str(req.scheduleType()))
                && req.scheduledAt() != null && !req.scheduledAt().isBlank();
        String status = later ? "scheduled" : "draft";
        int audience = req.audience() == null ? 0 : Math.max(req.audience(), 0);
        String meta = later ? "발송 예정: " + req.scheduledAt() : "최종 수정: 방금 전";
        String id = "cp-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        // 빈 문자열로 전달(널 바인딩 회피) — 매퍼가 '' 를 NULL scheduled_at 으로 처리.
        mapper.insertCampaign(id, eventId, name, status, audience, meta,
                later ? req.scheduledAt() : "");
        return new CampaignDto(id, name, status, audience, meta, "-", "-");
    }

    /**
     * 캠페인 실 발송(수신동의자 한정). 정보통신망법 준수: agree_marketing=true + 이메일 보유자만,
     * 회당 상한 {@value #SEND_CAP} 건, {@value #BATCH_SIZE} 건 배치. 상태를 done 으로 전이하고 발송 건수를 기록한다.
     * 수신자 원문 이메일은 발송 경로 한정 — 응답/로그에는 건수만 노출한다(§0-3).
     */
    public CampaignSendResult sendCampaign(String eventId, String campaignId) {
        Map<String, Object> c = mapper.findCampaign(eventId, campaignId);
        if (c == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "존재하지 않는 캠페인입니다.");
        }
        String status = str(c.get("status"));
        if ("sending".equalsIgnoreCase(status) || "done".equalsIgnoreCase(status)) {
            throw new ApiException(ErrorCode.CONFLICT, "이미 발송되었거나 발송 중인 캠페인입니다.");
        }
        String name = str(c.get("name"));
        String eventName = null;

        List<Map<String, Object>> recipients = mapper.listMarketingRecipients(eventId, SEND_CAP);
        List<String> emails = new ArrayList<>();
        if (recipients != null) {
            for (Map<String, Object> r : recipients) {
                String e = str(r.get("email"));
                if (e != null && !e.isBlank()) {
                    emails.add(e);
                }
            }
        }

        String subject = "[KINTEX] " + (name == null ? "행사 소식" : name);
        String html = com.zioinfo.kintex.mail.MailTemplates.edm(name, eventName == null ? "KINTEX" : eventName);

        int sent = 0;
        // 50건 배치로 분할 발송(각 건은 MailService @Async·실패 격리). MAIL_ENABLED=false면 내부에서 스킵.
        for (List<String> batch : com.zioinfo.kintex.mail.MailBatch.partition(emails, BATCH_SIZE)) {
            for (String email : batch) {
                mailService.send("EDM", campaignId, email, subject, html);
                sent++;
            }
        }

        boolean mailEnabled = mailProperties.isEnabled();
        String meta = mailEnabled
                ? "발송 완료: " + sent + "건 (수신동의자)"
                : "발송 대상 " + sent + "건 (MAIL 비활성 — 실 전송 없음)";
        mapper.markCampaignSent(eventId, campaignId, "done", sent, meta);
        return new CampaignSendResult(campaignId, "done", sent, mailEnabled, meta);
    }

    // ── 스폰서십 ──
    public SponsorshipView sponsorship(String eventId) {
        List<Map<String, Object>> pkgRows = mapper.findPackages(eventId);
        List<Map<String, Object>> sponsorRows = mapper.findSponsors(eventId);
        Map<String, Object> counts = mapper.findSponsorshipCounts(eventId);
        if (counts == null) counts = Map.of();

        List<TierDto> tiers = new ArrayList<>();
        long soldValue = 0;
        if (pkgRows != null) {
            for (Map<String, Object> p : pkgRows) {
                long total = lng(p.get("totalQty"));
                long sold = lng(p.get("soldQty"));
                long price = lng(p.get("price"));
                int remaining = (int) Math.max(total - sold, 0);
                soldValue += price * sold;
                tiers.add(new TierDto(
                        str(p.get("id")), str(p.get("name")), str(p.get("nameKo")),
                        formatWon(price), str(p.get("accent")), parseStrings(p.get("benefits")),
                        remaining, remaining <= 0 ? "soldout" : "available",
                        p.get("dday") == null ? null : intVal(p.get("dday"))));
            }
        }

        List<SponsorDto> sponsors = new ArrayList<>();
        long fulfillTotal = 0, fulfillDone = 0;
        if (sponsorRows != null) {
            for (Map<String, Object> s : sponsorRows) {
                List<Fulfillment> f = parseFulfillment(s.get("fulfillment"));
                for (Fulfillment item : f) {
                    fulfillTotal++;
                    if (item.done()) fulfillDone++;
                }
                String name = str(s.get("name"));
                sponsors.add(new SponsorDto(
                        str(s.get("id")), name,
                        (name == null || name.isEmpty()) ? "?" : name.substring(0, 1),
                        str(s.get("tierLabel")), str(s.get("contractStatus")), f));
            }
        }

        List<MktKpi> kpis = new ArrayList<>();
        kpis.add(new MktKpi("스폰서", String.valueOf(lng(counts.get("sponsorCount"))), "사", "primary"));
        kpis.add(new MktKpi("판매액", formatMillion(soldValue), null, "primary"));
        kpis.add(new MktKpi("잔여 패키지", String.valueOf(lng(counts.get("remainingPackages"))), "개", "ai"));
        kpis.add(new MktKpi("이행물 진행", fulfillTotal > 0 ? String.valueOf(Math.round(fulfillDone * 100.0 / fulfillTotal)) : "0",
                "%", "success"));

        return new SponsorshipView(kpis, tiers, sponsors);
    }

    // ── helpers ──
    private List<String> parseStrings(Object json) {
        if (json == null) return List.of();
        try {
            return JSON.readValue(String.valueOf(json), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {
            });
        } catch (Exception e) {
            return List.of();
        }
    }

    private List<Fulfillment> parseFulfillment(Object json) {
        if (json == null) return List.of();
        try {
            List<Map<String, Object>> raw = JSON.readValue(String.valueOf(json),
                    new com.fasterxml.jackson.core.type.TypeReference<List<Map<String, Object>>>() {
                    });
            List<Fulfillment> out = new ArrayList<>(raw.size());
            for (Map<String, Object> m : raw) {
                out.add(new Fulfillment(str(m.get("id")), str(m.get("label")),
                        m.get("done") instanceof Boolean b ? b : Boolean.parseBoolean(str(m.get("done")))));
            }
            return out;
        } catch (Exception e) {
            return List.of();
        }
    }

    private static String formatWon(long won) {
        return "₩" + String.format("%,d", won);
    }

    private static String formatMillion(long won) {
        double m = won / 1_000_000.0;
        return "₩" + String.format("%.1f", m) + "M";
    }

    private static String blankToNull(String s) {
        return s == null || s.isBlank() ? null : s;
    }

    private static String trimOrNull(String s) {
        if (s == null) return null;
        String t = s.trim();
        return t.isEmpty() ? null : t;
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    private static long lng(Object o) {
        if (o == null) return 0;
        if (o instanceof Number n) return n.longValue();
        try {
            return Long.parseLong(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static int intVal(Object o) {
        return (int) lng(o);
    }
}
