#!/usr/bin/env python3
"""bentofy.py · migra una pantalla del prototipo Pulso a la capa Bento.

Uso:
  python3 bentofy.py <ruta/pantalla.html>           informe, sin escribir
  python3 bentofy.py <ruta/pantalla.html> --write   reescribe la pantalla

Hace, en este orden:
  1. desenvuelve los <div style="display:grid;…"> que agrupaban tarjetas;
  2. convierte cada <a|div class="card…"> en <… class="tile [span]" style="--i:N">
     con el span de SPANS[pantalla] en orden DOM;
  3. sustituye la anatomía inline repetida por clases (SUBS);
  4. resuelve los contenedores ambiguos (cifra con flecha frente a leyenda);
  5. envuelve el tramo de tiles de <main> en <div class="bento"> y marca con
     bento__full los hermanos que no son tiles;
  6. si hay sparkline de 326x72, la cambia por el gráfico nuevo (variante B).
Al final lista los style="…" que quedan, con su recuento, para el retoque manual.
"""
import re, sys, pathlib

# ---- spans por pantalla, en orden DOM de las tarjetas ------------------------
SPANS = {
    "index.html":      ["hero", "tall", "", "wide", "", "", "", "", ""],
    "ventas.html":     ["hero", "third", "third", "third", "wide", "wide", "wide"],
    "salud.html":      ["hero", "wide"],
    "margenes.html":   ["hero", "wide", "wide", "wide"],
    "canales.html":    ["hero", "wide", "wide"],
    "control.html":    ["hero", "", "", "", "", "wide", "wide"],
    "prevision.html":  ["hero", "wide"],
    "carta.html":      ["hero", "wide", "wide"],
    "clientes.html":   ["hero", "", "", "wide", "wide", "wide", "wide"],
    "ritmo.html":      ["hero", "wide", "wide", "wide"],
    "aviso.html":      ["hero", "wide"],
    "decisiones.html": ["hero", "", "", "wide"],
    "costes.html":     ["wide", "wide", "wide", "wide", "wide"],
}

VOID = {"br", "img", "input", "path", "circle", "polyline", "polygon", "line", "meta", "link", "stop", "hr"}
TAG = re.compile(r"<(/?)([a-zA-Z][\w-]*)([^>]*?)(/?)>")

def find_close(html, open_start):
    """Índice justo después del cierre del elemento que abre en open_start."""
    depth = 0
    for m in TAG.finditer(html, open_start):
        closing, tag, _, selfclose = m.group(1), m.group(2).lower(), m.group(3), m.group(4)
        if tag in VOID or selfclose:
            continue
        depth += -1 if closing else 1
        if depth == 0:
            return m.end()
    raise ValueError("cierre no encontrado")

def children(html, start, end):
    """Rangos (ini, fin) de los hijos directos del elemento cuyo contenido va de start a end."""
    out, pos = [], start
    while True:
        m = TAG.search(html, pos, end)
        if not m or m.group(1):
            break
        tag = m.group(2).lower()
        if tag in VOID or m.group(4):
            pos = m.end(); continue
        close = find_close(html, m.start())
        out.append((m.start(), close)); pos = close
    return out

