p = 'backend/app/services/diagram.py'
s = open(p).read()

# An arrow that ends on the edge of a box, whatever direction it comes from.
helper = '''
def _edge_point(box: "Box", dx: float, dy: float, clear: float = 7.0) -> tuple[float, float]:
    """Where a ray leaving a box's centre crosses its side.

    Half the box width is the right answer only for a horizontal ray. Using it
    for a diagonal one — which is every arrow on a cycle — starts the line far
    outside the box on one axis and inside it on the other, and four arrows
    came out as stubs floating in the middle of the figure.
    """
    span = math.hypot(dx, dy) or 1.0
    ux, uy = dx / span, dy / span
    reach = min(
        (box.width / 2 + clear) / abs(ux) if abs(ux) > 1e-6 else float("inf"),
        (box.height / 2 + clear) / abs(uy) if abs(uy) > 1e-6 else float("inf"),
    )
    return box.cx + ux * reach, box.cy + uy * reach


def _elbow(x1: float, y1: float, x2: float, y2: float, drop: float) -> str:
    """Down, across, and down again — for the step back to the start of a row.

    A straight line from the end of one row to the start of the next crosses
    every box between them. The elbow goes down into the gap, back along it,
    and down into the target.
    """
    mid = y1 + drop
    return (f'<path d="M{x1:.1f} {y1:.1f} V{mid:.1f} H{x2:.1f} V{y2:.1f}" fill="none" '
            f'stroke="{ARROW}" stroke-width="1.6" stroke-linejoin="round" '
            f'marker-end="url(#tip)"/>')

'''
s = s.replace('\ndef _edges_of(spec: dict)', helper + '\ndef _edges_of(spec: dict)')

# ── flow: a gap wide enough for the label that goes in it, and an elbow at the
#    end of a row.
old_flow = s[s.index('def _flow('):s.index('def _branch(')]
new_flow = '''def _flow(boxes: list[Box], edges: list[dict], ids: dict) -> tuple[float, float, str]:
    """Left to right, wrapping to a new row after ROW_MAX."""
    rows = [boxes[i:i + ROW_MAX] for i in range(0, len(boxes), ROW_MAX)]
    row_of = {id(box): number for number, row in enumerate(rows) for box in row}
    labels = {(e["from"], e["to"]): e.get("label") for e in edges}

    def label_between(a: Box, b: Box) -> str | None:
        return labels.get((ids[id(a)], ids[id(b)]))

    y = 0.0
    width = 0.0
    for row in rows:
        height = max(box.height for box in row)
        x = 0.0
        for index, box in enumerate(row):
            if index:
                # The gap carries the arrow and, when there is one, its label.
                # A fixed gap put "within 20 s" through the side of the figure.
                text = label_between(row[index - 1], box)
                x += max(GAP_X, (_text_width(text, EDGE) + 22) if text else 0)
            box.x, box.y = x, y + (height - box.height) / 2
            x += box.width
        width = max(width, x)
        y += height + GAP_Y
    parts = []
    for index in range(len(boxes) - 1):
        a, b = boxes[index], boxes[index + 1]
        label = label_between(a, b)
        if row_of[id(a)] == row_of[id(b)]:
            parts.append(_arrow(a.x + a.width + 4, a.cy, b.x - 6, b.cy, label))
        else:
            parts.append(_elbow(a.cx, a.y + a.height + 3, b.cx, b.y - 7, GAP_Y / 2))
    return width, y - GAP_Y, "".join(parts)


'''
s = s.replace(old_flow, new_flow)

# ── cycle: arrows that land on the boxes.
old_cycle = s[s.index('def _cycle('):s.index('def _compare(')]
new_cycle = '''def _cycle(boxes: list[Box], edges: list[dict], ids: dict) -> tuple[float, float, str]:
    """On a circle, each pointing at the next, the last back to the first."""
    count = len(boxes)
    tallest = max(box.height for box in boxes)
    # Big enough that neighbours do not touch: the chord between two adjacent
    # centres has to clear a box and a gap.
    radius = max(115.0, (BOX_W + 30) / (2 * math.sin(math.pi / count)))
    cx = cy = radius + BOX_W / 2
    for index, box in enumerate(boxes):
        angle = -math.pi / 2 + index * 2 * math.pi / count
        box.x = cx + radius * math.cos(angle) - box.width / 2
        box.y = cy + radius * math.sin(angle) - box.height / 2
    # Placed on the circle first, then pulled back to the origin, then the
    # arrows drawn — an arrow computed before the shift points at where the
    # box used to be.
    left = min(box.x for box in boxes)
    top = min(box.y for box in boxes)
    for box in boxes:
        box.x -= left
        box.y -= top
    parts = []
    for index, box in enumerate(boxes):
        nxt = boxes[(index + 1) % count]
        dx, dy = nxt.cx - box.cx, nxt.cy - box.cy
        start = _edge_point(box, dx, dy, clear=4)
        end = _edge_point(nxt, -dx, -dy, clear=8)
        parts.append(_arrow(start[0], start[1], end[0], end[1]))
    width = max(box.x + box.width for box in boxes)
    height = max(box.y + box.height for box in boxes)
    _ = tallest
    return width, height, "".join(parts)


'''
s = s.replace(old_cycle, new_cycle)
open(p, 'w').write(s)
print('written')
