package com.zioinfo.kintex.analytics;

import com.zioinfo.kintex.analytics.dto.AnalyticsDto;
import com.zioinfo.kintex.analytics.dto.AnalyticsOverviewDto;
import com.zioinfo.kintex.tenant.TenantContext;
import org.springframework.stereotype.Service;

import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 경영분석 BI 서비스 — 판매면적×임대 요율로 매출을 산출하고 부스/유틸리티 실적을 조립.
 * perspective(operator|exhibitor)에 따라 필드 세트가 갈린다. 원천 부재 지표는 빈배열(정직).
 */
@Service
public class AnalyticsService {

    private final AnalyticsMapper mapper;

    public AnalyticsService(AnalyticsMapper mapper) {
        this.mapper = mapper;
    }

    public AnalyticsDto getAnalytics(String eventId, String perspective, String period) {
        boolean exhibitor = "exhibitor".equalsIgnoreCase(perspective);
        int days = periodDays(period);

        Double rate = mapper.findRentalRatePerM2();
        double ratePerM2 = rate == null ? 0.0 : rate;

        Map<String, Object> ac = mapper.findAreaAndCount(eventId);
        double salesArea = dbl(ac == null ? null : ac.get("salesArea"));
        long boothCount = lng(ac == null ? null : ac.get("boothCount"));
        long rentalRevenue = Math.round(salesArea * ratePerM2);
        long utilityRevenue = Math.round(dbl(mapper.findUtilityRevenue(eventId)));

        List<AnalyticsDto.Kpi> kpis = new ArrayList<>();
        kpis.add(new AnalyticsDto.Kpi("임대 매출", formatKrw(rentalRevenue), null, "flat"));
        kpis.add(new AnalyticsDto.Kpi("유틸리티 매출", formatKrw(utilityRevenue), null, "flat"));
        kpis.add(new AnalyticsDto.Kpi("판매 부스", String.valueOf(boothCount), null, "flat"));
        kpis.add(new AnalyticsDto.Kpi("판매 면적", Math.round(salesArea) + "㎡", null, "flat"));

        List<AnalyticsDto.TrendPoint> trend = new ArrayList<>();
        List<Map<String, Object>> trendRows = mapper.findMonthlyTrend(eventId, days);
        if (trendRows != null) {
            for (Map<String, Object> r : trendRows) {
                long area = Math.round(dbl(r.get("salesArea")));
                trend.add(new AnalyticsDto.TrendPoint(
                        str(r.get("period")), Math.round(area * ratePerM2), lng(r.get("booths"))));
            }
        }

        // 손익(operator 관점) — 실 매출 항목만. 업종 분류 원천 부재 → sectors 미집계.
        List<AnalyticsDto.PnlRow> pnl = new ArrayList<>();
        List<AnalyticsDto.Sector> sectors = new ArrayList<>();
        if (!exhibitor) {
            pnl.add(new AnalyticsDto.PnlRow("부스 임대", rentalRevenue, grade(rentalRevenue)));
            pnl.add(new AnalyticsDto.PnlRow("유틸리티", utilityRevenue, grade(utilityRevenue)));
        }

        // 참가업체 성과 — 방문자 리드(M13) 원천 부재 → 미집계(빈배열).
        List<AnalyticsDto.ExhibitorPerf> exhibitorPerf = new ArrayList<>();

        return new AnalyticsDto(kpis, trend, sectors, pnl, exhibitorPerf);
    }

    // ── 테넌트 전역(행사 횡단) BI — M16 잔여 갭 ───────────────────────────────

