// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.

import React from 'react';
import type { ActionDetail, NodeMeta, EdgeMeta } from '../types';
import type { ModelDescriptor } from '../api';
import { getActionTargetVoltageLevels, getActionTargetLines, isCouplingAction } from '../utils/svgUtils';
import { colors } from '../styles/tokens';
import { SeverityIcon, type SeverityKind } from './SeverityIcon';

interface ActionCardProps {
    id: string;
    details: ActionDetail;
    index: number;
    isViewing: boolean;
    isSelected: boolean;
    isRejected: boolean;
    linesOverloaded: string[];
    monitoringFactor: number;
    nodesByEquipmentId: Map<string, NodeMeta> | null;
    edgesByEquipmentId: Map<string, EdgeMeta> | null;
    cardEditMw: Record<string, string>;
    cardEditTap: Record<string, string>;
    resimulating: string | null;
    onActionSelect: (actionId: string | null) => void;
    onActionFavorite: (actionId: string) => void;
    onActionReject: (actionId: string) => void;
    onAssetClick: (actionId: string, assetName: string, tab?: 'action' | 'contingency') => void;
    onVlDoubleClick?: (actionId: string, vlName: string) => void;
    onCardEditMwChange: (actionId: string, value: string) => void;
    onCardEditTapChange: (actionId: string, value: string) => void;
    onResimulate: (actionId: string, newMw: number) => void;
    onResimulateTap: (actionId: string, newTap: number) => void;
    /** Resolve an element ID to its human-readable display name. Falls back to the ID. */
    displayName?: (id: string) => string;
    /**
     * Registered recommender models — used to resolve `details.origin`
     * (a model id) to a human-readable label in the unfolded card's
     * "Source" row. Absent / no-match falls back to the raw id.
     */
    availableModels?: ModelDescriptor[];
}

const clickableLinkStyle: React.CSSProperties = {
    background: 'none',
    border: 'none',
    cursor: 'pointer',
    padding: 0,
    fontSize: 'inherit',
    color: colors.brand,
    fontWeight: 600,
    textDecoration: 'underline dotted',
};