# ---- anatomía inline → clases (orden importa) ------------------------------
HEAD_INNER = '<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;">'
SUBS = [
    (HEAD_INNER + HEAD_INNER, '<div class="tile__head">'),
    ('<p style="font-size:14px;line-height:16px;font-weight:400;color:var(--text-accent);margin:0;letter-spacing:1.4px;text-transform:uppercase;">', '<p class="tile__label">'),
    ('</p></div><div style="flex-grow:1;"></div><span class="ds" data-ds="Icon" data-props=\'{"name": "nav_forward", "size": 20}\' style="color:var(--text-subtle);"></span></div>',
     '</p><span class="tile__chevron ds" data-ds="Icon" data-props=\'{"name": "nav_forward", "size": 20}\'></span></div>'),
    ('</p></div><div style="flex-grow:1;"></div>', '</p><span class="grow"></span>'),
    (HEAD_INNER, '<div class="tile__kv">'),
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:14px;flex-wrap:wrap;">', '<div class="tile__row tile__row--tight">'),
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:16px;">', '<div class="tile__row">'),
    ('<div style="display:flex;align-items:baseline;gap:6px;">', '<div class="tile__figure">'),
    ('<span style="font-size:44px;line-height:52px;font-weight:300;color:var(--text-primary);margin:0;">', '<span>'),
    ('<span style="font-size:33px;line-height:40px;font-weight:300;color:var(--text-primary);margin:0;">', '<span class="tile__figure--m">'),
    ('<span style="font-size:28px;line-height:36px;font-weight:300;color:var(--text-primary);margin:0;">', '<span>'),
    ('<span style="font-size:15px;line-height:20px;font-weight:300;color:var(--app-text-support);margin:0;">', '<span class="tile__unit">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;width:96px;flex-shrink:0;">', '<p class="tile__kv-label">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;width:56px;text-align:right;flex-shrink:0;color:var(--text-primary);">', '<p class="tile__kv-value">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-ok-text);margin:0;">', '<p class="tile__note tile__note--ok">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-warn-text);margin:0;">', '<p class="tile__note tile__note--warn">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-bad-text);margin:0;">', '<p class="tile__note tile__note--bad">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-warn-text);margin:0;text-align:right;">', '<p class="tile__note tile__note--warn tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-bad-text);margin:0;text-align:right;">', '<p class="tile__note tile__note--bad tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;text-align:right;">', '<p class="tile__note tile__note--light tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;color:var(--text-primary);">', '<p class="tile__note tile__note--primary">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;text-align:center;">', '<p class="tile__note tile__note--light tile__note--center">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:300;color:var(--app-text-support);margin:0;">', '<p class="tile__note tile__note--light">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;">', '<p class="tile__note">'),
    ('<p style="font-size:16px;line-height:22px;font-weight:400;color:var(--text-primary);margin:0;">', '<p class="tile__body tile__body--regular">'),
    ('<p style="font-size:16px;line-height:22px;font-weight:300;color:var(--text-primary);margin:0;">', '<p class="tile__body">'),
    ('<div style="display:flex;flex-direction:row;height:8px;border-radius:6px;overflow:hidden;background:var(--border-subtle);">', '<div class="tile__bar">'),
    ('<div style="height:1px;background:var(--border-subtle);"></div>', '<div class="tile__divider"></div>'),
    ('<span style="color:var(--text-primary);font-weight:400;">', '<span class="tile__strong">'),
]
# icono de cabecera con el color inline del acento: el color lo hereda de .tile__head
ICON_ACCENT = re.compile(r'(<span class="ds" data-ds="Icon" data-props=\'[^\']*\') style="color:var\(--text-accent\);"></span> ?')
# segmento de barra: quitar el alto, dejar ancho y color
BAR_SEG = re.compile(r'<div style="width:([0-9.]+%);height:8px;background:(var\(--[a-z-]+\));"></div>')
# punto de leyenda
DOT = re.compile(r'<div style="width:10px;height:10px;border-radius:5px;background:(var\(--[a-z-]+\));"></div>')
CARD = re.compile(r'<(a|div) class="card(?: card--link)?"((?: href="[^"]+")?) style="[^"]*">')
GRID = re.compile(r'<div style="display:grid;grid-template-columns:repeat\(\d, minmax\(0, 1fr\)\);gap:12px;">')
AMBIG = '<div style="display:flex;align-items:center;gap:6px;">'

def unwrap_grids(html):
    while True:
        m = GRID.search(html)
        if not m:
            return html
        close = find_close(html, m.start())
        inner = html[m.end():close - len("</div>")]
        html = html[:m.start()] + inner + html[close:]

