# -*- coding: utf-8 -*-
"""サービスロボット導入シミュレーション 病院の搬送ロボット（検体・薬剤、同じ階）試算

版の記録
  h0（2026-11-09）：最初の版
  h2（2026-11-09）：青の道の決め方（route_plan：廊下だけ／裏口だけ／時間で切替）を足した。時間で切替は
     back_door_hours（外来の混む 8〜11時）の間だけロボットが裏口から積む（Unity 依頼134 で、混む時間は裏口で
     問題がほぼ半分、空いている時間は今の道の方が少なかったため）。人が歩くときはいつも表の短い道。
     ロボットの便の時刻表（trip_log）を残し、Unity に渡せるようにした
  h1（2026-11-09）：青の依頼の数を1時間8件 → 1.5件（慈恵柏の利用回数 1日4〜9回に合わせた。案1）。
     依頼1件は、ロボットに1回で預けるひとまとまり（採血管の袋など）と定義した。
     道の長さを何倍にするか（layout_scale）と、人が運ぶときの歩く以外の手間（staff_extra_s）を
     動かせるようにした（どちらも標準では効かない値：1.0倍・0秒）

前提の値はこのファイルの隣の assumptions.json（前提の表）から、フロアの形は
../hospital_layouts/hospital_h0.json から読む。コードの中に前提の値を書かない。

何を回すか
  1日（7:30〜16:30）、3ルート（緑・青・赤）に依頼が出る。依頼は各ルートの運行の時間帯の間だけ、
  1時間あたりの数（requests_per_h）でばらばらに出る（ポアソン到着）。至急の割合は urgent_share。
  ロボット0台：看護補助者が歩いて運ぶ（比べるもと）。まとめるために staff_hold_min 分まで待ち、至急ならすぐ出る。
  ロボット1台・2台：出発の決まり（rules）どおりに運ぶ。
    - いま運行の時間帯にあるルートの出発地で待つ（時間帯が切り替わったら、次のルートの出発地へ移る）
    - 満杯（収納の数）か、いちばん古い依頼が max_hold_min 分待ったら出発。至急はすぐ。時間帯が終わって残った依頼もすぐ
    - 積む（load_s）→ 走る（距離/速さ＋一時停止・立ち往生）→ 受け取る人を待つ（pickup_wait_s の平均の指数分布）
      → 取り出す（unload_s）→ 戻る
    - 至急の依頼が出たとき、その出発地に空いているロボットがいなければ、看護補助者が歩いて運ぶ
  依頼の出方は、乱数の種ごとに台数によらず同じ（台数の違いだけを比べるため）。

出すもの（ルートごと・1日）
  届くまでの時間（依頼が出てから受け取る人が取り出すまで）の平均・90%・最大、至急の平均、
  運んだ回数（ロボットの便・人が歩いた回数）、看護補助者などが搬送に使った時間
  （人が歩く往復と渡す時間・ロボットに積む時間・取り出す時間・立ち往生を解く時間）。
  ロボットが走った時間と距離、山場（8〜11時の青）の届くまでの時間。
  判定：届くまでの平均が judge_mean_min 分以上、または至急の平均が judge_urgent_min 分以上で「間に合わない」。

使い方
  python3 hospital_sim.py                 # 標準の条件（0/1/2台）と、仮置きの値を動かした条件を回す
  python3 hospital_sim.py --out 出力先     # results.json と summary.txt を書く
"""
import argparse
import json
import math
import random
import statistics
from pathlib import Path

import simpy

HERE = Path(__file__).resolve().parent


def hm(s):
    h, m = s.split(":")
    return int(h) * 3600 + int(m) * 60


def load_inputs(layout=None):
    A = json.loads((HERE / "assumptions.json").read_text(encoding="utf-8"))
    L = json.loads((HERE / (layout or A["layout"])).read_text(encoding="utf-8"))
    return A, L


def params(A, over=None):
    c = {k: v["value"] for k, v in A["common"].items()}
    r = {k: v["value"] for k, v in A["rules"].items()}
    p = {"c": c, "r": r, "req_mult": 1.0}
    if over:
        for k, v in over.items():
            if k == "req_mult":
                p["req_mult"] = v
            elif k.startswith("rph_"):
                p.setdefault("rph", {})[k[4:]] = v
            elif k in c:
                c[k] = v
            else:
                r[k] = v
    return p