const ActionCard: React.FC<ActionCardProps> = ({
    id,
    details,
    index,
    isViewing,
    isSelected,
    isRejected,
    linesOverloaded,
    monitoringFactor,
    nodesByEquipmentId,
    edgesByEquipmentId,
    cardEditMw,
    cardEditTap,
    resimulating,
    onActionSelect,
    onActionFavorite,
    onActionReject,
    onAssetClick,
    onVlDoubleClick,
    onCardEditMwChange,
    onCardEditTapChange,
    onResimulate,
    onResimulateTap,
    displayName = (id: string) => id,
    availableModels,
}) => {
    const maxRhoPct = details.max_rho != null ? (details.max_rho * 100).toFixed(1) : null;

    // Provenance label for the unfolded card's "Source" row. `origin`
    // is either the literal `"user"` (manual simulation) or a
    // recommender model id — resolved here to the model's label.
    const originLabel = details.origin == null
        ? null
        : details.origin === 'user'
            ? 'Manual simulation (user)'
            : (availableModels?.find(m => m.name === details.origin)?.label ?? details.origin);
    const severity = details.max_rho != null
        ? (details.max_rho > monitoringFactor ? 'red' as const : details.max_rho > (monitoringFactor - 0.05) ? 'orange' as const : 'green' as const)
        : (details.is_rho_reduction ? 'green' as const : 'red' as const);
    const severityColors = {
        green: { border: colors.success, badgeBg: colors.successSoft, badgeText: colors.successText, label: 'Solves overload', kind: 'solves' as SeverityKind },
        orange: { border: colors.warningStrong, badgeBg: colors.warningSoft, badgeText: colors.warningText, label: 'Solved — low margin', kind: 'lowMargin' as SeverityKind },
        red: { border: colors.danger, badgeBg: colors.dangerSoft, badgeText: colors.dangerText, label: details.is_rho_reduction ? 'Still overloaded' : 'No reduction', kind: 'unsolved' as SeverityKind },
    };
    const sc = details.non_convergence
        ? { border: colors.danger, badgeBg: colors.danger, badgeText: colors.textOnBrand, label: 'divergent', kind: 'divergent' as SeverityKind }
        : details.is_islanded
            ? { border: colors.danger, badgeBg: colors.danger, badgeText: colors.textOnBrand, label: 'islanded', kind: 'islanded' as SeverityKind }
            : severityColors[severity];

    const renderRho = (arr: number[] | null, actionId: string, tab: 'action' | 'contingency' = 'action'): React.ReactNode => {
        if (!arr || arr.length === 0) return '—';
        const halfOpen = details.half_open_overloads;
        return arr.map((v, i) => {
            const lineName = linesOverloaded[i] || `line ${i}`;
            // A line the action left open at one end shows a non-zero loading
            // that is pure capacitive charging current (the diagrams show p = 0).
            // Annotate it with the live-end reactive power so the operator reads
            // it as charging current, not a residual overload.
            const reactiveMvar = halfOpen ? halfOpen[lineName] : undefined;
            return (
                <React.Fragment key={i}>
                    {i > 0 && ', '}
                    <button
                        style={clickableLinkStyle}
                        title={`Zoom to ${lineName}`}
                        onClick={(e) => { e.stopPropagation(); onAssetClick(actionId, lineName, tab); }}
                    >{displayName(lineName)}</button>
                    {`: ${(v * 100).toFixed(1)}%`}
                    {reactiveMvar != null && (
                        <span
                            style={{ color: colors.textTertiary, fontStyle: 'italic' }}
                            title="Line open at one end — this loading is the line's capacitive charging current (reactive power), not real power flow. The line carries no active power (the diagrams show 0)."
                        >
                            {` — open one end · ${reactiveMvar.toFixed(1)} MVAr capacitive`}
                        </span>
                    )}
                </React.Fragment>
            );
        });
    };

    const renderBadges = () => {
        const badges: React.ReactNode[] = [];
        const badgeBtn = (name: string, bg: string, color: string, title: string, onDoubleClick?: (e: React.MouseEvent) => void) => (
            <button key={name}
                style={{ padding: '2px 7px', borderRadius: '4px', border: 'none', cursor: 'pointer', fontSize: '11px', fontWeight: 600, textDecoration: 'underline dotted', flexShrink: 0, backgroundColor: bg, color }}
                title={title}
                onClick={(e) => { e.stopPropagation(); onAssetClick(id, name, 'action'); }}
                onDoubleClick={onDoubleClick}>
                {displayName(name)}
            </button>
        );

        // Collect badges from every source that applies. A combined
        // action like ``load_shedding_X+reco_Y`` owes a badge to BOTH
        // sub-actions — using an if/else-if/else here used to drop the
        // topology-based sub-action (reco / disco / coupling) whenever
        // the pair also contained a load-shedding or curtailment leg.
        const vlSet = new Set<string>();

        // Highest-priority signal: backend-supplied VL hint on the
        // topology blob. The recommender pipeline writes this for
        // pypowsybl switch-based / coupling actions (the dict_action
        // entry's ``VoltageLevelId``) so the operator gets a
        // clickable, double-clickable VL chip even when the action ID
        // is opaque (e.g. UUID-prefixed ``..._VLNAME_..._coupling``).
        const explicitVlHint = (details.action_topology as { voltage_level_id?: string } | undefined)?.voltage_level_id;
        if (explicitVlHint && !vlSet.has(explicitVlHint)) {
            vlSet.add(explicitVlHint);
            badges.push(badgeBtn(explicitVlHint, colors.successSoft, colors.successText, `Click: zoom to ${explicitVlHint} | Double-click: open SLD`, (e) => {
                e.stopPropagation();
                onVlDoubleClick?.(id, explicitVlHint);
            }));
        }

        details.load_shedding_details?.forEach(ls => {
            if (ls.voltage_level_id && !vlSet.has(ls.voltage_level_id)) {
                vlSet.add(ls.voltage_level_id);
                badges.push(badgeBtn(ls.voltage_level_id, colors.successSoft, colors.successText, `Click: zoom to ${ls.voltage_level_id} | Double-click: open SLD`, (e) => {
                    e.stopPropagation();
                    onVlDoubleClick?.(id, ls.voltage_level_id!);
                }));
            }
        });

        details.curtailment_details?.forEach(rc => {
            if (rc.voltage_level_id && !vlSet.has(rc.voltage_level_id)) {
                vlSet.add(rc.voltage_level_id);
                badges.push(badgeBtn(rc.voltage_level_id, colors.successSoft, colors.successText, `Click: zoom to ${rc.voltage_level_id} | Double-click: open SLD`, (e) => {
                    e.stopPropagation();
                    onVlDoubleClick?.(id, rc.voltage_level_id!);
                }));
            }
        });

        details.redispatch_details?.forEach(rd => {
            if (rd.voltage_level_id && !vlSet.has(rd.voltage_level_id)) {
                vlSet.add(rd.voltage_level_id);
                badges.push(badgeBtn(rd.voltage_level_id, colors.successSoft, colors.successText, `Click: zoom to ${rd.voltage_level_id} | Double-click: open SLD`, (e) => {
                    e.stopPropagation();
                    onVlDoubleClick?.(id, rd.voltage_level_id!);
                }));
            }
        });

        if (nodesByEquipmentId) {
            const vlNames = getActionTargetVoltageLevels(details, id, nodesByEquipmentId);
            vlNames.forEach(vlName => {
                if (vlSet.has(vlName)) return;
                vlSet.add(vlName);
                badges.push(badgeBtn(vlName, colors.successSoft, colors.successText, `Click: zoom to ${vlName} | Double-click: open SLD`, (e) => {
                    e.stopPropagation();
                    onVlDoubleClick?.(id, vlName);
                }));
            });
        }

        const isCoupling = isCouplingAction(id, details.description_unitaire);
        const lineNames = edgesByEquipmentId
            ? getActionTargetLines(details, id, edgesByEquipmentId)
            : Array.from(new Set([
                ...(isCoupling ? [] : Object.keys(details.action_topology?.lines_ex_bus || {})),
                ...(isCoupling ? [] : Object.keys(details.action_topology?.lines_or_bus || {})),
                ...Object.keys(details.action_topology?.pst_tap || {}),
            ]));

        lineNames.forEach(name => {
            if (badges.some(b => React.isValidElement(b) && b.key === name)) return;
            badges.push(badgeBtn(name, colors.brandSoft, colors.brand, `Zoom to ${name}`));
        });

        if (badges.length === 0) {
            const topo = details.action_topology;
            const equipNames = Array.from(new Set([
                ...Object.keys(topo?.gens_bus || {}),
                ...Object.keys(topo?.loads_bus || {}),
                ...Object.keys(topo?.loads_p || {}),
                ...Object.keys(topo?.gens_p || {}),
            ]));
            equipNames.forEach(name => {
                badges.push(badgeBtn(name, colors.brandSoft, colors.brand, `Zoom to ${name}`));
            });
        }

        return (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', flexShrink: 0, justifyContent: 'flex-end' }}>
                {badges}
            </div>
        );
    };

    const isFault = !!(details.non_convergence || details.is_islanded);
    // Higher-saturation accent stripe for the viewing card — replaces
    // the old vertical "VIEWING" ribbon with a quieter signal that
    // doesn't steal a column of horizontal space inside the card.
    const accentColor = isViewing ? colors.brandStrong : sc.border;

    const editorRowStyle: React.CSSProperties = {
        fontSize: '12px',
        padding: '6px 10px',
        marginTop: '5px',
        borderRadius: '4px',
        fontWeight: 500,
    };

    return (
        <div
            data-testid={`action-card-${id}`}
            data-viewing={isViewing ? 'true' : 'false'}
            className={`action-card${isViewing ? ' is-viewing' : ''}`}
            style={{
                background: isFault ? colors.dangerSoft : (isViewing ? colors.brandSoft : colors.surface),
                border: isFault ? `1px solid ${colors.danger}` : `1px solid ${colors.border}`,
                borderRadius: '8px',
                marginBottom: '10px',
                boxShadow: isViewing ? '0 2px 8px rgba(0,0,0,0.15)' : '0 2px 4px rgba(0,0,0,0.1)',
                borderLeft: `5px solid ${accentColor}`,
                cursor: 'pointer',
                transition: 'all 0.15s ease',
                overflow: 'hidden',
                padding: '10px',
                position: 'relative',
            }} onClick={() => onActionSelect(id)}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '6px' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: '5px', flex: 1, minWidth: 0 }}>
                    {/* Severity pictogram — placed before the title so the
                        operator immediately reads the outcome colour. */}
                    <span
                        data-testid={`action-card-${id}-severity`}
                        title={sc.label}
                        aria-label={sc.label}
                        role="img"
                        style={{
                            display: 'inline-flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            padding: '3px',
                            borderRadius: '50%',
                            background: sc.badgeBg,
                            color: sc.badgeText,
                            flexShrink: 0,
                            lineHeight: 0,
                        }}
                    >
                        <SeverityIcon kind={sc.kind} />
                    </span>
                    <h4 style={{
                        margin: 0,
                        fontSize: '12px',
                        color: isViewing ? colors.brandStrong : undefined,
                        flex: 1,
                        minWidth: 0,
                        overflowWrap: 'anywhere',
                        fontWeight: 700,
                        lineHeight: 1.35,
                    }}>
                        #{index + 1} {'—'} {id}
                    </h4>
                </div>
                {/* Star / reject rail — top-right, revealed on hover via
                    CSS (.action-card-rail opacity transition). */}
                <div className="action-card-rail" style={{ display: 'flex', gap: '3px', flexShrink: 0 }}>
                    {!isSelected && (
                        <button
                            data-testid={`favorite-${id}`}
                            onClick={(e) => { e.stopPropagation(); onActionFavorite(id); }}
                            style={{ background: colors.surface, border: `1px solid ${colors.border}`, borderRadius: '4px', cursor: 'pointer', padding: '2px 4px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
                            title="Select this action"
                        ><span style={{ fontSize: '15px', lineHeight: 1 }}>⭐</span></button>
                    )}
                    {!isRejected && (
                        <button
                            data-testid={`reject-${id}`}
                            onClick={(e) => { e.stopPropagation(); onActionReject(id); }}
                            style={{ background: colors.surface, border: `1px solid ${colors.border}`, borderRadius: '4px', cursor: 'pointer', padding: '2px 4px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
                            title={isSelected ? "Remove from selected" : "Reject this action"}
                        ><span style={{ fontSize: '15px', lineHeight: 1 }}>❌</span></button>
                    )}
                </div>
            </div>

            {/* Compact at-rest body: max loading + target badges. The
                row wraps so multi-VL badge stacks flow to a second
                line when the title is long or the badges don't fit
                alongside the loading metric. */}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', rowGap: '6px', gap: '8px', marginTop: '6px' }}>
                <div style={{ flex: '1 1 160px', fontSize: '12px', minWidth: 'min-content' }}>
                    {maxRhoPct != null ? (
                        <div>
                            Max loading: <strong style={{ color: sc.border }}>{maxRhoPct}%</strong>
                            {details.max_rho_line && (
                                <span style={{ color: colors.textTertiary }}> on <button
                                    style={{ ...clickableLinkStyle, color: colors.textTertiary }}
                                    title={`Zoom to ${details.max_rho_line}`}
                                    onClick={(e) => { e.stopPropagation(); onAssetClick(id, details.max_rho_line, 'action'); }}
                                >{displayName(details.max_rho_line)}</button></span>
                            )}
                        </div>
                    ) : (
                        <div style={{ color: colors.textTertiary }}>No loading metric</div>
                    )}
                </div>
                {renderBadges()}
            </div>

            {/* Fault states (divergent / islanded) are primary signals
                and stay visible regardless of the viewing-state — they
                replace the missing max-loading indicator. */}
            {details.non_convergence && (
                <div style={{ fontSize: '11px', color: colors.warningText, backgroundColor: colors.warningSoft, padding: '2px 6px', borderRadius: '4px', marginTop: '6px', border: `1px solid ${colors.warningBorder}`, display: 'inline-block' }}>
                    ⚠️ LoadFlow failure: {details.non_convergence}
                </div>
            )}
            {details.is_islanded && (
                <div style={{ fontSize: '12px', background: colors.dangerSoft, color: colors.danger, padding: '6px 10px', marginTop: '6px', borderRadius: '4px', border: `1px solid ${colors.danger}`, fontWeight: 500 }}>
                    🏝️ Islanding detected ({details.disconnected_mw?.toFixed(1)} MW disconnected)
                </div>
            )}

            {/* Progressive disclosure: description, parameter editors,
                and per-line "Loading after" only render on the viewing
                card. Keeps non-viewing cards to five fields each. */}
            {isViewing && (
                <div
                    data-testid={`action-card-${id}-disclosure`}
                    style={{ marginTop: '8px', borderTop: `1px solid ${colors.borderSubtle}`, paddingTop: '8px' }}
                    onClick={(e) => e.stopPropagation()}
                    onMouseDown={(e) => e.stopPropagation()}
                >
                    <p style={{ fontSize: '12px', margin: 0, color: colors.textPrimary }}>{details.description_unitaire}</p>

                    {details.load_shedding_details && details.load_shedding_details.length > 0 && (
                        <div style={{ ...editorRowStyle, background: colors.warningSoft, color: colors.warningText, border: `1px solid ${colors.warningBorder}` }}>
                            {details.load_shedding_details.map((ls, i) => (
                                <div key={ls.load_name} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginTop: i > 0 ? '4px' : 0 }}>
                                    <span>Shedding on <strong>{ls.load_name}</strong> in MW:</span>
                                    <input
                                        data-testid={`edit-mw-${id}`}
                                        type="number"
                                        min={0}
                                        step={0.1}
                                        value={cardEditMw[id] ?? ls.shedded_mw.toFixed(1)}
                                        onChange={(e) => onCardEditMwChange(id, e.target.value)}
                                        style={{ width: '65px', fontSize: '11px', fontFamily: 'monospace', padding: '2px 4px', border: `1px solid ${colors.warningStrong}`, borderRadius: '3px', textAlign: 'right' }}
                                    />
                                    <button
                                        data-testid={`resimulate-${id}`}
                                        onClick={() => {
                                            const mwVal = parseFloat(cardEditMw[id] ?? String(ls.shedded_mw));
                                            if (!isNaN(mwVal) && mwVal >= 0) onResimulate(id, mwVal);
                                        }}
                                        disabled={resimulating === id}
                                        style={{ fontSize: '10px', padding: '2px 8px', borderRadius: '3px', border: `1px solid ${colors.warningStrong}`, background: colors.warning, color: colors.textOnBright, cursor: resimulating === id ? 'wait' : 'pointer', fontWeight: 600, whiteSpace: 'nowrap' }}
                                    >
                                        {resimulating === id ? 'Simulating...' : 'Re-simulate'}
                                    </button>
                                </div>
                            ))}
                        </div>
                    )}

                    {details.curtailment_details && details.curtailment_details.length > 0 && (
                        <div style={{ ...editorRowStyle, background: colors.infoSoft, color: colors.infoText, border: `1px solid ${colors.infoBorder}` }}>
                            {details.curtailment_details.map((rc, i) => (
                                <div key={rc.gen_name} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginTop: i > 0 ? '4px' : 0 }}>
                                    <span>Curtailment on <strong>{rc.gen_name}</strong> in MW:</span>
                                    <input
                                        data-testid={`edit-mw-${id}`}
                                        type="number"
                                        min={0}
                                        step={0.1}
                                        value={cardEditMw[id] ?? rc.curtailed_mw.toFixed(1)}
                                        onChange={(e) => onCardEditMwChange(id, e.target.value)}
                                        style={{ width: '65px', fontSize: '11px', fontFamily: 'monospace', padding: '2px 4px', border: `1px solid ${colors.info}`, borderRadius: '3px', textAlign: 'right' }}
                                    />
                                    <button
                                        data-testid={`resimulate-${id}`}
                                        onClick={() => {
                                            const mwVal = parseFloat(cardEditMw[id] ?? String(rc.curtailed_mw));
                                            if (!isNaN(mwVal) && mwVal >= 0) onResimulate(id, mwVal);
                                        }}
                                        disabled={resimulating === id}
                                        style={{ fontSize: '10px', padding: '2px 8px', borderRadius: '3px', border: `1px solid ${colors.info}`, background: colors.infoBorder, color: colors.infoText, cursor: resimulating === id ? 'wait' : 'pointer', fontWeight: 600, whiteSpace: 'nowrap' }}
                                    >
                                        {resimulating === id ? 'Simulating...' : 'Re-simulate'}
                                    </button>
                                </div>
                            ))}
                        </div>
                    )}

                    {details.redispatch_details && details.redispatch_details.length > 0 && (
                        <div style={{ ...editorRowStyle, background: colors.infoSoft, color: colors.infoText, border: `1px solid ${colors.infoBorder}` }}>
                            {details.redispatch_details.map((rd, i) => {
                                // Signed-delta bounds from the generator's [min_p, max_p]:
                                // a raise can go up to +max_raise_mw, a lower down to -max_lower_mw.
                                const headroom = rd.direction === 'up' ? rd.max_raise_mw : rd.max_lower_mw;
                                const minDelta = rd.direction === 'up' ? 0 : (rd.max_lower_mw != null ? -rd.max_lower_mw : undefined);
                                const maxDelta = rd.direction === 'up' ? (rd.max_raise_mw ?? undefined) : 0;
                                const clamp = (v: number) => {
                                    if (minDelta != null) v = Math.max(minDelta, v);
                                    if (maxDelta != null) v = Math.min(maxDelta, v);
                                    return v;
                                };
                                return (
                                <div key={rd.gen_name} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginTop: i > 0 ? '4px' : 0 }}>
                                    <span>Redispatch on <strong>{rd.gen_name}</strong> ({rd.direction === 'up' ? 'raise' : 'lower'}) in MW:</span>
                                    <input
                                        data-testid={`edit-mw-${id}`}
                                        type="number"
                                        step={0.1}
                                        min={minDelta}
                                        max={maxDelta}
                                        value={cardEditMw[id] ?? rd.delta_mw.toFixed(1)}
                                        onChange={(e) => onCardEditMwChange(id, e.target.value)}
                                        style={{ width: '65px', fontSize: '11px', fontFamily: 'monospace', padding: '2px 4px', border: `1px solid ${colors.info}`, borderRadius: '3px', textAlign: 'right' }}
                                    />
                                    {headroom != null && (
                                        <span style={{ fontSize: '10px', color: colors.info }}>
                                            max {rd.direction === 'up' ? 'raise' : 'lower'}: {headroom.toFixed(0)} MW
                                        </span>
                                    )}
                                    <button
                                        data-testid={`resimulate-${id}`}
                                        onClick={() => {
                                            const raw = parseFloat(cardEditMw[id] ?? String(rd.delta_mw));
                                            if (!isNaN(raw)) onResimulate(id, clamp(raw));
                                        }}
                                        disabled={resimulating === id}
                                        style={{ fontSize: '10px', padding: '2px 8px', borderRadius: '3px', border: `1px solid ${colors.info}`, background: colors.infoBorder, color: colors.infoText, cursor: resimulating === id ? 'wait' : 'pointer', fontWeight: 600, whiteSpace: 'nowrap' }}
                                    >
                                        {resimulating === id ? 'Simulating...' : 'Re-simulate'}
                                    </button>
                                </div>
                                );
                            })}
                        </div>
                    )}

                    {details.pst_details && details.pst_details.length > 0 && (
                        <div style={{ ...editorRowStyle, background: colors.accentSoft, color: colors.accentText, border: `1px solid ${colors.accentBorder}` }}>
                            {details.pst_details.map((pst, i) => (
                                <div key={pst.pst_name} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', marginTop: i > 0 ? '4px' : 0 }}>
                                    <span>PST <strong>{pst.pst_name}</strong> tap:</span>
                                    <input
                                        data-testid={`edit-tap-${id}`}
                                        type="number"
                                        min={pst.low_tap ?? undefined}
                                        max={pst.high_tap ?? undefined}
                                        step={1}
                                        value={cardEditTap[id] ?? pst.tap_position}
                                        onChange={(e) => onCardEditTapChange(id, e.target.value)}
                                        style={{ width: '55px', fontSize: '11px', fontFamily: 'monospace', padding: '2px 4px', border: `1px solid ${colors.accent}`, borderRadius: '3px', textAlign: 'right' }}
                                    />
                                    {pst.low_tap != null && pst.high_tap != null && (
                                        <span style={{ fontSize: '10px', color: colors.accent }}>
                                            [{pst.low_tap}..{pst.high_tap}]
                                        </span>
                                    )}
                                    <button
                                        data-testid={`resimulate-tap-${id}`}
                                        onClick={() => {
                                            const tapVal = parseInt(cardEditTap[id] ?? String(pst.tap_position), 10);
                                            if (!isNaN(tapVal)) onResimulateTap(id, tapVal);
                                        }}
                                        disabled={resimulating === id}
                                        style={{ fontSize: '10px', padding: '2px 8px', borderRadius: '3px', border: `1px solid ${colors.accent}`, background: colors.accentBorder, color: colors.accentText, cursor: resimulating === id ? 'wait' : 'pointer', fontWeight: 600, whiteSpace: 'nowrap' }}
                                    >
                                        {resimulating === id ? 'Simulating...' : 'Re-simulate'}
                                    </button>
                                </div>
                            ))}
                        </div>
                    )}

                    {/* "Loading before" stays in the sticky Overloads N-1
                        section of the left feed — no need to duplicate
                        it per card (see git blame for the original
                        rationale). */}
                    <div style={{ fontSize: '12px', background: colors.brandSoft, padding: '5px', marginTop: '8px', borderRadius: '4px' }}>
                        Overload loading after: {renderRho(details.rho_after, id, 'action')}
                    </div>

                    {/* Source / provenance sits at the very bottom of
                        the unfolded card — it's a low-information
                        attribution row that shouldn't compete with the
                        operational fields above. */}
                    {originLabel && (
                        <div
                            data-testid={`action-card-${id}-origin`}
                            style={{ fontSize: '11px', marginTop: '8px', color: colors.textTertiary }}
                        >
                            Source: <strong style={{ color: colors.textSecondary }}>{originLabel}</strong>
                        </div>
                    )}
                </div>
            )}
        </div>
    );
};

export default React.memo(ActionCard);