def convert_cards(html, spans):
    n = 0
    def rep(m):
        nonlocal n
        span = spans[n] if n < len(spans) else ""
        cls = "tile" + (f" tile--{span}" if span else "")
        out = f'<{m.group(1)} class="{cls}"{m.group(2)} style="--i:{n};">'
        n += 1
        return out
    html = CARD.sub(rep, html)
    return html, n

def resolve_ambiguous(html):
    pos = 0
    while True:
        i = html.find(AMBIG, pos)
        if i < 0:
            return html
        close = find_close(html, i)
        inner = html[i + len(AMBIG):close]
        big = re.search(r"font-size:(44|33|28)px", inner)
        new = '<div class="tile__figure tile__figure--center">' if big else '<div class="tile__legend">'
        html = html[:i] + new + html[i + len(AMBIG):]
        pos = i + len(new)

def wrap_bento(html):
    m_open = re.search(r'<main class="content"[^>]*>', html)
    m_close = html.find("</main>", m_open.end())
    kids = children(html, m_open.end(), m_close)
    tiles = [k for k in kids if re.match(r'<(a|div) class="tile', html[k[0]:k[0] + 40])]
    if not tiles:
        return html
    first, last = tiles[0][0], tiles[-1][1]
    out, pos = [], first
    for k in kids:
        if first <= k[0] < last and k not in tiles:
            seg = html[k[0]:k[1]]
            seg = re.sub(r'^<(\w+) class="', r'<\1 class="bento__full ', seg, count=1) if re.match(r'<\w+ class="', seg) else re.sub(r'^<(\w+)', r'<\1 class="bento__full"', seg, count=1)
            out.append(html[pos:k[0]] + seg); pos = k[1]
    out.append(html[pos:last])
    return html[:first] + '<div class="bento">' + "".join(out) + '</div>' + html[last:]

def clean_main_gap(html):
    return html.replace('<main class="content" style="gap:12px;">', '<main class="content">')

# ---- gráfico de Ventas (variante B), portado de gen-chart.py ------------------
W, H, PAD_T, PAD_B, PLOT_R = 326.0, 120.0, 10.0, 10.0, 318.0
TICKS = [(6, "4 may"), (13, "11 may"), (20, "18 may"), (27, "25 may")]

def smooth(vals, win=3):
    half = win // 2
    out = [sum(vals[max(0, i - half):i + half + 1]) / len(vals[max(0, i - half):i + half + 1]) for i in range(len(vals))]
    for k in range(min(3, len(vals))):
        i = len(vals) - 1 - k; w = 1 - k / 3
        out[i] = w * vals[i] + (1 - w) * out[i]
    return out

def monotone_path(xs, ys):
    k = len(xs)
    d = [(ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]) for i in range(k - 1)]
    m = [0.0] * k; m[0], m[-1] = d[0], d[-1]
    for i in range(1, k - 1):
        if d[i - 1] * d[i] <= 0: m[i] = 0.0
        else:
            h0, h1 = xs[i] - xs[i - 1], xs[i + 1] - xs[i]
            m[i] = 3 * (h0 + h1) / ((2 * h1 + h0) / d[i - 1] + (h1 + 2 * h0) / d[i])
    parts = [f"M{xs[0]},{ys[0]}"]
    for i in range(k - 1):
        h = xs[i + 1] - xs[i]
        parts.append(f"C{xs[i] + h / 3:.1f},{ys[i] + m[i] * h / 3:.1f} {xs[i + 1] - h / 3:.1f},{ys[i + 1] - m[i + 1] * h / 3:.1f} {xs[i + 1]},{ys[i + 1]}")
    return " ".join(parts)

