// Visual components for the dispatch frontend.
//
// The map is plain SVG (no external graph lib) — sufficient for ~10 nodes and
// gives us full control over courier animations. Couriers are rendered as
// foreign objects whose (x, y) interpolates between their previous and target
// node when in transit, driven by `eta_remaining` and `step_progress`.

import React from 'react';
import { STATUS_COLORS, COURIER_STATUS_LABELS } from './fixtures.js';

// Helper: find a node by id
const nodeById = (nodes, id) => nodes.find((n) => n.id === id);

// Distribute couriers so they don't stack on top of each other when several
// share a location (e.g. all 5 couriers at the hub at t=0, or two heading to
// the same store). Returns a Map<courier_id, {x, y}>.
//
// Two cluster types:
//   - "node":  multiple couriers at the same node          -> fan out in a circle
//   - "edge":  multiple couriers traversing the same edge  -> fan out perpendicular
//              to the edge direction (so they look like a row of bikes)
//
// Couriers in transit are rendered slightly past the midpoint of their edge
// (t=0.55) so the eye reads them as "on the way" rather than at either endpoint.
function distributeCouriers(couriers, nodes) {
  const positions = new Map();
  const groups = new Map();

  for (const c of couriers) {
    const here = nodeById(nodes, c.node_id);
    if (!here) continue;

    let entry;
    if (c.target_node_id && c.eta_remaining > 0 && c.status !== 'idle') {
      const there = nodeById(nodes, c.target_node_id);
      if (there) {
        const t = 0.55;
        const centerX = here.x + (there.x - here.x) * t;
        const centerY = here.y + (there.y - here.y) * t;
        const dx = there.x - here.x;
        const dy = there.y - here.y;
        const len = Math.hypot(dx, dy) || 1;
        const normal = { x: -dy / len, y: dx / len };
        entry = {
          key: `edge:${c.node_id}->${c.target_node_id}`,
          type: 'edge',
          centerX, centerY, normal,
        };
      }
    }
    if (!entry) {
      entry = {
        key: `node:${c.node_id}`,
        type: 'node',
        centerX: here.x,
        centerY: here.y,
      };
    }

    if (!groups.has(entry.key)) groups.set(entry.key, { ...entry, members: [] });
    groups.get(entry.key).members.push(c);
  }

  for (const group of groups.values()) {
    const { centerX, centerY, members, type } = group;
    if (members.length === 1) {
      positions.set(members[0].id, { x: centerX, y: centerY });
      continue;
    }
    if (type === 'edge') {
      const spacing = 18;
      members.forEach((c, i) => {
        const offset = (i - (members.length - 1) / 2) * spacing;
        positions.set(c.id, {
          x: centerX + offset * group.normal.x,
          y: centerY + offset * group.normal.y,
        });
      });
    } else {
      // node cluster: fan around the node center
      const radius = 26;
      members.forEach((c, i) => {
        const angle = (2 * Math.PI * i) / members.length - Math.PI / 2;
        positions.set(c.id, {
          x: centerX + radius * Math.cos(angle),
          y: centerY + radius * Math.sin(angle),
        });
      });
    }
  }
  return positions;
}

// ───────────────────────────────────────────────────────────────
// Map
// ───────────────────────────────────────────────────────────────