def make_requests(A, p, seed):
    """依頼の出方。台数によらず同じになるよう、専用の乱数で作る"""
    rng = random.Random(f"req-{seed}")
    day0 = hm(A["rules"]["day"]["value"][0])
    out = []
    for key in sorted(A["routes"]):
        R = A["routes"][key]
        lam = p.get("rph", {}).get(key, R["requests_per_h"]["value"]) * p["req_mult"] / 3600.0
        for a, b in R["windows"]:
            t, end = hm(a) - day0, hm(b) - day0
            while True:
                t += rng.expovariate(lam)
                if t >= end:
                    break
                out.append({"route": key, "t": t, "urgent": rng.random() < p["c"]["urgent_share"]})
    out.sort(key=lambda q: q["t"])
    for i, q in enumerate(out):
        q["id"] = i
    return out


class Day:
    def __init__(self, A, L, p, n_robots, seed):
        self.A, self.L, self.p, self.n = A, L, p, n_robots
        self.c, self.r = p["c"], p["r"]
        self.env = simpy.Environment()
        self.rng = random.Random(f"run-{seed}-{n_robots}")
        self.day0 = hm(A["rules"]["day"]["value"][0])
        self.day_end = hm(A["rules"]["day"]["value"][1]) - self.day0
        # 積む所・届け先の点はフロアの形のルートに合わせる（h0b は青が裏口から出る）
        self.routes = {k: dict(R, from_point=L["routes"][k]["from"], to_point=L["routes"][k]["to"]) for k, R in A["routes"].items()}
        self.win = {k: [(hm(a) - self.day0, hm(b) - self.day0) for a, b in R["windows"]] for k, R in self.routes.items()}
        self.queue = {k: [] for k in self.routes}
        self.done = []
        self.staff_s = {k: 0.0 for k in self.routes}  # 人が搬送に使った秒
        self.staff_walks = {k: 0 for k in self.routes}
        self.robot_trips = {k: 0 for k in self.routes}
        self.robot_run_s = 0.0
        self.robot_m = 0.0
        self.stuck_n = 0
        self.idle_at = {}  # ロボット番号 → 待っている出発地のルート（走っているときは None）
        self.reqs = make_requests(A, p, seed)
        # 青の道の決め方（h2）：廊下だけ／裏口だけ／時間で切替（back_door_hours の間だけ裏口）
        self.plan = self.r.get("route_plan", "廊下だけ")
        if self.plan != "廊下だけ" and "内科外来の処置室（裏口）" not in str(self.L["distances"].keys()):
            self.L = json.loads((HERE / "../hospital_layouts/hospital_h0b.json").read_text(encoding="utf-8"))
        self.back_hours = [(hm(a) - self.day0, hm(b) - self.day0) for a, b in self.r.get("back_door_hours", [])]
        self.trip_log = []  # ロボットの便（Unity に渡す時刻表）

    def robot_from(self, k, t):
        """ロボットが積む所。青だけ、決め方により裏口になる。人が歩くときはいつも表（routes の from_point）"""
        if k == "blue" and (self.plan == "裏口だけ" or (self.plan == "時間で切替" and any(a <= t < b for a, b in self.back_hours))):
            return "内科外来の処置室（裏口）"
        return self.routes[k]["from_point"]

    # 距離
    def dist(self, a, b):
        if a == b:
            return 0.0
        return self.L["distances"][f"{a}|{b}"] * self.c["layout_scale"]

    def route_dist(self, k):
        R = self.routes[k]
        return self.dist(R["from_point"], R["to_point"])

    def active(self, t):
        return [k for k, ws in self.win.items() if any(a <= t < b for a, b in ws)]

    def next_route(self, t):
        best = None
        for k, ws in self.win.items():
            for a, b in ws:
                if a > t and (best is None or a < best[0]):
                    best = (a, k)
        return best[1] if best else None

    # 依頼が出る
    def arrivals(self):
        for q in self.reqs:
            yield self.env.timeout(max(0.0, q["t"] - self.env.now))
            k = q["route"]
            if self.n > 0 and q["urgent"] and k not in self.idle_at.values():
                self.env.process(self.staff_walk([q], k))  # 決まり：空いているロボットがいなければ人が運ぶ
            else:
                self.queue[k].append(q)

    # 人が歩いて運ぶ
    def staff_walk(self, items, k):
        d = self.route_dist(k)
        v = self.c["staff_speed"]
        extra = self.c["staff_extra_s"]
        yield self.env.timeout(d / v + extra + self.c["staff_handoff_s"])
        for q in items:
            self.finish(q, "staff")
        self.staff_s[k] += 2 * d / v + extra + self.c["staff_handoff_s"]
        self.staff_walks[k] += 1

    def staff_route(self, k):
        """0台のとき：まとめるために staff_hold_min 分まで待ち、至急ならすぐ。時間帯が終わって残ればすぐ"""
        hold = self.c["staff_hold_min"] * 60
        while True:
            yield self.env.timeout(5)
            Q = self.queue[k]
            if not Q:
                if self.env.now > self.day_end + 3600:
                    return
                continue
            now = self.env.now
            if any(q["urgent"] for q in Q) or now - Q[0]["t"] >= hold or k not in self.active(now):
                items, self.queue[k] = Q[:], []
                self.env.process(self.staff_walk(items, k))

    # ロボット
    def leg(self, d):
        """片道。距離・速さ・一時停止・立ち往生"""
        s = d / self.c["robot_speed"]
        if d > 0 and self.rng.random() < self.c["pause_rate"]:
            s += self.rng.uniform(*self.c["pause_s"])
        stuck = 0.0
        if d > 0 and self.rng.random() < self.c["stuck_rate"]:
            stuck = self.c["stuck_resolve_s"]
            self.stuck_n += 1
        self.robot_m += d
        self.robot_run_s += s + stuck
        return s + stuck, stuck

    def robot(self, i):
        cap = self.c["compartments"]
        hold = self.r["max_hold_min"] * 60
        loc = None  # 最初は1つ目のルートの出発地にいる
        first = self.next_route(-1) or sorted(self.routes)[0]
        loc = self.routes[first]["from_point"]
        while True:
            now = self.env.now
            act = self.active(now)
            waiting = [k for k in self.routes if self.queue[k]]
            if not act and not waiting and self.next_route(now) is None:
                self.idle_at[i] = None
                return
            # 行き先のルート：残った依頼（時間帯の外）→ いま運行中で古い依頼があるもの → いま運行中 → 次の時間帯
            leftover = [k for k in waiting if k not in act]
            if leftover:
                k = min(leftover, key=lambda x: self.queue[x][0]["t"])
            elif act:
                k = min(act, key=lambda x: self.queue[x][0]["t"] if self.queue[x] else math.inf)
            else:
                k = self.next_route(now)
            home = self.robot_from(k, now)
            if loc != home:
                self.idle_at[i] = None
                s, _ = self.leg(self.dist(loc, home))
                yield self.env.timeout(s)
                loc = home
                continue
            Q = self.queue[k]
            go = bool(Q) and (len(Q) >= cap or any(q["urgent"] for q in Q) or now - Q[0]["t"] >= hold or k not in act)
            if not go:
                self.idle_at[i] = k
                yield self.env.timeout(5)
                continue
            self.idle_at[i] = None
            Q.sort(key=lambda q: (not q["urgent"], q["t"]))
            items, self.queue[k] = Q[:cap], Q[cap:]
            yield self.env.timeout(self.c["load_s"])
            dest = self.routes[k]["to_point"]
            self.trip_log.append({"route": k, "depart_s": round(self.env.now, 1), "from": home, "to": dest, "items": len(items),
                                  "robot": i})
            s, stuck = self.leg(self.dist(home, dest))
            yield self.env.timeout(s)
            pw = self.rng.expovariate(1 / self.c["pickup_wait_s"]) if self.c["pickup_wait_s"] > 0 else 0.0
            yield self.env.timeout(pw + self.c["unload_s"])
            for q in items:
                self.finish(q, "robot")
            self.robot_trips[k] += 1
            self.staff_s[k] += self.c["load_s"] + self.c["unload_s"] + stuck
            loc = dest

    def finish(self, q, by):
        q2 = dict(q)
        q2["done"] = self.env.now
        q2["by"] = by
        self.done.append(q2)

    def run(self):
        self.env.process(self.arrivals())
        if self.n == 0:
            for k in self.routes:
                self.env.process(self.staff_route(k))
        else:
            for i in range(self.n):
                self.env.process(self.robot(i))
        self.env.run(until=self.day_end + 3 * 3600)
        return self.summary()

    def summary(self):
        pk0, pk1 = (hm(x) - self.day0 for x in self.r["peak"])
        out = {"routes": {}, "robot_run_min": self.robot_run_s / 60, "robot_km": self.robot_m / 1000,
               "stuck": self.stuck_n, "not_done": len(self.reqs) - len(self.done)}
        for k in self.routes:
            D = [(q["done"] - q["t"]) / 60 for q in self.done if q["route"] == k]
            U = [(q["done"] - q["t"]) / 60 for q in self.done if q["route"] == k and q["urgent"]]
            P = [(q["done"] - q["t"]) / 60 for q in self.done if q["route"] == k and pk0 <= q["t"] < pk1]
            out["routes"][k] = {
                "n": len(D),
                "mean_min": statistics.mean(D) if D else None,
                "p90_min": sorted(D)[int(0.9 * (len(D) - 1))] if D else None,
                "max_min": max(D) if D else None,
                "urgent_n": len(U),
                "urgent_mean_min": statistics.mean(U) if U else None,
                "peak_mean_min": statistics.mean(P) if P else None,
                "robot_trips": self.robot_trips[k],
                "staff_walks": self.staff_walks[k],
                "staff_min": self.staff_s[k] / 60,
            }
        return out