    /**
     * 테넌트 전역 경영분석 — 행사 간 매출 비교·홀 가동률 추이(월별)·참가사 리텐션·수요예측(추세).
     * years(1..20)는 가동률 추이 창(=years×12개월)과 수요예측 표시 기간을 결정한다.
     * 모든 지표는 기존 테이블 read-only 집계이며 AI 호출은 없다.
     */
    public AnalyticsOverviewDto getOverview(int years) {
        int y = Math.max(1, Math.min(20, years));
        int months = y * 12;

        // 행사 횡단 집계는 현재 테넌트로 격리(§1A-2 · §8-2) — 기준 테넌트('kintex') 단일 운영 시 회귀 0.
        String tenantId = TenantContext.currentTenantId();

        Double rate = mapper.findRentalRatePerM2();
        double ratePerM2 = rate == null ? 0.0 : rate;
        long hallCount = mapper.countHalls(tenantId);
        long totalEvents = mapper.countEvents(tenantId);

        // 행사 간 매출 비교(상위 8) — 임대(면적×요율)+유틸리티.
        Map<String, Long> utilByEvent = new HashMap<>();
        List<Map<String, Object>> utilRows = mapper.findUtilityRevenueByEvent(tenantId);
        if (utilRows != null) {
            for (Map<String, Object> r : utilRows) {
                utilByEvent.put(str(r.get("eventId")), Math.round(dbl(r.get("utility"))));
            }
        }
        List<AnalyticsOverviewDto.EventRevenue> eventComparison = new ArrayList<>();
        List<Map<String, Object>> ranking = mapper.findEventAreaRanking(tenantId, 8);
        if (ranking != null) {
            for (Map<String, Object> r : ranking) {
                String eventId = str(r.get("eventId"));
                long area = Math.round(dbl(r.get("areaM2")));
                long rental = Math.round(area * ratePerM2);
                long revenue = rental + utilByEvent.getOrDefault(eventId, 0L);
                eventComparison.add(new AnalyticsOverviewDto.EventRevenue(
                        eventId, str(r.get("eventName")), revenue,
                        lng(r.get("boothCount")), area, grade(revenue)));
            }
            // 매출(임대+유틸) 기준 재정렬 — 랭킹은 면적순이라 유틸 반영 후 순위가 바뀔 수 있다.
            eventComparison.sort((a, b) -> Long.compare(b.revenue(), a.revenue()));
        }

        // 홀 가동률 추이(월별) — bookedDays / (홀수 × 그달 일수) × 100.
        List<AnalyticsOverviewDto.OccupancyPoint> occupancy = new ArrayList<>();
        List<Map<String, Object>> occRows = mapper.findMonthlyEventDays(tenantId, months);
        if (occRows != null) {
            for (Map<String, Object> r : occRows) {
                String period = str(r.get("period"));
                long booked = lng(r.get("bookedDays"));
                long capacity = hallCount * daysInMonth(period);
                double pct = capacity > 0 ? (booked * 100.0) / capacity : 0.0;
                occupancy.add(new AnalyticsOverviewDto.OccupancyPoint(
                        period, round1(pct), booked, capacity));
            }
        }

        // 참가사 리텐션(재참가).
        Map<String, Object> retSum = mapper.findRetentionSummary(tenantId);
        long totalCompanies = lng(retSum == null ? null : retSum.get("totalCompanies"));
        long returningCompanies = lng(retSum == null ? null : retSum.get("returningCompanies"));
        double retentionPct = totalCompanies > 0
                ? round1((returningCompanies * 100.0) / totalCompanies) : 0.0;
        List<AnalyticsOverviewDto.RetentionYear> retByYear = new ArrayList<>();
        List<Map<String, Object>> retRows = mapper.findRetentionByYear(tenantId);
        if (retRows != null) {
            for (Map<String, Object> r : retRows) {
                long companies = lng(r.get("companies"));
                long returning = lng(r.get("returningCompanies"));
                double pct = companies > 0 ? round1((returning * 100.0) / companies) : 0.0;
                retByYear.add(new AnalyticsOverviewDto.RetentionYear(
                        str(r.get("period")), companies, returning, pct));
            }
        }
        AnalyticsOverviewDto.Retention retention = new AnalyticsOverviewDto.Retention(
                totalCompanies, returningCompanies, retentionPct, retByYear);

        // 수요예측(단순 추세) — 연도별 개최 행사 수 실측 + 최소자승 2년 투영.
        AnalyticsOverviewDto.Forecast forecast = buildForecast(mapper.findYearlyEventCounts(tenantId), y);

        // 요약 KPI.
        List<AnalyticsOverviewDto.Kpi> kpis = new ArrayList<>();
        kpis.add(new AnalyticsOverviewDto.Kpi("누적 개최 행사", String.valueOf(totalEvents), "전 기간"));
        kpis.add(new AnalyticsOverviewDto.Kpi("참가사 재참가율", retentionPct + "%",
                returningCompanies + "/" + totalCompanies + " 재참가"));
        double avgOcc = occupancy.stream().mapToDouble(AnalyticsOverviewDto.OccupancyPoint::occupancyPercent)
                .average().orElse(0.0);
        kpis.add(new AnalyticsOverviewDto.Kpi("평균 홀 가동률", round1(avgOcc) + "%", "최근 " + y + "년"));
        if (!forecast.projection().isEmpty()) {
            AnalyticsOverviewDto.ForecastPoint next = forecast.projection().get(0);
            kpis.add(new AnalyticsOverviewDto.Kpi("내년 개최 예측",
                    String.valueOf(Math.round(next.value())) + "건", next.period() + " 추세 투영"));
        }

        return new AnalyticsOverviewDto(kpis, eventComparison, occupancy, retention, forecast);
    }

