"""
=============================================================================
CS486 · 第 10 讲 · 代码 03：运行时重规划——变故之后，从头再来还是尽量少改？什么时候发现很重要吗？
=============================================================================

【这个脚本要回答的问题】（讲义 §7）
    计划很少按计划执行。MACI（Chang 2025）的感恩节晚餐案例里，航班延误 3 小时：没有运行时监控的 LLM 要么把晚餐推迟（违反
    "18:00 开饭"），要么让烤箱无人看管；有"预警"的版本一早就知道延误，重新安排了接机。ALAS（Geng & Chang 2025）主张
    **局部修补**：只改受影响的一小块（"有界的编辑半径"），而不是全局重算。经典规划里，Fox et al.（2006）把它叫作
    **计划稳定性**：修补后的计划和原计划越像越好——每改一处，就要通知一个人"你的安排变了"。

    我们模拟 200 个"有解的一天"（12 个任务、4 个人、10 条先后约束，时间范围 11 个时段），每天发生 2 个随机变故
    （某个任务超时 1–3 个时段，或者某个任务最早开始时间推迟 1–3 个时段）。比较：
      - 不监控：照原计划执行；
      - 从头重规划：冻结已经开始的任务，其余重新求解；
      - 偏好原计划的重规划：完整求解，但每个任务先试原来的安排；
      - 局部修补：只放开出问题的任务，修不好再逐步扩大（ALAS 式）。
    以及监控器**提前多久**发现变故（0 = 到"咬人"的时候才发现；"一早" = 一天开始时就知道）。

    演示 1：不监控会怎样？一个变故会引起几处冲突？
    演示 2：四种做法 × 发现的早晚：成功率、改动的任务数、求解器的工作量。
    演示 3：一个例子——同一个变故，从头重规划和局部修补各改了什么？

【运行】
    python3 03_runtime_replanning.py           # 约 1 秒（只用标准库）
    python3 03_runtime_replanning.py --quick   # 冒烟测试

【依赖】本目录的 schedule_lab.py（这个脚本只用到它的标准库部分）。
"""

import argparse
import os
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

import schedule_lab as sch                                               # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

LEADS = (0, 2, 4, None)
HOWS = (("scratch", "从头重规划"), ("prefer", "偏好原计划"), ("local", "局部修补"))


def lead_name(lead):
    return "一早就知道" if lead is None else ("到时才发现" if lead == 0 else f"提前 {lead} 个时段")


def gantt(problem, plan, people=4, mark=()):
    """
    把计划画成每人一行的时间条：每个时段写正在做的任务名，空闲写 "·"；mark 里的任务用小写字母（"改动过"）。
    【返回】字符串列表
    """
    T = {t.name: t for t in problem.tasks}
    rows = []
    for w in range(people):
        line = ["·"] * problem.horizon
        for name, (s, who) in plan.items():
            if who == w:
                for k in range(s, min(problem.horizon, s + T[name].dur)):
                    line[k] = name.lower() if name in mark else name
        rows.append(f"    {w} 号  " + " ".join(line))
    return rows