def chart_block(svg_old, gid="grad-ventas"):
    polys = re.findall(r'<polyline points="([^"]+)"', svg_old)
    pts = lambda s: [float(p.split(",")[1]) for p in s.split()]
    expected, real = pts(polys[0]), pts(polys[1]); n = len(real)
    xs = [round(i / (n - 1) * PLOT_R, 1) for i in range(n)]
    rs, es = smooth(real), expected
    lo, hi = min(rs + es), max(rs + es); pad = (hi - lo) * 0.06
    fit = lambda v: round(PAD_T + (v - (lo - pad)) / ((hi + pad) - (lo - pad)) * (H - PAD_T - PAD_B), 1)
    ry, ey = [fit(v) for v in rs], [fit(v) for v in es]
    line = monotone_path(xs, ry); area = f"{line} L{xs[-1]},{H:g} L{xs[0]},{H:g} Z"; exp = monotone_path(xs, ey)
    grid = "".join(f'<line class="chart__grid" x1="{xs[i]}" y1="0" x2="{xs[i]}" y2="{H:g}"/>' for i, _ in TICKS)
    labels = "".join(f'<span style="left:{xs[i] / W * 100:.1f}%;">{t}</span>' for i, t in TICKS)
    dx, dy = xs[-1], ry[-1]
    svg = (f'<svg viewBox="0 0 {W:g} {H:g}" preserveAspectRatio="none" aria-label="Ventas de los últimos 30 días frente a lo esperado">'
           f'<defs><linearGradient id="{gid}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" class="chart__grad-a"/><stop offset="1" class="chart__grad-b"/></linearGradient></defs>'
           f'{grid}<path d="{area}" fill="url(#{gid})"/><path class="chart__expected" d="{exp}"/><path class="chart__line" d="{line}"/>'
           f'<circle class="chart__halo" cx="{dx}" cy="{dy}" r="9"/><circle class="chart__dot" cx="{dx}" cy="{dy}" r="4"/></svg>')
    tip = (f'<div class="chart__tip" style="right:calc(100% - {dx / W * 100:.1f}% + 16px);top:{max(0, dy - 27):.0f}px;">'
           f'<span class="chart__tip-date">Hoy · 27 may</span><span class="chart__tip-value">2.957 €</span></div>')
    legend = ('<div class="tile__row tile__row--tight">'
              '<span class="tile__legend"><span class="tile__dot" style="background:var(--app-chart-accent);"></span>venta real · 30 días</span>'
              '<span class="tile__legend"><span class="tile__dot" style="background:var(--text-secondary);"></span>esperado</span></div>')
    return f'<div class="chart">{svg}{tip}<div class="chart__axis">{labels}</div></div>{legend}'

def replace_chart(html):
    m = re.search(r'<svg viewBox="0 0 326 (?:72|80)"[^>]*>.*?</svg>', html, re.S)
    if not m:
        return html
    # la leyenda vieja es el siguiente hermano: la fila con gap 14 ya convertida a tile__row--tight
    end = m.end()
    if html.startswith('<div class="tile__row tile__row--tight">', end):
        end = find_close(html, end)
    return html[:m.start()] + chart_block(m.group(0)) + html[end:]

def migrate(path):
    html = path.read_text(encoding="utf-8")
    html = unwrap_grids(html)
    html, n = convert_cards(html, SPANS.get(path.name, []))
    html = resolve_ambiguous(html)
    for a, b in SUBS:
        html = html.replace(a, b)
    html = ICON_ACCENT.sub(r"\1></span>", html)
    html = BAR_SEG.sub(r'<div style="width:\1;background:\2;"></div>', html)
    html = DOT.sub(r'<span class="tile__dot" style="background:\1;"></span>', html)
    html = clean_main_gap(html)
    html = wrap_bento(html)
    html = replace_chart(html)
    return html, n

def report(html):
    styles = re.findall(r'style="([^"]*)"', re.search(r"<body>(.*)</body>", html, re.S).group(1))
    keep = re.compile(r"^(--i:\d+;|width:[0-9.]+%;background:var\(--[a-z-]+\);|background:var\(--[a-z-]+\);|right:calc[^;]+;top:[0-9-]+px;|left:[0-9.]+%;|display:block;width:100%;)$")
    counts = {}
    for s in styles:
        if not keep.match(s):
            counts[s] = counts.get(s, 0) + 1
    return sorted(counts.items(), key=lambda kv: -kv[1])