def run_condition(A, L, n, over=None, seeds=None):
    p = params(A, over)
    seeds = seeds or p["r"]["seeds"]
    runs = [Day(A, L, p, n, s).run() for s in range(seeds)]

    def avg(get):
        v = [get(x) for x in runs]
        v = [x for x in v if x is not None]
        return statistics.mean(v) if v else None

    res = {"robots": n, "over": over or {}, "seeds": seeds, "routes": {},
           "robot_run_min": avg(lambda x: x["robot_run_min"]), "robot_km": avg(lambda x: x["robot_km"]),
           "stuck": avg(lambda x: x["stuck"]), "not_done": avg(lambda x: x["not_done"])}
    for k in A["routes"]:
        res["routes"][k] = {f: avg(lambda x, f=f: x["routes"][k][f]) for f in runs[0]["routes"][k]}
    res["staff_min"] = sum(res["routes"][k]["staff_min"] for k in A["routes"])
    jm, ju = p["r"]["judge_mean_min"], p["r"]["judge_urgent_min"]
    res["late"] = {k: bool((r["mean_min"] or 0) >= jm or (r["urgent_mean_min"] or 0) >= ju) for k, r in res["routes"].items()}
    return res


SWEEPS = [  # 仮置きの値を動かす（ロボット1台・2台で）
    ("pickup_wait_s", [0, 60, 180, 300]),
    ("req_mult", [0.5, 1.0, 1.5, 2.0]),
    ("robot_speed", [0.4, 0.5, 0.8]),
    ("max_hold_min", [5, 10, 20]),
    ("stuck_rate", [0.0, 0.01, 0.05]),
    ("layout_scale", [1.0, 1.5, 2.0, 2.5]),     # 案2 の当たり：道を何倍にすると発表に届くか
    ("staff_extra_s", [0, 60, 120, 180, 240]),  # 案3 の当たり：人が運ぶときの歩く以外の手間
    ("route_plan", ["廊下だけ", "時間で切替", "裏口だけ"]),  # h2：青の道の決め方
]
ZERO_TOO = ("req_mult", "layout_scale", "staff_extra_s")  # 0台（比べるもと）も同じ値で回し直す


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", default=None)
    ap.add_argument("--layout", default=None, help="フロアの形を差し替える（例 ../hospital_layouts/hospital_h0b.json）")
    args = ap.parse_args()
    A, L = load_inputs(args.layout)
    if args.layout:
        A["model"] = A["model"] + "_" + L["meta"].get("variant", "x")
    base = [run_condition(A, L, n) for n in A["rules"]["robots"]["value"]]
    zero = base[0]
    sweeps = []
    for key, vals in SWEEPS:
        for v in vals:
            o = {key: v}
            z = run_condition(A, L, 0, o) if key in ZERO_TOO else zero
            for n in (1, 2):
                r = run_condition(A, L, n, o)
                r["saved_staff_min"] = z["staff_min"] - r["staff_min"]
                r["saved_by_route"] = {k: z["routes"][k]["staff_min"] - r["routes"][k]["staff_min"] for k in A["routes"]}
                sweeps.append(r)
    for r in base:
        r["saved_staff_min"] = zero["staff_min"] - r["staff_min"]
        r["saved_by_route"] = {k: zero["routes"][k]["staff_min"] - r["routes"][k]["staff_min"] for k in A["routes"]}
    result = {"model": A["model"], "layout": L["meta"]["name"], "base": base, "sweeps": sweeps}
    text = report(A, result)
    print(text)
    if args.out:
        out = Path(args.out)
        out.mkdir(parents=True, exist_ok=True)
        (out / f"results_{A['model']}.json").write_text(json.dumps(result, ensure_ascii=False, indent=1), encoding="utf-8")
        (out / f"summary_{A['model']}.txt").write_text(text, encoding="utf-8")