export function ScenarioMap({ nodes, snapshot, dimmed }) {
  if (!snapshot) return null;
  const { couriers, orders } = snapshot;

  // For every active assignment, draw a thin link from courier to its target.
  const courierLinks = couriers
    .filter((c) => c.target_node_id && c.eta_remaining > 0)
    .map((c) => ({
      from: nodeById(nodes, c.node_id),
      to:   nodeById(nodes, c.target_node_id),
      courier: c,
    }))
    .filter((l) => l.from && l.to);

  return (
    <svg viewBox="0 0 900 520" className={`map-svg${dimmed ? ' dimmed' : ''}`}>
      {/* subtle dotted background */}
      <defs>
        <pattern id="dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
          <circle cx="2" cy="2" r="1" fill="#2a3142" opacity="0.7" />
        </pattern>
      </defs>
      <rect width="900" height="520" fill="url(#dots)" />

      {/* base edges: lighter lines from hub to every store and store to nearest customers */}
      {nodes.filter((n) => n.kind === 'pickup').map((store) => {
        const hub = nodeById(nodes, 'hub');
        return (
          <line key={`hub-${store.id}`}
            x1={hub.x} y1={hub.y} x2={store.x} y2={store.y}
            stroke="#2c3548" strokeWidth="1" strokeDasharray="3 4" />
        );
      })}

      {/* live courier links (overlay) */}
      {courierLinks.map((l, i) => (
        <line key={`link-${i}`}
          x1={l.from.x} y1={l.from.y} x2={l.to.x} y2={l.to.y}
          stroke="#5DA9F0" strokeOpacity="0.6" strokeWidth="2" strokeDasharray="6 4">
          <animate attributeName="stroke-dashoffset" from="0" to="-20" dur="1s" repeatCount="indefinite" />
        </line>
      ))}

      {/* nodes */}
      {nodes.map((n) => <NodeIcon key={n.id} node={n} orders={orders} />)}

      {/* couriers (rendered last so they sit on top) */}
      {(() => {
        const positions = distributeCouriers(couriers, nodes);
        return couriers.map((c) => {
          const pos = positions.get(c.id) || { x: 0, y: 0 };
          return <CourierIcon key={c.id} courier={c} x={pos.x} y={pos.y} orders={orders} />;
        });
      })()}
    </svg>
  );
}

function NodeIcon({ node, orders }) {
  // Glyph + color per kind
  let glyph = '⭐';
  let fill  = '#7C8DB5';
  if (node.kind === 'pickup') { glyph = '🍔'; fill = '#F0B541'; }
  if (node.kind === 'dropoff') { glyph = '🏠'; fill = '#7CC082'; }
  if (node.kind === 'hub') { glyph = '🏢'; fill = '#7C8DB5'; }

  // For dropoff: highlight if customer has an active order with deadline pressure
  const orderHere = orders?.find((o) => o.dropoff_node_id === node.id && o.status !== 'delivered' && o.status !== 'expired');
  const isUrgent = orderHere && (orderHere.deadline_tick - (orderHere.delivered_tick ?? 0)) < 5;

  return (
    <g className="node-group" transform={`translate(${node.x}, ${node.y})`}>
      <circle r={isUrgent ? 22 : 20} fill={fill} fillOpacity="0.18" stroke={fill} strokeWidth="1.5" />
      <text textAnchor="middle" dy="0.35em" fontSize="20">{glyph}</text>
      <text textAnchor="middle" y="34" fontSize="11" fill="#a4afc7" className="node-label">{node.label}</text>
    </g>
  );
}

function CourierIcon({ courier, x, y, orders }) {
  const carrying = courier.load && orders.find((o) => o.id === courier.load);
  const carryColor = carrying ? STATUS_COLORS.picked : '#5DA9F0';
  const isMoving = courier.status !== 'idle' && courier.target_node_id;

  return (
    <g className={`courier-group${isMoving ? ' moving' : ''}`}
       transform={`translate(${x}, ${y})`}
       style={{ transition: 'transform 0.6s ease-out' }}>
      {/* glow ring when active */}
      {isMoving && <circle r="18" fill={carryColor} fillOpacity="0.2">
        <animate attributeName="r" values="14;20;14" dur="1.5s" repeatCount="indefinite" />
      </circle>}
      <circle r="13" fill={carryColor} stroke="#0f1419" strokeWidth="2" />
      <text textAnchor="middle" dy="0.35em" fontSize="14">🛵</text>
      <text textAnchor="middle" y="-19" fontSize="10" fill="#dde2ed" fontWeight="600">{courier.id.replace('courier_', 'C')}</text>
      {courier.eta_remaining > 0 && (
        <text textAnchor="middle" y="26" fontSize="10" fill="#a4afc7">eta {courier.eta_remaining}</text>
      )}
    </g>
  );
}

// ───────────────────────────────────────────────────────────────
// Orders panel
// ───────────────────────────────────────────────────────────────