# =============================================================================
# Extensión tras la primera prueba en seco: patrones repetidos de las secciones
# =============================================================================
SPANS["salud.html"] = ["hero", "row", "row", "row", "row", "row", "wide"]
SPANS["prevision.html"] = ["hero", "row", "row", "wide"]

SUBS += [
    # filas clave-valor y rótulos pequeños
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:12px;">', '<div class="tile__kv">'),
    ('<div style="display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:10px;">', '<div class="tile__kv">'),
    ('<div style="flex-grow:1;"></div>', '<span class="grow"></span>'),
    ('<div style="flex-grow:1;height:10px;border-radius:5px;background:var(--border-subtle);overflow:hidden;">', '<div class="tile__kv-track">'),
    ('<p style="font-size:15px;line-height:20px;font-weight:300;color:var(--text-primary);margin:0;color:var(--app-text-support);">', '<p class="tile__key">'),
    ('<p style="font-size:15px;line-height:20px;font-weight:400;color:var(--text-primary);margin:0;">', '<p class="tile__val">'),
    ('<p style="font-size:15px;line-height:20px;font-weight:400;color:var(--app-status-ok-text);margin:0;">', '<p class="tile__val tile__val--ok">'),
    ('<p style="font-size:15px;line-height:20px;font-weight:400;color:var(--app-status-bad-text);margin:0;">', '<p class="tile__val tile__val--bad">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;width:110px;flex-shrink:0;">', '<p class="tile__kv-label tile__kv-label--w">'),
    ('<p style="font-size:12px;line-height:16px;font-weight:400;color:var(--app-text-support);margin:0;letter-spacing:1.2px;text-transform:uppercase;text-align:right;">', '<p class="tile__caps tile__note--right">'),
    ('<p style="font-size:12px;line-height:16px;font-weight:400;color:var(--app-text-support);margin:0;letter-spacing:1.2px;text-transform:uppercase;padding:4px 0 0;">', '<p class="bento__heading">'),
    ('<p style="font-size:12px;line-height:16px;font-weight:400;color:var(--app-text-support);margin:0;letter-spacing:1.2px;text-transform:uppercase;padding:8px 0 0;">', '<p class="bento__heading">'),
    ('<p style="font-size:12px;line-height:16px;font-weight:400;color:var(--app-text-support);margin:0;letter-spacing:1.2px;text-transform:uppercase;">', '<p class="tile__caps">'),
    ('<p style="font-size:11px;line-height:14px;font-weight:400;color:var(--app-text-support);margin:0;letter-spacing:1px;text-transform:uppercase;">', '<p class="tile__tag">'),
    ('<p style="font-size:11px;line-height:14px;font-weight:400;color:var(--app-status-warn-text);margin:0;letter-spacing:1px;text-transform:uppercase;">', '<p class="tile__tag tile__tag--warn">'),
    ('<p style="font-size:11px;line-height:14px;font-weight:400;color:var(--app-status-ok-text);margin:0;letter-spacing:1px;text-transform:uppercase;">', '<p class="tile__tag tile__tag--ok">'),
    ('<p style="font-size:20px;line-height:28px;font-weight:300;color:var(--app-text-support);margin:0;">', '<p class="tile__unit tile__unit--l">'),
    ('<p style="font-size:10px;line-height:12px;font-weight:400;color:var(--app-text-support);margin:0;">', '<p class="tile__col-label">'),
    ('<span style="font-size:28px;line-height:36px;font-weight:300;color:var(--app-status-bad-text);margin:0;">', '<span class="tile__note--bad">'),
    ('<span style="font-weight:400;">', '<span class="tile__strong">'),
    # barra gruesa
    ('<div style="display:flex;flex-direction:row;height:14px;border-radius:6px;overflow:hidden;background:var(--border-subtle);">', '<div class="tile__bar tile__bar--thick">'),
    # lista con icono (Canal a canal)
    ('<div style="display:flex;flex-direction:row;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border-subtle);">', '<div class="tile__list-row">'),
    ('<div style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--text-primary);flex-shrink:0;">', '<div class="tile__list-icon">'),
    ('<div style="display:flex;flex-direction:column;gap:2px;flex-grow:1;min-width:0;">', '<div class="tile__list-main">'),
    ('<div style="display:flex;flex-direction:column;gap:2px;align-items:flex-end;flex-shrink:0;">', '<div class="tile__list-aside">'),
    # columnas de gráfico
    ('<div style="display:flex;flex-direction:column;align-items:center;gap:6px;flex-grow:1;">', '<div class="tile__col">'),
    ('<div style="display:flex;flex-direction:column;align-items:center;gap:4px;flex-grow:1;">', '<div class="tile__col">'),
    ('<div style="position:relative;width:100%;height:96px;display:flex;align-items:flex-end;">', '<div class="tile__col-track">'),
    ('<div style="position:relative;width:100%;height:120px;display:flex;align-items:flex-end;">', '<div class="tile__col-track tile__col-track--tall">'),
    ('<div style="width:100%;height:72px;display:flex;align-items:flex-end;">', '<div class="tile__col-track tile__col-track--short">'),
    # ítem de aviso: punto de estado y columna de texto
    ('<div style="display:flex;flex-direction:column;gap:4px;flex-grow:1;">', '<div class="tile__stack">'),
]
SUBS += [
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-status-ok-text);margin:0;text-align:right;">', '<p class="tile__note tile__note--ok tile__note--right">'),
    ('<p style="font-size:13px;line-height:18px;font-weight:400;color:var(--app-text-support);margin:0;text-align:right;">', '<p class="tile__note tile__note--right">'),
    ('<p style="font-size:24px;line-height:30px;font-weight:400;color:var(--text-primary);margin:0;">', '<p class="tile__title">'),
    ('<div style="width:100%;height:99%;position:absolute;bottom:0;left:0;border-radius:4px;border:1px dashed var(--app-chart-accent);box-sizing:border-box;"></div>', '<div class="tile__col-outline"></div>'),
]
SUBS += [(a.replace('<p style=', '<span style=', 1), b.replace('<p class=', '<span class=', 1)) for a, b in SUBS if a.startswith('<p style=')]
STATUS_DOT = re.compile(r'<div style="width:12px;height:12px;border-radius:6px;background:(var\(--[a-z-]+\));margin-top:5px;flex-shrink:0;"></div>')
BAR_SEG = re.compile(r'<div style="width:([0-9.]+%);height:(?:8|10|14)px;background:(var\(--[a-z-]+\));"></div>')
KV_FILL = re.compile(r'<div style="width:([0-9.]+%);height:10px;border-radius:5px;background:(var\(--[a-z-]+\));"></div>')
COL_BAR = re.compile(r'<div style="width:100%;height:([0-9.]+%);background:(var\(--[a-z-]+\));border-radius:[34]px;"></div>')
TABLE_ROW = re.compile(r'<div style="display:grid;grid-template-columns:([^;]+);gap:(?:6|8)px;align-items:baseline;padding:(?:8|10)px 0;border-bottom:1px solid var\(--border-subtle\);">')
TABLE_HEAD = re.compile(r'<div style="display:grid;grid-template-columns:([^;]+);gap:6px;padding:0 0 6px;border-bottom:1px solid var\(--border-default\);">')
BTN = re.compile(r'(<a[^>]*?) style="display:flex;align-items:center;justify-content:center;height:64px;border-radius:8px;background:var\(--action-(secondary|primary)\);color:var\(--text-on-(?:secondary|accent)\);font-size:20px;line-height:24px;font-weight:400;text-transform:uppercase;text-decoration:none;flex-grow:1;">')
STEP = re.compile(r'(<a[^>]*?) style="display:flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:8px;background:var\(--action-secondary\);color:var\(--text-on-secondary\);text-decoration:none;">')
CHEVRON_SUBTLE = re.compile(r'<span class="ds" data-ds="Icon" data-props=\'\{"name": "nav_forward", "size": 20\}\' style="color:var\(--text-subtle\);"></span>')
ALERT_STYLE = 'display:flex;flex-direction:row;align-items:flex-start;gap:12px;padding:14px 16px;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:12px;text-decoration:none;color:inherit;'
ITEM = re.compile(r'<(a|div) class="card(?: card--link)?"((?: href="[^"]+")?) style="[^"]*">|<(a|div)((?: href="[^"]+")?) style="' + re.escape(ALERT_STYLE) + '">')
FLEXEND = re.compile(r'<div style="display:flex;flex-direction:row;align-items:flex-end;justify-content:flex-start;gap:(?:4|6|8)px;">')

