package jnpf.attendance.schedule;

import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;

/**
 * 智能排班<strong>第九、十步</strong>的「单上下文」编排入口（与 {@link ShiftPlanFinalStaffAssigner} 并行存在）。
 * <p>
 * <b>差异</b>：本类对整批候选人使用<strong>同一个</strong> {@link StaffAssignmentContext} 做规则判断；
 * {@link ShiftPlanFinalStaffAssigner} 则为每位员工单独构造上下文（含个人当日已排工时与休息间隔），生产路径以 Assigner 为准。
 * <p>
 * <b>处理流程（{@link #pickForPostDemand}）</b>
 * <ol>
 *   <li>{@link #buildCandidatePool}：工作站专岗/通岗求交 + 随机打乱；</li>
 *   <li>{@link FixedSoftConstraintRelaxationPlanner#pickWithRelaxation}：第九步硬约束 + 第十步固定规则（含尽量放松）；</li>
 *   <li>按 {@link StaffAssignmentDayLedger} 过滤并标记：同日不可既固定又划线。</li>
 * </ol>
 * <p>
 * <b>划线排班</b>：{@code lineAssignment=true} 时不传 {@link FixedSchedulingRuleVo}，不参与固定规则放松。
 */
public final class ScheduleStaffAssignmentSteps910 {

    private ScheduleStaffAssignmentSteps910() {}

    /**
     * 为某岗位、某需求人数选出员工。
     *
     * @param targetCount            目标人数（岗位需求 headcount）
     * @param rawCandidatesForPost   上游给出的该岗位候选员工 ID（如专岗+通岗有序列表）
     * @param workstationBuckets     工作站岗位绑定；用于与候选求交
     * @param port                   第九～十步规则判定端口
     * @param context                本段班次的分配上下文（本方法中全员共用同一 context）
     * @param fixedSchedulingRules   固定排班规则；划线班时由调用方在内部置 null
     * @param ledger                 同日固定/划线互斥账本
     * @param lineAssignment         true=划线班（跳过固定规则）
     * @param fixedShiftGroupKey     固定班逻辑键（{@link ShiftPlanFinalStaffAssigner#fixedShiftGroupKey} 同源）；
     *                               划线时可 null
     * @param random                 候选池打乱用；null 则新建 Random
     * @return 最多 {@code targetCount} 个员工 ID，顺序为规则过滤后的遍历顺序（非专岗优先序）
     */
    public static List<String> pickForPostDemand(
            int targetCount,
            List<String> rawCandidatesForPost,
            WorkstationPostStaffBuckets workstationBuckets,
            StaffRuleEvaluationPort port,
            StaffAssignmentContext context,
            FixedSchedulingRuleVo fixedSchedulingRules,
            StaffAssignmentDayLedger ledger,
            boolean lineAssignment,
            String fixedShiftGroupKey,
            Random random) {
        if (targetCount <= 0) {
            return Collections.emptyList();
        }
        Objects.requireNonNull(port, "port");
        Objects.requireNonNull(context, "context");
        Objects.requireNonNull(ledger, "ledger");
        Random rnd = random == null ? new Random() : random;

        // 步骤 1：构建本岗位候选池（工作站求交 + shuffle）
        List<String> pool =
                buildCandidatePool(
                        context.getPostId(), rawCandidatesForPost, workstationBuckets, rnd);

        // 步骤 2：第十步 — 固定规则 + 尽量放松（划线班 fixedVo=null）
        FixedSchedulingRuleVo fixedVo = lineAssignment ? null : fixedSchedulingRules;
        List<String> relaxed =
                FixedSoftConstraintRelaxationPlanner.pickWithRelaxation(
                        targetCount, pool, fixedVo, port, context, rnd);

        // 步骤 3：账本互斥 — 在放松结果上按序取人并 mark，满 targetCount 即停
        List<String> out = new ArrayList<>();
        for (String id : relaxed) {
            if (id == null || id.trim().isEmpty()) {
                continue;
            }
            if (lineAssignment) {
                if (!ledger.canAssignLine(id, context.getScheduleDay())) {
                    continue;
                }
            } else {
                if (!ledger.canAssignFixed(id, context.getScheduleDay(), fixedShiftGroupKey)) {
                    continue;
                }
            }
            out.add(id);
            if (lineAssignment) {
                ledger.markLineShift(id, context.getScheduleDay());
            } else {
                ledger.markFixed(id, context.getScheduleDay(), fixedShiftGroupKey);
            }
            if (out.size() >= targetCount) {
                break;
            }
        }
        return Collections.unmodifiableList(out);
    }

    /**
     * 构建本岗位候选员工池。
     * <p>
     * <b>无工作站绑定</b>：清洗 {@code rawCandidatesForPost} 后 shuffle 返回。<br>
     * <b>有绑定</b>：与工作站专岗+通岗 ID 求交；交集为空则返回空列表（不再退化为工作站全集），再 shuffle。
     * <p>
     * 与 {@link ShiftPlanFinalStaffAssigner#intersectWorkstationPool} 逻辑类似，但本方法始终打乱顺序以均衡人选。
     */
    private static List<String> buildCandidatePool(
            String postId,
            List<String> rawCandidatesForPost,
            WorkstationPostStaffBuckets workstationBuckets,
            Random rnd) {
        List<String> raw = rawCandidatesForPost == null ? Collections.emptyList() : rawCandidatesForPost;
        if (workstationBuckets == null || postId == null || !workstationBuckets.hasWorkstationBinding(postId)) {
            List<String> copy = new ArrayList<>();
            for (String s : raw) {
                if (s != null && !s.trim().isEmpty()) {
                    copy.add(s.trim());
                }
            }
            Collections.shuffle(copy, rnd);
            return copy;
        }
        Set<String> allowed = new LinkedHashSet<>();
        allowed.addAll(workstationBuckets.getSpecialistIds(postId));
        allowed.addAll(workstationBuckets.getGeneralIds(postId));
        List<String> out = new ArrayList<>();
        for (String s : raw) {
            if (s == null || s.trim().isEmpty()) {
                continue;
            }
            String t = s.trim();
            if (allowed.contains(t)) {
                out.add(t);
            }
        }
        Collections.shuffle(out, rnd);
        return out;
    }
}