def main():
    ap = argparse.ArgumentParser(description="运行时重规划")
    ap.add_argument("--quick", action="store_true", help="冒烟测试")
    args = ap.parse_args()
    t0 = time.perf_counter()
    count = 20 if args.quick else 200
    days = sch.random_days(seed=11, count=count)

    # ------------------------------------------------------------------
    print_header(f"演示 1：不监控、照原计划执行（{count} 天，每天 2 个变故）")
    keep = [sch.run_day(p, plan, ds, "keep", None) for p, plan, ds in days]
    ok_keep = sum(k[0] for k in keep)
    viol = [k[3] for k in keep]
    rows = [[v, sum(x == v for x in viol)] for v in sorted(set(viol))]
    print(f"  这一天结束时所有约束都满足：{ok_keep} / {count} 天（{pct(ok_keep / count)}）")
    print_table(["结束时违反的约束数", "天数"], rows, [20, 8])

    # ------------------------------------------------------------------
    print_header("演示 2：发现变故之后重规划——四种发现时机 × 三种做法")
    rows, d2 = [], {}
    for lead in LEADS:
        for how, name in HOWS:
            res = [sch.run_day(p, plan, ds, how, lead) for p, plan, ds in days]
            good = [r for r in res if r[0]]
            d2[(lead, how)] = (len(good) / count, sum(r[1] for r in good) / max(1, len(good)),
                               sum(r[2] for r in res) / count)
            rows.append([lead_name(lead), name, pct(d2[(lead, how)][0]), f"{d2[(lead, how)][1]:.2f}", f"{d2[(lead, how)][2]:.0f}"])
    print_table(["监控器发现变故", "做法", "成功", "成功的天里平均改动了几个任务", "求解器平均尝试次数"],
                rows, [16, 14, 8, 30, 20])
    print("  （'一早就知道' = 一天开始时就知道当天会发生的变故，所有任务都还能挪；这时失败的天，是变故让问题本身无解了）")

    # ------------------------------------------------------------------
    print_header("演示 3：一个例子——同一个变故，从头重规划 vs 局部修补（小写 = 改动过的任务）")
    shown = False
    for p, plan, ds in days:
        kind, target, amount = ds[0]
        T = {t.name: t for t in p.tasks}
        if kind == "overrun":
            p2 = sch.apply_disruption(p, kind, target, amount)
            bite = plan[target][0] + T[target].dur
        else:
            p2 = sch.apply_disruption(p, kind, target, plan[target][0] + amount)
            bite = plan[target][0]
        if not sch.violations(p2, plan):
            continue
        a, _ = sch.replan(p2, plan, 0, "scratch")
        b, _ = sch.replan(p2, plan, 0, "local")
        if a is None or b is None or sch.n_changes(plan, a) - sch.n_changes(plan, b) < 3:
            continue
        what = (f"任务 {target} 超时 {amount} 个时段" if kind == "overrun"
                else f"任务 {target} 最早只能在第 {plan[target][0] + amount} 个时段开始（原计划第 {plan[target][0]} 个）")
        print(f"  变故：{what}；原计划下会造成 {len(sch.violations(p2, plan))} 处冲突。一早就知道。")
        print("  原计划：")
        print("\n".join(gantt(p, plan)))
        ca = [n for n in plan if plan[n] != a[n]]
        cb = [n for n in plan if plan[n] != b[n]]
        print(f"  从头重规划（改动 {len(ca)} 个任务：{', '.join(ca)}）：")
        print("\n".join(gantt(p2, a, mark=ca)))
        print(f"  局部修补（改动 {len(cb)} 个任务：{', '.join(cb)}）：")
        print("\n".join(gantt(p2, b, mark=cb)))
        shown = True
        break
    if not shown:
        print("  （这批题里没有找到合适的例子）")

    if args.quick:
        print("\n（--quick 冒烟测试：数字没有意义，不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return
    s = lambda lead, how: d2[(lead, how)]
    print_observations([
        f"不监控，几乎每天都会出问题（演示 1）：两个变故之后，只有 {pct(ok_keep / count)} 的天里原计划还能用；"
        f"平均每天结束时违反 {sum(viol) / count:.2f} 条约束——一个任务超时，会挤占同一个人的下一个任务、推迟依赖它的任务，"
        "冲突会传播。MACI 里没有运行时监控的 LLM 把晚餐推迟、让烤箱无人看管，是同一类问题。",
        f"早发现的好处在这里不大（演示 2）：从头重规划的成功率，到时才发现 {pct(s(0, 'scratch')[0])}，提前 2 / 4 个时段 "
        f"{pct(s(2, 'scratch')[0])} / {pct(s(4, 'scratch')[0])}，一早就知道 {pct(s(None, 'scratch')[0])}——只有'一早就知道'稍高一点"
        "（可以把本来排在前面的任务和受影响的任务对调）。剩下 40% 多的天，变故让问题本身无解了（时间范围太紧），"
        "早发现也救不回来；这时该做的是告诉用户哪条约束必须放松（代码 01、02 的冲突解释）。"
        "MACI 的案例里，早预警让一个 LLM 提前改派人直接去机场；早发现值多少，取决于问题里有多少可以提前挪动的余地——"
        "我们的随机题余地不多。",
        f"局部修补改动得少（演示 2）：一早就知道时，从头重规划平均改动 {s(None, 'scratch')[1]:.2f} 个任务（共 12 个），"
        f"偏好原计划 {s(None, 'prefer')[1]:.2f} 个，局部修补 {s(None, 'local')[1]:.2f} 个；成功率几乎相同"
        f"（{pct(s(None, 'scratch')[0])} / {pct(s(None, 'prefer')[0])} / {pct(s(None, 'local')[0])}）。"
        "每一处改动都要通知一个人，还可能引起新的连锁反应；Fox et al.（2006）的'计划稳定性'、ALAS 的'有界编辑半径'说的就是这件事。",
        f"一个反直觉的细节（演示 2）：发现得越早，从头重规划改动得越多（到时才发现 {s(0, 'scratch')[1]:.2f} → 一早就知道 "
        f"{s(None, 'scratch')[1]:.2f}），局部修补几乎不变（{s(0, 'local')[1]:.2f} → {s(None, 'local')[1]:.2f}）。"
        "越早，还没开始、可以挪动的任务越多；从头重规划会把它们全部重新排一遍，哪怕它们和变故毫无关系。",
        "这些做法都需要一个**完备的、能接受'固定一部分'的求解器**，以及一个**独立于规划者的监控器**——"
        "MACI 把规划和验证分开、用运行时监控触发重规划，ALAS 用版本化的执行日志知道'哪些已经发生、不能再改'，都是为了这两点。"
        "让同一个 LLM 一边执行、一边自己发现问题、再从头重写计划，三个环节都可能出错（第 7 讲）。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