def convert_cards(html, spans):
    n = 0
    def rep(m):
        nonlocal n
        tag = m.group(1) or m.group(3); attrs = m.group(2) or m.group(4) or ""
        span = spans[n] if n < len(spans) else ""
        if m.group(3) and not span:
            span = "row"
        cls = "tile" + {"": "", "row": " tile--wide tile--row"}.get(span, f" tile--{span}")
        out = f'<{tag} class="{cls}"{attrs} style="--i:{n};">'
        n += 1
        return out
    return ITEM.sub(rep, html), n

def resolve_flexend(html):
    """Fila alineada abajo: columnas de gráfico si contiene columnas, si no fila de cifra."""
    pos = 0
    while True:
        m = FLEXEND.search(html, pos)
        if not m:
            return html
        close = find_close(html, m.start())
        inner = html[m.end():close]
        new = '<div class="tile__cols">' if ('tile__col"' in inner or 'flex-direction:column;align-items:center' in inner) else '<div class="tile__kv tile__kv--end">'
        html = html[:m.start()] + new + html[m.end():]
        pos = m.start() + len(new)

def wrap_bento(html):
    m_open = re.search(r'<main class="content"[^>]*>', html)
    m_close = html.find("</main>", m_open.end())
    kids = children(html, m_open.end(), m_close)
    is_tile = lambda k: re.match(r'<(a|div) class="tile(?: |")', html[k[0]:k[0] + 40]) is not None
    holds = lambda k: re.search(r'class="tile(?: |")', html[k[0]:k[1]]) is not None
    rel = [k for k in kids if is_tile(k) or holds(k)]
    if not rel:
        return html
    first, last = rel[0][0], rel[-1][1]
    out, pos = [], first
    for k in kids:
        if not (first <= k[0] < last):
            continue
        if is_tile(k):
            continue
        seg = html[k[0]:k[1]]
        if holds(k):                       # envoltorio de lista: se desenvuelve
            open_end = TAG.match(seg).end()
            seg = seg[open_end:seg.rfind("</")]
        elif re.match(r'<\w+ class="', seg):
            seg = re.sub(r'^<(\w+) class="', r'<\1 class="bento__full ', seg, count=1)
        else:
            seg = re.sub(r'^<(\w+)', r'<\1 class="bento__full"', seg, count=1)
        out.append(html[pos:k[0]] + seg); pos = k[1]
    out.append(html[pos:last])
    return html[:first] + '<div class="bento">' + "".join(out) + '</div>' + html[last:]