export function OrdersPanel({ snapshot, maxTicks }) {
  if (!snapshot) return null;
  const orders = [...snapshot.orders].sort((a, b) => {
    // sort: in-progress first, then by deadline
    const order = { ready: 0, queued: 1, picked: 2, delivered: 3, expired: 4 };
    return (order[a.status] - order[b.status]) || (a.deadline_tick - b.deadline_tick);
  });

  return (
    <div className="panel orders-panel">
      <div className="panel-header">orders<span className="panel-meta">{orders.filter(o => o.status === 'delivered').length}/{orders.length} delivered</span></div>
      <div className="orders-list">
        {orders.map((o) => <OrderCard key={o.id} order={o} tick={snapshot.tick} maxTicks={maxTicks} />)}
      </div>
    </div>
  );
}

function OrderCard({ order, tick, maxTicks }) {
  const ticksLeft = order.deadline_tick - tick;
  const isUrgent = order.status !== 'delivered' && order.status !== 'expired' && ticksLeft <= 3;
  return (
    <div className={`order-card status-${order.status}${isUrgent ? ' urgent' : ''}`}>
      <div className="order-row1">
        <span className="order-id">{order.id}</span>
        <span className={`order-kind kind-${order.kind}`}>{order.kind}</span>
        <span className="order-status">{order.status}</span>
      </div>
      <div className="order-row2">
        <span>{order.pickup_node_id} → {order.dropoff_node_id}</span>
      </div>
      <div className="order-row3">
        {order.status === 'delivered' ? (
          <span className="order-delivered-info">✓ at t{order.delivered_tick}</span>
        ) : order.status === 'expired' ? (
          <span className="order-delivered-info">✗ expired</span>
        ) : (
          <span className={isUrgent ? 'deadline-urgent' : ''}>⏱ {ticksLeft}t left</span>
        )}
        <span className="order-assigned">
          {order.assigned_courier_id ? `→ ${order.assigned_courier_id.replace('courier_', 'C')}` : '—'}
        </span>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// Agent feed
// ───────────────────────────────────────────────────────────────

export function AgentFeed({ trajectory, currentTick, onJumpToTick }) {
  return (
    <div className="panel agent-feed">
      <div className="panel-header">agent feed</div>
      <div className="feed-list">
        {trajectory.slice(0, currentTick + 1).reverse().map((step, idx) => (
          <FeedRow key={step.tick}
                   step={step}
                   isCurrent={step.tick === currentTick}
                   onClick={() => onJumpToTick(step.tick)} />
        ))}
      </div>
    </div>
  );
}

function FeedRow({ step, isCurrent, onClick }) {
  const a = step.action;
  let label, args = '';
  if (!a) { label = '(reset)'; }
  else if (a.type === 'assign') { label = 'assign'; args = `${a.courier_id} ← ${a.order_id}`; }
  else if (a.type === 'reposition') { label = 'reposition'; args = `${a.courier_id} → ${a.node_id}`; }
  else if (a.type === 'hold') { label = 'hold'; }
  else if (a.type === 'prioritize') { label = 'prioritize'; args = a.order_id; }
  else if (a.type === 'view_dashboard') { label = 'view_dashboard'; }
  else if (a.type === 'finish_shift') { label = 'finish_shift'; }
  else { label = a.type || 'unknown'; }

  return (
    <div className={`feed-row${isCurrent ? ' current' : ''}`} onClick={onClick}>
      <span className="feed-tick">t{step.tick}</span>
      <span className="feed-action">{label}</span>
      <span className="feed-args">{args}</span>
      {step.events && step.events.length > 0 && (
        <span className="feed-events">{step.events[0]}</span>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// Reward panel
// ───────────────────────────────────────────────────────────────

const REWARD_KEYS = [
  ['total',     'total reward',  '#5DA9F0'],
  ['success',   'delivered',     '#7CC082'],
  ['on_time',   'on-time bonus', '#7CC082'],
  ['progress',  'progress',      '#5DA9F0'],
  ['step_cost', 'step cost',     '#a4afc7'],
  ['idle',      'idle',          '#a4afc7'],
  ['invalid',   'invalid',       '#E07560'],
  ['late',      'late',          '#E07560'],
  ['churn',     'churn',         '#a4afc7'],
  ['fairness',  'fairness',      '#a4afc7'],
];

export function RewardPanel({ snapshot, cumulative, showRewards = true }) {
  if (!snapshot) return null;
  if (!showRewards) {
    return (
      <div className="panel reward-panel">
        <div className="panel-header">reward</div>
        <div className="panel-empty">
          Reward details stay hidden until you start the replay with step, play, scrub, or reset.
        </div>
      </div>
    );
  }
  const rb = snapshot.reward_breakdown || {};
  // For visualization, compute cumulative for each component (mock has only this-step values).
  // The bar width is proportional to abs(value), and bars are colored by sign.

  const maxAbs = Math.max(0.5, ...REWARD_KEYS.map(([k]) => Math.abs(rb[k] || 0)));

  return (
    <div className="panel reward-panel">
      <div className="panel-header">reward
        <span className="panel-meta cumulative-reward">total: {cumulative >= 0 ? '+' : ''}{cumulative.toFixed(2)}</span>
      </div>
      <div className="reward-rows">
        {REWARD_KEYS.map(([key, label, color]) => {
          const val = rb[key] ?? 0;
          const widthPct = (Math.abs(val) / maxAbs) * 100;
          const isNeg = val < 0;
          return (
            <div className="reward-row" key={key}>
              <span className="reward-label">{label}</span>
              <span className="reward-bar-track">
                <span className={`reward-bar ${isNeg ? 'neg' : 'pos'}`}
                      style={{ width: `${widthPct}%`, background: color, opacity: val === 0 ? 0.15 : 1 }} />
              </span>
              <span className={`reward-val ${isNeg ? 'neg' : ''}`}>
                {val > 0 ? '+' : ''}{val.toFixed(2)}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// Top control bar
// ───────────────────────────────────────────────────────────────

export function ControlBar({
  scenarios,
  scenarioId,
  onScenarioChange,
  modes,
  modeId,
  onModeChange,
  isPlaying,
  onPlayToggle,
  onStepBack,
  onStepForward,
  onReset,
}) {
  return (
    <div className="control-bar">
      <div className="control-bar-left">
        <span className="brand">Dispatch Arena</span>
        <select className="ctrl-select" value={scenarioId} onChange={(e) => onScenarioChange(e.target.value)}>
          {Object.entries(scenarios).map(([id, s]) => (
            <option key={id} value={id}>
              {s.metadata.theme} ({s.metadata.difficulty})
            </option>
          ))}
        </select>
        <select className="ctrl-select" value={modeId} onChange={(e) => onModeChange(e.target.value)}>
          {modes.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
        </select>
      </div>
      <div className="control-bar-right">
        <button className="ctrl-btn" onClick={onStepBack}>⏮</button>
        <button className="ctrl-btn primary" onClick={onPlayToggle}>{isPlaying ? '⏸' : '▶'}</button>
        <button className="ctrl-btn" onClick={onStepForward}>⏭</button>
        <button className="ctrl-btn" onClick={onReset}>⟲</button>
      </div>
    </div>
  );
}

function formatActionLabel(action) {
  if (!action) return 'Waiting for the shift to begin';
  const type = action.type || action.action_type || 'unknown';
  if (type === 'assign') {
    return `Assign ${shortCourier(action.courier_id)} to ${shortOrder(action.order_id)}`;
  }
  if (type === 'reposition') {
    return `Reposition ${shortCourier(action.courier_id)} to ${action.node_id}`;
  }
  if (type === 'prioritize') {
    return action.order_id ? `Prioritize ${shortOrder(action.order_id)}` : 'Prioritize the backlog';
  }
  if (type === 'hold') return 'Hold and wait for more information';
  if (type === 'finish_shift') return 'Finish the shift';
  if (type === 'go_pickup') return 'Head to pickup';
  if (type === 'pickup') return 'Pick up the order';
  if (type === 'go_dropoff') return 'Head to dropoff';
  if (type === 'dropoff') return 'Deliver the order';
  return type.replace(/_/g, ' ');
}

function shortCourier(courierId) {
  return courierId ? courierId.replace('courier_', 'C') : 'a courier';
}

function shortOrder(orderId) {
  return orderId ? orderId.replace('order_', 'O') : 'an order';
}

export function ActionBanner({ trajectory, currentTick, playbackPhase, isPlaying }) {
  const currentStep = trajectory[currentTick];
  const nextStep = currentTick < trajectory.length - 1 ? trajectory[currentTick + 1] : null;

  let status = 'Ready';
  let actionText = currentStep?.action ? formatActionLabel(currentStep.action) : 'Choose play to start the replay';
  let subtext = currentStep?.events?.[0] || 'Manual stepping is available below.';

  if (currentTick >= trajectory.length - 1) {
    status = 'Shift complete';
    actionText = currentStep?.events?.[0] || 'No more actions left in this replay';
    subtext = `Final cumulative reward: ${(currentStep?.cumulative_reward ?? 0).toFixed(2)}`;
  } else if (isPlaying && playbackPhase === 'thinking') {
    status = 'Agent is thinking';
    actionText = nextStep?.action ? formatActionLabel(nextStep.action) : 'Waiting for the next move';
    subtext = 'Reading the current state and planning the next action...';
  } else if (isPlaying && playbackPhase === 'acting') {
    status = 'Action taken';
    actionText = nextStep?.action ? formatActionLabel(nextStep.action) : 'Waiting for the next move';
    subtext = nextStep?.events?.[0] || 'Applying the action to the simulator.';
  } else if (currentStep?.action) {
    status = 'Last action';
  } else if (nextStep?.action) {
    status = 'Next action';
    actionText = formatActionLabel(nextStep.action);
    subtext = nextStep?.events?.[0] || 'Use play to watch the next step.';
  }

  return (
    <div className={`action-banner phase-${playbackPhase}`}>
      <div className="action-banner-status">{status}</div>
      <div className="action-banner-line">{actionText}</div>
      <div className="action-banner-subtext">{subtext}</div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// Tick scrubber (timeline at the bottom)
// ───────────────────────────────────────────────────────────────

export function TickScrubber({ trajectory, currentTick, onTickChange, maxTicks }) {
  const totalLen = trajectory.length;
  return (
    <div className="tick-scrubber">
      <div className="tick-meta">
        <span>tick {currentTick} / {maxTicks}</span>
        <span className="tick-events">{trajectory[currentTick]?.events?.[0] || ''}</span>
      </div>
      <input type="range"
             min={0}
             max={Math.max(0, totalLen - 1)}
             value={currentTick}
             onChange={(e) => onTickChange(parseInt(e.target.value, 10))}
             className="tick-slider" />
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// Final summary card (shown at end of episode)
// ───────────────────────────────────────────────────────────────

export function FinalCard({ trajectory, scenario }) {
  const last = trajectory[trajectory.length - 1];
  const orders = last.orders;
  const delivered = orders.filter((o) => o.status === 'delivered').length;
  const onTime = orders.filter((o) => o.status === 'delivered' && (o.delivered_tick ?? 99) <= o.deadline_tick).length;
  const expired = orders.filter((o) => o.status === 'expired').length;
  const successRate = (delivered / orders.length) * 100;

  return (
    <div className="final-card">
      <div className="final-header">shift complete · {scenario.metadata.theme}</div>
      <div className="final-stats">
        <Stat label="delivered" value={`${delivered}/${orders.length}`} accent={successRate >= 80 ? 'good' : successRate >= 50 ? 'mid' : 'bad'} />
        <Stat label="on time" value={`${onTime}/${delivered}`} />
        <Stat label="expired" value={expired} accent={expired > 0 ? 'bad' : 'good'} />
        <Stat label="reward" value={(last.cumulative_reward ?? 0).toFixed(1)} accent={last.cumulative_reward > 0 ? 'good' : 'bad'} />
        <Stat label="ticks" value={`${last.tick}/${scenario.metadata.max_ticks}`} />
      </div>
    </div>
  );
}

function Stat({ label, value, accent }) {
  return (
    <div className={`stat${accent ? ' accent-' + accent : ''}`}>
      <div className="stat-value">{value}</div>
      <div className="stat-label">{label}</div>
    </div>
  );
}
