package jnpf.attendance.schedule;

import jnpf.model.attendance.vo.DayShiftRevenueStatVo;
import jnpf.model.attendance.vo.ShiftPostHeadcountVo;
import jnpf.model.attendance.vo.ShiftPostStatVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 将「相似历史日」对应的 {@link DayShiftRevenueStatVo} 班次，拆成半小时粒度、按岗位人数展开（表结构：日期 | 时间段 | 岗位 | 人数）。
 * <p>
 * 入参/出参字段级详细日志见 {@link #buildHalfHourRowsFromSuccessfulSimilarDays}；可通过系统属性限制条数或把逐条降到 DEBUG（见类内说明）。
 */
public final class SimilarHistoricalDaySlotTableBuilder {

    private static final Logger LOG = LoggerFactory.getLogger(SimilarHistoricalDaySlotTableBuilder.class);

    private static final String P = "[SimilarHistoricalDaySlotTableBuilder]";

    /**
     * 入参 {@code fullDayShiftHistory} 最多打印多少条「日维度」明细（DEBUG）。
     * 系统属性：{@code jnpf.schedule.similarSlotLogInputHistoryMax}；
     * 默认 {@code 0}（仅汇总）；{@code <0} 不限制；{@code >0} 最多该条数。
     */

    /**
     * 入参 map 最多打印多少条 entry 明细（DEBUG）。系统属性：{@code jnpf.schedule.similarSlotLogInputMapMax}；默认 {@code 0}。
     */

    /**
     * 出参最多打印多少行字段明细（DEBUG）。系统属性：{@code jnpf.schedule.similarSlotLogOutputRowsMax}；默认 {@code 0}。
     */

    /**
     * 每个相似历史日最多打印多少条 {@code expand_shift}（DEBUG）。
     * 系统属性：{@code jnpf.schedule.similarSlotLogExpandShiftMax}；默认 {@code 0}（不逐班次）；{@code <0} 不限制；{@code >0} 上限。
     */

    private static final long SLOT_NANOS = 30L * 60 * 1_000_000_000L;
    private static final long NANOS_PER_DAY = 24L * 60 * 60 * 1_000_000_000L;

    private SimilarHistoricalDaySlotTableBuilder() {}

    /**
     * 仅处理 {@link ScheduleTemplateSimilarDaysResult#isSuccess()} 为 true 的排班日；对每个相似历史日，
     * 从 {@code fullDayShiftHistory} 中按 {@code day} 匹配 VO，再拆班次为半小时 × 岗位人数行。
     *
     * @param fullDayShiftHistory              与 {@link jnpf.attendance.service.AttendanceDailyRuleService#getGroupShiftHistory90Days} 同源列表
     * @param similarTemplateDaysByScheduleDay 排班日 → 模板参考日筛选结果
     */
    public static List<SimilarHistoricalJobSlotRow> buildHalfHourRowsFromSuccessfulSimilarDays(
            List<DayShiftRevenueStatVo> fullDayShiftHistory,
            Map<LocalDate, ScheduleTemplateSimilarDaysResult> similarTemplateDaysByScheduleDay) {
        logInputFullDetail(fullDayShiftHistory, similarTemplateDaysByScheduleDay);

        if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) {
            LOG.error(
                    "[SimilarHistoricalDaySlotTableBuilder] STEP | early_exit | reason=similarTemplateDaysByScheduleDay is null or empty");
            logOutputFullDetail(Collections.emptyList());
            return Collections.emptyList();
        }

        Map<String, DayShiftRevenueStatVo> byDay = indexByDay(fullDayShiftHistory);

        List<SimilarHistoricalJobSlotRow> out = new ArrayList<>();
        int skippedNullResult = 0;
        int skippedNotSuccess = 0;
        int similarDateNull = 0;
        int missingHistoryVo = 0;
        int missingShiftsList = 0;
        int scheduleDaysProcessed = 0;
        int similarDatesExpanded = 0;
        int shiftsProcessed = 0;
        int rowsFromShifts = 0;

        for (Map.Entry<LocalDate, ScheduleTemplateSimilarDaysResult> e : similarTemplateDaysByScheduleDay.entrySet()) {
            LocalDate scheduleDay = e.getKey();
            ScheduleTemplateSimilarDaysResult sim = e.getValue();
            if (sim == null) {
                skippedNullResult++;
                LOG.error("{} STEP | skip_schedule_day | scheduleDay={} | reason=result_null", P, scheduleDay);
                continue;
            }
            if (!sim.isSuccess()) {
                skippedNotSuccess++;
                LOG.error(
                        "{} STEP | skip_schedule_day | scheduleDay={} | success=false | tier={} | message={}",
                        P,
                        scheduleDay,
                        sim.getTier(),
                        sim.getMessage());
                continue;
            }
            scheduleDaysProcessed++;
            LOG.error(
                    "{} STEP | process_schedule_day | scheduleDay={} | tier={} | similarDates={}",
                    P,
                    scheduleDay,
                    sim.getTier(),
                    sim.getSimilarDates());

            for (LocalDate similarDate : sim.getSimilarDates()) {
                if (similarDate == null) {
                    similarDateNull++;
                    LOG.error(
                            "{} STEP | skip_similar_date | scheduleDay={} | reason=similarDate_null", P, scheduleDay);
                    continue;
                }
                DayShiftRevenueStatVo vo = byDay.get(similarDate.toString());
                if (vo == null) {
                    missingHistoryVo++;
                    LOG.error(
                            "{} STEP | skip_similar_date | scheduleDay={} | similarDate={} | reason=no_day_vo_in_history_index",
                            P,
                            scheduleDay,
                            similarDate);
                    continue;
                }
                if (vo.getShifts() == null) {
                    missingShiftsList++;
                    LOG.error(
                            "{} STEP | skip_similar_date | scheduleDay={} | similarDate={} | reason=shifts_null",
                            P,
                            scheduleDay,
                            similarDate);
                    continue;
                }
                similarDatesExpanded++;
                int rowsThisSimilarDate = 0;
                int shiftLogCap = similarSlotLogExpandShiftMax();
                int shiftLogged = 0;
                List<ShiftPostStatVo> shifts = vo.getShifts();
                LOG.error(
                        "{} STEP | expand_similar_date | scheduleDay={} | similarDate={} | shiftCount={}",
                        P,
                        scheduleDay,
                        similarDate,
                        shifts.size());

                for (ShiftPostStatVo shift : shifts) {
                    shiftsProcessed++;
                    List<SimilarHistoricalJobSlotRow> piece = expandShiftToHalfHourRows(similarDate, shift, scheduleDay);
                    rowsFromShifts += piece.size();
                    rowsThisSimilarDate += piece.size();
                    out.addAll(piece);
                    if (shouldLogExpandShiftDetail(shiftLogCap, shiftLogged)) {
                        LOG.error(
                                "{} STEP | expand_shift | similarDate={} | shiftId={} | shiftName={} | startTime={} | endTime={} | lineSchedule={} | rowsAdded={}",
                                P,
                                similarDate,
                                shift == null ? null : shift.getShiftId(),
                                shift == null ? null : shift.getShiftName(),
                                shift == null ? null : shift.getStartTime(),
                                shift == null ? null : shift.getEndTime(),
                                shift == null ? null : shift.getLineSchedule(),
                                piece.size());
                        shiftLogged++;
                    }
                }
                LOG.error(
                        "{} STEP | expand_similar_date_done | scheduleDay={} | similarDate={} | shifts={} | rowsAdded={}",
                        P,
                        scheduleDay,
                        similarDate,
                        shifts.size(),
                        rowsThisSimilarDate);
            }
        }

        LOG.error(
                "{} done | historyDays={} | scheduleDaysProcessed={} | similarDatesExpanded={} | shiftsProcessed={} | outputRows={} | skipped(nullResult={}, notSuccess={}, similarDateNull={}, missingHistoryVo={}, missingShiftsList={})",
                P,
                byDay.size(),
                scheduleDaysProcessed,
                similarDatesExpanded,
                shiftsProcessed,
                out.size(),
                skippedNullResult,
                skippedNotSuccess,
                similarDateNull,
                missingHistoryVo,
                missingShiftsList);
        LOG.error(
                "{} STEP | aggregate | rowsFromShifts={}",
                P,
                rowsFromShifts);
        logOutputFullDetail(out);

        return out;
    }

    /**
     * {@link ScheduleTemplateSimilarDaysResult#isSuccess()} 为 true，但聚合后无任何 {@link SimilarHistoricalJobSlotRow}
     * （召回到的全部参考日的班次结构不可用或全部被解析过滤）。仅这些排班日在后续需求矩阵中为「假性成功」，需按日对用户提示；
     * 见 intelligent-schedule-product-constraints_reply §2.4。
     */
    public static List<LocalDate> listScheduleDaysWithSuccessfulSimilarityButZeroHalfHourRows(
            Map<LocalDate, ScheduleTemplateSimilarDaysResult> similarTemplateDaysByScheduleDay,
            List<SimilarHistoricalJobSlotRow> halfHourRows) {
        if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) {
            return Collections.emptyList();
        }
        HashSet<LocalDate> templateDaysWithRows = new HashSet<>();
        if (halfHourRows != null) {
            for (SimilarHistoricalJobSlotRow r : halfHourRows) {
                if (r != null && r.getTemplateScheduleDay() != null) {
                    templateDaysWithRows.add(r.getTemplateScheduleDay());
                }
            }
        }
        List<LocalDate> missing = new ArrayList<>();
        for (Map.Entry<LocalDate, ScheduleTemplateSimilarDaysResult> e : similarTemplateDaysByScheduleDay.entrySet()) {
            LocalDate scheduleDay = e.getKey();
            ScheduleTemplateSimilarDaysResult sim = e.getValue();
            if (scheduleDay == null || sim == null || !sim.isSuccess()) {
                continue;
            }
            if (!templateDaysWithRows.contains(scheduleDay)) {
                missing.add(scheduleDay);
            }
        }
        Collections.sort(missing);
        return missing;
    }

    private static int intSysProp(String key, int defaultValue) {
        String s = System.getProperty(key);
        if (s == null || s.trim().isEmpty()) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(s.trim());
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    private static int similarSlotLogInputHistoryMax() {
        return intSysProp("jnpf.schedule.similarSlotLogInputHistoryMax", 0);
    }

    private static int similarSlotLogInputMapMax() {
        return intSysProp("jnpf.schedule.similarSlotLogInputMapMax", 0);
    }

    private static int similarSlotLogOutputRowsMax() {
        return intSysProp("jnpf.schedule.similarSlotLogOutputRowsMax", 0);
    }

    private static int similarSlotLogExpandShiftMax() {
        return intSysProp("jnpf.schedule.similarSlotLogExpandShiftMax", 0);
    }

    /** {@code maxCfg < 0} 不限制；{@code 0} 不打印；{@code >0} 最多该条数。 */
    private static boolean shouldLogExpandShiftDetail(int maxCfg, int alreadyLogged) {
        if (maxCfg < 0) {
            return true;
        }
        if (maxCfg == 0) {
            return false;
        }
        return alreadyLogged < maxCfg;
    }

    /**
     * @param configuredMax 系统属性取值：{@code <0} 不限制；{@code 0} 不输出逐条；{@code >0} 上限
     */
    private static int detailLogCap(int configuredMax, int naturalCount) {
        if (configuredMax < 0) {
            return naturalCount;
        }
        if (configuredMax == 0) {
            return 0;
        }
        return Math.min(naturalCount, configuredMax);
    }

    private static void logInputFullDetail(
            List<DayShiftRevenueStatVo> fullDayShiftHistory,
            Map<LocalDate, ScheduleTemplateSimilarDaysResult> similarTemplateDaysByScheduleDay) {
        if (!LOG.isErrorEnabled()) {
            return;
        }
        LOG.error("{} INPUT | begin | fullDayShiftHistory", P);
        if (fullDayShiftHistory == null) {
            LOG.error("{} INPUT | fullDayShiftHistory | value=null", P);
        } else {
            int n = fullDayShiftHistory.size();
            int maxCfg = similarSlotLogInputHistoryMax();
            int cap = detailLogCap(maxCfg, n);
            LOG.error(
                    "{} INPUT | fullDayShiftHistory | listSize={} | detailLoggedItems={} | limitProperty=jnpf.schedule.similarSlotLogInputHistoryMax (<0=unlimited,0=summary_only,>0=cap)",
                    P,
                    n,
                    cap);
            if (maxCfg > 0 && n > cap) {
                LOG.error(
                        "{} INPUT | fullDayShiftHistory | truncated | notLoggedItemCount={} | raise jnpf.schedule.similarSlotLogInputHistoryMax or set to <0 for unlimited",
                        P,
                        n - cap);
            }
            for (int i = 0; i < cap; i++) {
                DayShiftRevenueStatVo vo = fullDayShiftHistory.get(i);
                if (vo == null) {
                    LOG.error("{} INPUT | fullDayShiftHistory[{}] | value=null", P, i);
                    continue;
                }
                BigDecimal rev = vo.getRevenue();
                LOG.error(
                        "{} INPUT | fullDayShiftHistory[{}] | day={} | revenue={}",
                        P,
                        i,
                        vo.getDay(),
                        rev == null ? "null" : rev.toPlainString());
                List<ShiftPostStatVo> shifts = vo.getShifts();
                if (shifts == null) {
                    LOG.error("{} INPUT | fullDayShiftHistory[{}] | shifts=null", P, i);
                    continue;
                }
                LOG.error("{} INPUT | fullDayShiftHistory[{}] | shifts.size={}", P, i, shifts.size());
                for (int j = 0; j < shifts.size(); j++) {
                    ShiftPostStatVo sh = shifts.get(j);
                    if (sh == null) {
                        LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | value=null", P, i, j);
                        continue;
                    }
                    LOG.error(
                            "{} INPUT | fullDayShiftHistory[{}].shifts[{}] | shiftId={} | shiftName={} | startTime={} | endTime={} | lineSchedule={}",
                            P,
                            i,
                            j,
                            sh.getShiftId(),
                            sh.getShiftName(),
                            sh.getStartTime(),
                            sh.getEndTime(),
                            sh.getLineSchedule());
                    List<ShiftPostHeadcountVo> posts = sh.getPosts();
                    if (posts == null) {
                        LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | posts=null", P, i, j);
                        continue;
                    }
                    LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | posts.size={}", P, i, j, posts.size());
                    for (int k = 0; k < posts.size(); k++) {
                        ShiftPostHeadcountVo p = posts.get(k);
                        if (p == null) {
                            LOG.error(
                                    "{} INPUT | fullDayShiftHistory[{}].shifts[{}].posts[{}] | value=null",
                                    P, i, j, k);
                            continue;
                        }
                        LOG.error(
                                "{} INPUT | fullDayShiftHistory[{}].shifts[{}].posts[{}] | postId={} | headcount={}",
                                P,
                                i,
                                j,
                                k,
                                p.getPostId(),
                                p.getHeadcount());
                    }
                }
            }
        }

        LOG.error("{} INPUT | begin | similarTemplateDaysByScheduleDay", P);
        if (similarTemplateDaysByScheduleDay == null) {
            LOG.error("{} INPUT | similarTemplateDaysByScheduleDay | value=null", P);
        } else {
            int m = similarTemplateDaysByScheduleDay.size();
            int maxMapCfg = similarSlotLogInputMapMax();
            int mapCap = detailLogCap(maxMapCfg, m);
            LOG.error(
                    "{} INPUT | similarTemplateDaysByScheduleDay | mapSize={} | detailLoggedEntries={} | limitProperty=jnpf.schedule.similarSlotLogInputMapMax (<0=unlimited,0=summary_only,>0=cap)",
                    P,
                    m,
                    mapCap);
            if (maxMapCfg > 0 && m > mapCap) {
                LOG.error(
                        "{} INPUT | similarTemplateDaysByScheduleDay | truncated | notLoggedEntryCount={}",
                        P,
                        m - mapCap);
            }
            int ei = 0;
            for (Map.Entry<LocalDate, ScheduleTemplateSimilarDaysResult> e : similarTemplateDaysByScheduleDay.entrySet()) {
                if (ei >= mapCap) {
                    break;
                }
                LocalDate scheduleDay = e.getKey();
                ScheduleTemplateSimilarDaysResult r = e.getValue();
                LOG.error(
                        "{} INPUT | similarTemplateDaysByScheduleDay[{}] | mapKey.scheduleDay={}",
                        P,
                        ei,
                        scheduleDay);
                if (r == null) {
                    LOG.error("{} INPUT | similarTemplateDaysByScheduleDay[{}] | value=null", P, ei);
                } else {
                    LOG.error(
                            "{} INPUT | similarTemplateDaysByScheduleDay[{}] | success={} | tier={} | message={}",
                            P,
                            ei,
                            r.isSuccess(),
                            r.getTier(),
                            r.getMessage());
                    List<LocalDate> sims = r.getSimilarDates();
                    LOG.error(
                            "{} INPUT | similarTemplateDaysByScheduleDay[{}] | similarDates.size={}",
                            P,
                            ei,
                            sims == null ? "null" : sims.size());
                    if (sims != null) {
                        for (int si = 0; si < sims.size(); si++) {
                            LOG.error(
                                    "{} INPUT | similarTemplateDaysByScheduleDay[{}] | similarDates[{}]={}",
                                    P,
                                    ei,
                                    si,
                                    sims.get(si));
                        }
                    }
                }
                ei++;
            }
        }
        LOG.error("{} INPUT | end", P);
    }

    private static void logOutputFullDetail(List<SimilarHistoricalJobSlotRow> out) {
        if (!LOG.isErrorEnabled()) {
            return;
        }
        LOG.error("{} OUTPUT | begin", P);
        if (out == null) {
            LOG.error("{} OUTPUT | list=null", P);
            LOG.error("{} OUTPUT | end", P);
            return;
        }
        int n = out.size();
        int maxOutCfg = similarSlotLogOutputRowsMax();
        int cap = detailLogCap(maxOutCfg, n);
        LOG.error(
                "{} OUTPUT | rowCount={} | detailLoggedRows={} | limitProperty=jnpf.schedule.similarSlotLogOutputRowsMax (<0=unlimited,0=summary_only,>0=cap)",
                P,
                n,
                cap);
        if (maxOutCfg > 0 && n > cap) {
            LOG.error("{} OUTPUT | truncated | notLoggedRowCount={}", P, n - cap);
        }
        for (int i = 0; i < cap; i++) {
            SimilarHistoricalJobSlotRow r = out.get(i);
            if (r == null) {
                LOG.error("{} OUTPUT | rows[{}] | value=null", P, i);
                continue;
            }
            LOG.error(
                    "{} OUTPUT | rows[{}] | rowDate={} | slotStartInclusive={} | slotEndExclusive={} | slotRangeLabel={} | postId={} | headcount={} | shiftId={} | templateScheduleDay={}",
                    P,
                    i,
                    r.getRowDate(),
                    r.getSlotStartInclusive(),
                    r.getSlotEndExclusive(),
                    r.getSlotRangeLabel(),
                    r.getPostId(),
                    r.getHeadcount(),
                    r.getShiftId(),
                    r.getTemplateScheduleDay());
        }
        LOG.error("{} OUTPUT | end", P);
    }

    private static Map<String, DayShiftRevenueStatVo> indexByDay(List<DayShiftRevenueStatVo> list) {
        Map<String, DayShiftRevenueStatVo> map = new LinkedHashMap<>();
        if (list == null) {
            return map;
        }
        for (DayShiftRevenueStatVo vo : list) {
            if (vo != null && vo.getDay() != null && !vo.getDay().trim().isEmpty()) {
                String trimmed = vo.getDay().trim();
                String key = AttendanceScheduleDayParse.normalizeToIsoDayString(trimmed).orElse(trimmed);
                map.putIfAbsent(key, vo);
            }
        }
        return map;
    }

    private static List<SimilarHistoricalJobSlotRow> expandShiftToHalfHourRows(
            LocalDate rowDate, ShiftPostStatVo shift, LocalDate templateScheduleDay) {
        if (shift == null) {
            return Collections.emptyList();
        }
        LocalTime start = parseHhMm(shift.getStartTime());
        LocalTime end = parseHhMm(shift.getEndTime());
        if (start == null || end == null) {
            return Collections.emptyList();
        }
        if (start.equals(end)) {
            return Collections.emptyList();
        }
        long sn = start.toNanoOfDay();
        long en = end.toNanoOfDay();
        List<SimilarHistoricalJobSlotRow> out = new ArrayList<>();
        if (en > sn) {
            out.addAll(expandNanoRange(rowDate, sn, en, shift, templateScheduleDay));
        } else {
            out.addAll(expandNanoRange(rowDate, sn, NANOS_PER_DAY, shift, templateScheduleDay));
            out.addAll(expandNanoRange(rowDate.plusDays(1), 0, en, shift, templateScheduleDay));
        }
        return out;
    }

    /** 左闭右开区间 [startNanoInclusive, endNanoExclusive)，且均在同一自然日内（{@code endNanoExclusive ≤ 24h}）。 */
    private static List<SimilarHistoricalJobSlotRow> expandNanoRange(
            LocalDate rowDate,
            long startNanoInclusive,
            long endNanoExclusive,
            ShiftPostStatVo shift,
            LocalDate templateScheduleDay) {
        if (endNanoExclusive <= startNanoInclusive) {
            return Collections.emptyList();
        }
        List<SimilarHistoricalJobSlotRow> rows = new ArrayList<>();
        String sid = Objects.toString(shift.getShiftId(), "");
        for (long t = startNanoInclusive; t < endNanoExclusive; t += SLOT_NANOS) {
            long rawEnd = Math.min(t + SLOT_NANOS, endNanoExclusive);
            LocalTime st = LocalTime.ofNanoOfDay(t);
            LocalTime enTime =
                    rawEnd >= NANOS_PER_DAY ? LocalTime.MIDNIGHT : LocalTime.ofNanoOfDay(rawEnd);
            addPostRows(rows, rowDate, st, enTime, shift.getPosts(), sid, templateScheduleDay);
        }
        return rows;
    }

    private static void addPostRows(
            List<SimilarHistoricalJobSlotRow> out,
            LocalDate rowDate,
            LocalTime slotStart,
            LocalTime slotEnd,
            List<ShiftPostHeadcountVo> posts,
            String shiftId,
            LocalDate templateScheduleDay) {
        if (posts == null) {
            return;
        }
        for (ShiftPostHeadcountVo p : posts) {
            if (p == null) {
                continue;
            }
            int hc = p.getHeadcount() == null ? 0 : p.getHeadcount();
            if (hc <= 0) {
                continue;
            }
            out.add(
                    new SimilarHistoricalJobSlotRow(
                            rowDate, slotStart, slotEnd, p.getPostId(), hc, shiftId, templateScheduleDay));
        }
    }

    private static LocalTime parseHhMm(String hhMm) {
        if (hhMm == null) {
            return null;
        }
        String s = hhMm.trim();
        if (s.isEmpty()) {
            return null;
        }
        try {
            return LocalTime.parse(s);
        } catch (DateTimeParseException e) {
            return null;
        }
    }
}