def migrate(path):
    html = path.read_text(encoding="utf-8")
    html = unwrap_grids(html)
    html, n = convert_cards(html, SPANS.get(path.name, []))
    html = resolve_ambiguous(html)
    for a, b in SUBS:
        html = html.replace(a, b)
    html = resolve_flexend(html)
    html = ICON_ACCENT.sub(r"\1></span>", html)
    html = CHEVRON_SUBTLE.sub('<span class="tile__chevron ds" data-ds="Icon" data-props=\'{"name": "nav_forward", "size": 20}\'></span>', html)
    html = STATUS_DOT.sub(r'<span class="tile__status" style="background:\1;"></span>', html)
    html = KV_FILL.sub(r'<div class="tile__kv-fill" style="width:\1;background:\2;"></div>', html)
    html = COL_BAR.sub(r'<div class="tile__col-bar" style="height:\1;background:\2;"></div>', html)
    html = BAR_SEG.sub(r'<div style="width:\1;background:\2;"></div>', html)
    html = DOT.sub(r'<span class="tile__dot" style="background:\1;"></span>', html)
    html = TABLE_ROW.sub(r'<div class="tile__table-row" style="grid-template-columns:\1;">', html)
    html = TABLE_HEAD.sub(r'<div class="tile__table-row tile__table-row--head" style="grid-template-columns:\1;">', html)
    html = BTN.sub(r'\1 class="btn btn--\2">', html)
    html = STEP.sub(r'\1 class="btn btn--secondary btn--step">', html)
    html = clean_main_gap(html)
    html = wrap_bento(html)
    html = replace_chart(html)
    return html, n