def f(x, d=1):
    return "-" if x is None else f"{x:.{d}f}"


def report(A, R):
    names = {k: v["name"] for k, v in A["routes"].items()}
    L = [f"試算 {R['model']}（{R['layout']}）乱数の種 {R['base'][0]['seeds']} 個の平均", ""]
    L.append("■ 標準の条件")
    for r in R["base"]:
        L.append(f"ロボット{r['robots']}台  人が搬送に使った時間 {f(r['staff_min'])}分/日（0台より {f(r['saved_staff_min'])}分減）"
                 + (f"  ロボットが走った {f(r['robot_run_min'],0)}分・{f(r['robot_km'],2)}km" if r["robots"] else ""))
        for k in ["green", "blue", "red"]:
            x = r["routes"][k]
            L.append(f"  {names[k]} 依頼{f(x['n'],0)}件 届くまで 平均{f(x['mean_min'])}分 90%{f(x['p90_min'])}分 最大{f(x['max_min'])}分"
                     f" 至急{f(x['urgent_mean_min'])}分 山場{f(x['peak_mean_min'])}分"
                     f" 便{f(x['robot_trips'])} 人が歩いた{f(x['staff_walks'])}回 人の時間{f(x['staff_min'])}分"
                     + ("  ← 間に合わない" if r["late"][k] else ""))
    L.append("")
    L.append("■ 仮置きの値を動かす（青のルートの届くまでの平均・至急・人の時間の減り）")
    for r in R["sweeps"]:
        (k, v), = r["over"].items()
        b = r["routes"]["blue"]
        sb = r.get("saved_by_route", {})
        L.append(f"{k}={v} ロボット{r['robots']}台  減り 青{f(sb.get('blue'))}分 赤{f(sb.get('red'))}分（1便あたり 青{f(sb.get('blue',0)/max(r['routes']['blue']['robot_trips'] or 1,1e-9))}分 赤{f(sb.get('red',0)/max(r['routes']['red']['robot_trips'] or 1,1e-9))}分）  青 平均{f(b['mean_min'])}分 至急{f(b['urgent_mean_min'])}分 山場{f(b['peak_mean_min'])}分"
                 f"  人の時間 {f(r['saved_staff_min'])}分減" + ("  ← 間に合わない：" + "・".join(names[x] for x, y in r["late"].items() if y) if any(r["late"].values()) else ""))
    return "\n".join(L)


if __name__ == "__main__":
    main()