    /** 연도별 실측 + 최소자승 선형 추세로 향후 2년 투영. 표시 기간은 최근 {@code years}년으로 제한. */
    private static AnalyticsOverviewDto.Forecast buildForecast(List<Map<String, Object>> yearlyRows, int years) {
        List<int[]> series = new ArrayList<>(); // [year, count]
        if (yearlyRows != null) {
            for (Map<String, Object> r : yearlyRows) {
                series.add(new int[]{(int) lng(r.get("yr")), (int) lng(r.get("eventCount"))});
            }
        }

        List<AnalyticsOverviewDto.ForecastPoint> history = new ArrayList<>();
        List<AnalyticsOverviewDto.ForecastPoint> projection = new ArrayList<>();
        if (series.isEmpty()) {
            return new AnalyticsOverviewDto.Forecast(
                    "yearlyEventCount", "linear-least-squares", history, projection);
        }

        // 최소자승(전체 실측 사용) — x=연도 인덱스.
        int n = series.size();
        double sx = 0, sy = 0, sxy = 0, sxx = 0;
        for (int i = 0; i < n; i++) {
            double x = i, yv = series.get(i)[1];
            sx += x; sy += yv; sxy += x * yv; sxx += x * x;
        }
        double denom = n * sxx - sx * sx;
        double slope = denom != 0 ? (n * sxy - sx * sy) / denom : 0.0;
        double intercept = (sy - slope * sx) / n;

        // 실측 히스토리는 최근 years년만 노출.
        int fromIdx = Math.max(0, n - years);
        for (int i = fromIdx; i < n; i++) {
            history.add(new AnalyticsOverviewDto.ForecastPoint(
                    String.valueOf(series.get(i)[0]), series.get(i)[1], false));
        }
        // 향후 2년 투영(음수 방지).
        int lastYear = series.get(n - 1)[0];
        for (int k = 1; k <= 2; k++) {
            double predicted = intercept + slope * (n - 1 + k);
            projection.add(new AnalyticsOverviewDto.ForecastPoint(
                    String.valueOf(lastYear + k), round1(Math.max(0.0, predicted)), true));
        }
        return new AnalyticsOverviewDto.Forecast(
                "yearlyEventCount", "linear-least-squares", history, projection);
    }

    /** "YYYY-MM" → 그 달의 일수. 파싱 실패 시 30(보수적). */
    private static int daysInMonth(String period) {
        try {
            return YearMonth.parse(period).lengthOfMonth();
        } catch (RuntimeException e) {
            return 30;
        }
    }

    private static double round1(double v) {
        return Math.round(v * 10.0) / 10.0;
    }

    private static int periodDays(String period) {
        if (period == null) {
            return 0;
        }
        return switch (period.toLowerCase()) {
            case "d90" -> 90;
            case "annual" -> 365;
            default -> 0; // event = 전체 기간
        };
    }

    /** 금액 등급 — 억 단위 임계(단순·결정적). 원천 임계 룰셋 확정 시 교체. */
    private static String grade(long amount) {
        if (amount >= 1_000_000_000L) return "A";
        if (amount >= 100_000_000L) return "B";
        return "C";
    }

    /** 원화 축약 표기(억/만). */
    private static String formatKrw(long won) {
        if (won >= 100_000_000L) {
            double eok = won / 100_000_000.0;
            return "₩" + trim(eok) + "억";
        }
        if (won >= 10_000L) {
            double man = won / 10_000.0;
            return "₩" + trim(man) + "만";
        }
        return "₩" + won;
    }

    private static String trim(double v) {
        double r = Math.round(v * 100.0) / 100.0;
        if (r == Math.floor(r)) {
            return String.valueOf((long) r);
        }
        return String.valueOf(r);
    }

    private static double dbl(Object o) {
        if (o == null) return 0.0;
        if (o instanceof Number n) return n.doubleValue();
        try {
            return Double.parseDouble(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0.0;
        }
    }

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

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