def report(html):
    styles = re.findall(r'style="([^"]*)"', re.search(r"<body>(.*)</body>", html, re.S).group(1))
    keep = re.compile(r"^(--i:\d+;|width:[0-9.]+%;background:var\(--[a-z-]+\);|height:[0-9.]+%;background:var\(--[a-z-]+\);|background:var\(--[a-z-]+\);|right:calc[^;]+;top:[0-9-]+px;|left:[0-9.]+%;|display:block;width:100%;|display:inline-flex;|grid-template-columns:[^;]+;)$")
    counts = {}
    for st in styles:
        if not keep.match(st):
            counts[st] = counts.get(st, 0) + 1
    return sorted(counts.items(), key=lambda kv: -kv[1])


TILE_OPEN = re.compile(r'<(a|div) class="tile(?: |")[^>]*>')

def flatten_nested(html):
    """Saca las tiles anidadas dentro de otra tile y las coloca, en su orden, como hermanas detrás de ella."""
    while True:
        moved = False
        for m in TILE_OPEN.finditer(html):
            end = find_close(html, m.start())
            blocks, cuts, pos = [], [], m.end()
            while True:
                inner = TILE_OPEN.search(html, pos, end)
                if not inner:
                    break
                i_end = find_close(html, inner.start())
                blocks.append(html[inner.start():i_end]); cuts.append((inner.start(), i_end)); pos = i_end
            if not blocks:
                continue
            parent = html[m.start():end]
            for a, b in reversed(cuts):
                parent = parent[:a - m.start()] + parent[b - m.start():]
            html = html[:m.start()] + parent + "".join(blocks) + html[end:]
            moved = True
            break
        if not moved:
            return html

def renumber(html):
    """Reasigna --i en orden de aparición de las tiles (0, 1, 2…)."""
    n = [0]
    def rep(m):
        out = m.group(1) + f'style="--i:{n[0]};"'; n[0] += 1; return out
    return re.sub(r'(<(?:a|div) class="tile(?: |")[^>]*?)style="--i:\d+;"', rep, html)

_migrate_prev = migrate
def migrate(path):
    html, n = _migrate_prev(path)
    html = renumber(flatten_nested(html))
    return html, n
if __name__ == "__main__":
    path = pathlib.Path(sys.argv[1]); write = "--write" in sys.argv
    html, n = migrate(path)
    left = html.count('class="card')
    print(f"{path.name}: {n} tarjetas → tiles · class=card queda {left} veces")
    for s, c in report(html):
        print(f"  {c:3d} × style=\"{s[:140]}\"")
    if write:
        path.write_text(html, encoding="utf-8"); print("escrito")
