// scene.jsx — isometric 3D scene: house, PV, battery, generator, tree, animated energy flows.

const { useMemo, useEffect, useRef, useState } = React;

// ── Color tokens (dark theme, with face shading) ─────────────────────────────
const C = {
  paper:   'oklch(0.18 0.015 250)',
  paper2:  'oklch(0.22 0.018 250)',
  paper3:  'oklch(0.30 0.018 250)',   // top face
  paper4:  'oklch(0.26 0.018 250)',   // right face (lit side)
  paper5:  'oklch(0.20 0.016 250)',   // left face (shadow side)
  ink:     'oklch(0.97 0.005 85)',
  ink2:    'oklch(0.78 0.01 250)',
  ink3:    'oklch(0.58 0.01 250)',
  line:    'oklch(0.42 0.018 250)',
  line2:   'oklch(0.34 0.016 250)',
  // Roof
  roof:    'oklch(0.40 0.06 30)',
  roofL:   'oklch(0.34 0.06 30)',
  roofD:   'oklch(0.28 0.05 30)',
  // Solar
  solar:    'oklch(0.82 0.16 80)',
  solarTop: 'oklch(0.32 0.10 250)',
  solarSide:'oklch(0.24 0.06 250)',
  // Battery
  battery:  'oklch(0.78 0.16 155)',
  // Generator
  gen:      'oklch(0.74 0.16 35)',
  genBody:  'oklch(0.32 0.04 30)',
  genBodyL: 'oklch(0.36 0.05 30)',
  genBodyD: 'oklch(0.26 0.03 30)',
  // Warn / tree
  warn:     'oklch(0.78 0.17 55)',
  treeLeaf: 'oklch(0.55 0.10 145)',
  treeLeaf2:'oklch(0.62 0.11 140)',
  treeLeafD:'oklch(0.42 0.08 145)',
  treeTrunk:'oklch(0.32 0.04 50)',
  // Ground
  ground:   'oklch(0.24 0.018 250)',
  groundL:  'oklch(0.28 0.020 250)',
};

const SW = 2.5;
const SW_THIN = 1.5;

// ── Iso projection helpers ───────────────────────────────────────────────────
// We use cabinet-ish projection: x' = x + dx*0.55, y' = y - dx*0.32 (depth direction)
// "back-right" displacement vector for depth.
const ISO_DX = 0.6;   // horizontal component of depth axis
const ISO_DY = -0.34; // vertical component of depth axis (negative = up)

function isoBack(x, y, depth) {
  return [x + depth * ISO_DX, y + depth * ISO_DY];
}

// Helper: build a 3D box (no top variability, just rectangular extruded)
// Returns three polygon point strings: front, right (back-right side), top.
function boxFaces(x, y, w, h, depth) {
  // Front face: (x,y) (x+w,y) (x+w,y+h) (x,y+h)
  const front = [[x,y],[x+w,y],[x+w,y+h],[x,y+h]];
  const [bx, by] = [x + depth*ISO_DX, y + depth*ISO_DY];
  // Right (depth) face: front-right-top → back-right-top → back-right-bottom → front-right-bottom
  const right = [[x+w, y], [x+w + depth*ISO_DX, y + depth*ISO_DY], [x+w + depth*ISO_DX, y+h + depth*ISO_DY], [x+w, y+h]];
  // Top face: front-left-top → front-right-top → back-right-top → back-left-top
  const top = [[x, y], [x+w, y], [x+w + depth*ISO_DX, y + depth*ISO_DY], [x + depth*ISO_DX, y + depth*ISO_DY]];
  return {
    front: front.map(p => p.join(',')).join(' '),
    right: right.map(p => p.join(',')).join(' '),
    top:   top.map(p => p.join(',')).join(' '),
  };
}

// Build rounded-corner 3D box paths: returns SVG path strings for top, right, front faces.
// All faces share the same corner radius. Front is a rounded rect; top and right are
// quads whose front-facing edge follows the rounded corner of the front face.
function roundedBoxFaces(x, y, w, h, depth, r = 12) {
  const dx = depth * ISO_DX;
  const dy = depth * ISO_DY;

  // Front: rounded rect
  const front =
    `M ${x + r},${y} ` +
    `L ${x + w - r},${y} ` +
    `Q ${x + w},${y} ${x + w},${y + r} ` +
    `L ${x + w},${y + h - r} ` +
    `Q ${x + w},${y + h} ${x + w - r},${y + h} ` +
    `L ${x + r},${y + h} ` +
    `Q ${x},${y + h} ${x},${y + h - r} ` +
    `L ${x},${y + r} ` +
    `Q ${x},${y} ${x + r},${y} Z`;

  // Top: starts at front-top-left rounded inset, traces back to back-left, back-right,
  // front-top-right rounded inset, then arcs along the front-top edge.
  const top =
    `M ${x},${y + r} ` +
    `Q ${x},${y} ${x + r},${y} ` +
    `L ${x + w - r},${y} ` +
    `Q ${x + w},${y} ${x + w},${y + r} ` +
    `L ${x + w + dx},${y + r + dy} ` +
    `L ${x + dx},${y + r + dy} Z`;

  // Right: front-right rounded inset down → back-right-top → back-right-bottom → front-bottom-right inset → arcs.
  const right =
    `M ${x + w},${y + r} ` +
    `L ${x + w + dx},${y + r + dy} ` +
    `L ${x + w + dx},${y + h - r + dy} ` +
    `L ${x + w},${y + h - r} ` +
    `Q ${x + w},${y + h} ${x + w - r},${y + h} ` +  // not used along bottom path; we skip
    `Z`;

  // Cleaner right: omit the bottom-front arc — the right face's bottom-front corner
  // is the same point ${x+w},${y+h-r} → straight segment back to ${x+w},${y+r}, which
  // is implicit by Z. The Q above creates a stray sliver. Replace with explicit close:
  const rightFinal =
    `M ${x + w},${y + r} ` +
    `L ${x + w + dx},${y + r + dy} ` +
    `L ${x + w + dx},${y + h - r + dy} ` +
    `L ${x + w},${y + h - r} Z`;

  return { front, top, right: rightFinal };
}

// ── Anchors (trunk routing) ──────────────────────────────────────────────────
const TRUNK_Y = 615;
const ANCHORS = {
  pvOut:        { x: 580, y: 360 },
  pvTrunk:      { x: 580, y: TRUNK_Y },
  batteryIn:    { x: 760, y: 580 },
  batteryTrunk: { x: 760, y: TRUNK_Y },
  genOut:       { x: 1035, y: 565 },
  genTrunk:     { x: 1035, y: TRUNK_Y },
  houseIn:      { x: 280, y: 560 },
  houseTrunk:   { x: 280, y: TRUNK_Y },
  inverter:     { x: 460, y: 555 },
  inverterTrunk:{ x: 460, y: TRUNK_Y },
};

function trunkPath(src, dst) {
  return `M${src.x},${src.y} L${src.x},${TRUNK_Y} L${dst.x},${TRUNK_Y} L${dst.x},${dst.y}`;
}

function buildFlows(state) {
  const flows = [];
  if (state.pvToHouse > 0.05)
    flows.push({ id: 'pv-house', d: trunkPath(ANCHORS.pvOut, ANCHORS.houseIn), color: C.solar, intensity: state.pvToHouse });
  if (state.pvToBattery > 0.05)
    flows.push({ id: 'pv-battery', d: trunkPath(ANCHORS.pvOut, ANCHORS.batteryIn), color: C.solar, intensity: state.pvToBattery });
  if (state.batteryToHouse > 0.05)
    flows.push({ id: 'battery-house', d: trunkPath(ANCHORS.batteryIn, ANCHORS.houseIn), color: C.battery, intensity: state.batteryToHouse });
  if (state.genToBattery > 0.05)
    flows.push({ id: 'gen-battery', d: trunkPath(ANCHORS.genOut, ANCHORS.batteryIn), color: C.gen, intensity: state.genToBattery });
  if (state.genToHouse > 0.05)
    flows.push({ id: 'gen-house', d: trunkPath(ANCHORS.genOut, ANCHORS.houseIn), color: C.gen, intensity: state.genToHouse });
  return flows;
}

// ── Animated dot stream ──────────────────────────────────────────────────────
function FlowPath({ flow, speedMult }) {
  const pathRef = useRef(null);
  const [length, setLength] = useState(0);
  useEffect(() => { if (pathRef.current) setLength(pathRef.current.getTotalLength()); }, [flow.d]);
  // Quantize intensity to a 4-step bucket so tiny fluctuations don't restart the dot stream.
  // Without this, every render of intensity=0.31→0.32→0.33 would swap the count/duration
  // and re-mount <animateMotion> elements out of phase, causing visible "jitter" especially
  // when PV output drops as clouds roll in.
  const bucket = Math.round(Math.min(1, Math.max(0, flow.intensity)) * 4);
  const count = Math.max(2, Math.min(7, bucket + 2));
  const speedKey = Math.round(speedMult * 4) / 4;
  const duration = Math.max(1.4, 5.0 - bucket * 0.6) / Math.max(0.1, speedKey);
  const dots = useMemo(() => Array.from({ length: count }, (_, i) => i), [count]);
  return (
    <g>
      <path ref={pathRef} d={flow.d} fill="none" stroke={flow.color}
        strokeOpacity="0.55" strokeWidth={4} strokeLinecap="round" strokeLinejoin="round" />
      {length > 0 && dots.map((i) => (
        <circle key={i} r="5.5" fill={flow.color}
          style={{ filter: `drop-shadow(0 0 8px ${flow.color})` }}>
          <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={flow.d}
            begin={`-${(duration * i) / count}s`} rotate="auto" />
          <animate attributeName="opacity" values="0;1;1;1;0" keyTimes="0;0.05;0.5;0.95;1"
            dur={`${duration}s`} repeatCount="indefinite" begin={`-${(duration * i) / count}s`} />
        </circle>
      ))}
    </g>
  );
}

// ── Sun / Moon / Stars ───────────────────────────────────────────────────────
function Celestial({ hour, weather, scenario }) {
  const isDay = hour >= 6 && hour <= 20;
  const t = (hour - 6) / 14;
  const cx = 200 + t * 800;
  const cy = 170 - Math.sin(t * Math.PI) * 110;
  if (!isDay) {
    return (
      <g>
        {[[140, 80], [260, 110], [420, 70], [700, 95], [880, 60], [1080, 130], [340, 140], [580, 70]].map(([x, y], i) => (
          <circle key={i} cx={x} cy={y} r={i % 3 === 0 ? 2 : 1.3}
            fill={C.ink} opacity={0.5 + (i % 3) * 0.15}>
            <animate attributeName="opacity" values="0.3;0.9;0.3" dur={`${2 + (i % 3)}s`} repeatCount="indefinite" begin={`-${i * 0.3}s`} />
          </circle>
        ))}
        <g transform={`translate(${980},${110})`}>
          <circle r="36" fill={C.solar} opacity="0.08" />
          <circle r="26" fill="oklch(0.94 0.03 80)" />
          <circle r="26" cx="9" fill={C.paper} />
        </g>
      </g>
    );
  }

  // Auto-mode rolling cloud: starts drifting in at 12:45, fully covers sun by 13:45, stays.
  // Coverage 0..1 mapped to cloud x-offset (slides in from the right) and sun dimming.
  let autoCover = 0;
  if (scenario === 'auto') {
    if (hour >= 12.75 && hour < 13.75) autoCover = (hour - 12.75) / 1.0;
    else if (hour >= 13.75) autoCover = 1;
  }
  const showCloud = autoCover > 0.01;
  const cloudOffsetX = (1 - autoCover) * 220;   // slides in from +220 → 0
  const sunDim = 1 - autoCover * 0.85;          // sun dims as cover grows

  return (
    <g transform={`translate(${cx},${cy})`}>
      {weather === 'clear' && (
        <g style={{ opacity: sunDim, transition: 'opacity 400ms' }}>
          <circle r="44" fill={C.solar} opacity="0.10" />
          <circle r="32" fill={C.solar} opacity="0.20" />
          <circle r="22" fill={C.solar} />
        </g>
      )}
      {weather === 'cloudy' && (
        <g>
          <circle r="22" fill={C.solar} opacity="0.45" />
          <g transform="translate(-6,4)">
            <ellipse cx="0" cy="0" rx="40" ry="14" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
            <ellipse cx="-16" cy="-6" rx="18" ry="12" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
            <ellipse cx="18" cy="-4" rx="18" ry="11" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
          </g>
        </g>
      )}
      {weather === 'overcast' && (
        <g>
          <ellipse cx="0" cy="0" rx="56" ry="18" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
          <ellipse cx="-26" cy="-8" rx="22" ry="14" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
          <ellipse cx="26" cy="-6" rx="22" ry="13" fill={C.paper3} stroke={C.line} strokeWidth={SW_THIN} />
        </g>
      )}

      {/* Auto-scenario rolling cloud — drifts in from the right and parks over the sun */}
      {showCloud && (
        <g transform={`translate(${cloudOffsetX}, 2)`} style={{ transition: 'transform 600ms ease-out' }}>
          {/* soft outer glow / shadow under cloud */}
          <ellipse cx="6" cy="14" rx="62" ry="8" fill="#000" opacity="0.20" />
          {/* cloud puffs — paper-toned, layered */}
          <ellipse cx="-30" cy="2"  rx="22" ry="14" fill={C.paper4} stroke={C.ink} strokeWidth={SW_THIN} />
          <ellipse cx="0"   cy="-6" rx="28" ry="18" fill={C.paper3} stroke={C.ink} strokeWidth={SW_THIN} />
          <ellipse cx="28"  cy="0"  rx="24" ry="15" fill={C.paper4} stroke={C.ink} strokeWidth={SW_THIN} />
          <ellipse cx="-8"  cy="6"  rx="40" ry="10" fill={C.paper3} stroke={C.ink} strokeWidth={SW_THIN} />
          {/* highlight rim catching light */}
          <ellipse cx="-4"  cy="-12" rx="18" ry="6" fill={C.paper2} opacity="0.55" />
        </g>
      )}
    </g>
  );
}

// ── Tree (proper tree with branches and irregular foliage) ──────────────────
function Tree({ x, y, scale = 1 }) {
  return (
    <g transform={`translate(${x},${y}) scale(${scale})`}>
      <ellipse cx="2" cy="4" rx="48" ry="8" fill="#000" opacity="0.35" />
      {/* trunk — slightly tapered with subtle bark */}
      <path d="M -7,4 Q -8,-30 -5,-60 L 5,-60 Q 8,-30 7,4 Z"
        fill={C.treeTrunk} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <path d="M 5,-60 Q 8,-30 7,4 L 11,4 Q 12,-25 9,-58 Z"
        fill="oklch(0.24 0.04 50)" stroke={C.ink} strokeWidth={SW_THIN} strokeLinejoin="round" />
      {/* branches peeking out */}
      <path d="M -2,-50 Q -18,-58 -28,-72" fill="none" stroke={C.treeTrunk} strokeWidth={SW + 1} strokeLinecap="round" />
      <path d="M 4,-58 Q 16,-65 26,-78" fill="none" stroke={C.treeTrunk} strokeWidth={SW + 1} strokeLinecap="round" />
      <path d="M 0,-66 Q 0,-80 -4,-92" fill="none" stroke={C.treeTrunk} strokeWidth={SW + 1} strokeLinecap="round" />
      {/* foliage — irregular silhouette via blob path */}
      <path d="
        M -36,-70 Q -48,-86 -38,-100 Q -42,-118 -22,-122
        Q -10,-138 6,-130 Q 24,-138 32,-122
        Q 50,-118 44,-100 Q 54,-86 40,-72
        Q 48,-56 28,-54 Q 16,-44 0,-50
        Q -16,-44 -28,-54 Q -46,-58 -36,-70 Z"
        fill={C.treeLeaf} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* darker shadow on left side of canopy */}
      <path d="
        M -36,-70 Q -48,-86 -38,-100 Q -42,-118 -22,-122
        Q -16,-110 -22,-94 Q -32,-82 -28,-70 Q -36,-62 -36,-70 Z"
        fill={C.treeLeafD} opacity="0.7" />
      {/* lighter highlights on right/top */}
      <ellipse cx="14" cy="-118" rx="10" ry="5" fill={C.treeLeaf2} opacity="0.85" />
      <ellipse cx="-6" cy="-126" rx="8" ry="4" fill="oklch(0.74 0.12 140)" opacity="0.55" />
      <ellipse cx="32" cy="-100" rx="6" ry="4" fill={C.treeLeaf2} opacity="0.7" />
      {/* a couple dotted leaves for texture */}
      <circle cx="-24" cy="-112" r="3" fill={C.treeLeaf2} opacity="0.6" />
      <circle cx="22" cy="-86" r="2.5" fill={C.treeLeaf2} opacity="0.6" />
      <circle cx="-12" cy="-78" r="2.5" fill={C.treeLeafD} opacity="0.6" />
    </g>
  );
}

// ── Bush ─────────────────────────────────────────────────────────────────────
function Bush({ x, y }) {
  // Tuft cluster — short blades. Each tuft is 5–7 short strokes fanning out.
  const tuft = (cx, cy, scale = 1, rot = 0) => {
    const blades = [
      { x1: 0, y1: 0, x2: -5, y2: -10 },
      { x1: 0, y1: 0, x2: -2, y2: -13 },
      { x1: 0, y1: 0, x2: 1, y2: -14 },
      { x1: 0, y1: 0, x2: 4, y2: -12 },
      { x1: 0, y1: 0, x2: 6, y2: -9 },
    ];
    return (
      <g transform={`translate(${cx},${cy}) rotate(${rot}) scale(${scale})`}>
        {blades.map((b, i) => (
          <line key={i} x1={b.x1} y1={b.y1} x2={b.x2} y2={b.y2}
            stroke={i % 2 === 0 ? C.treeLeafD : C.treeLeaf}
            strokeWidth={SW * 0.8} strokeLinecap="round" />
        ))}
      </g>
    );
  };
  return (
    <g transform={`translate(${x},${y})`}>
      {tuft(-22, 4, 1.0, -8)}
      {tuft(-8, 6, 1.15, 4)}
      {tuft(8, 5, 0.95, -6)}
      {tuft(22, 6, 1.1, 6)}
      {tuft(0, 9, 0.85, 0)}
      {tuft(-32, 8, 0.7, -10)}
      {tuft(32, 8, 0.75, 8)}
    </g>
  );
}

// ── House (isometric: gable end, body, roof, depth) ──────────────────────────
function House({ lit, type }) {
  if (type === 'commercial') {
    // Iso building: rectangular box with flat roof and depth
    const x = 110, y = 410, w = 280, h = 170, d = 70;
    const f = boxFaces(x, y, w, h, d);
    return (
      <g>
        <ellipse cx={x + w/2 + d*ISO_DX/2} cy={y + h + 6} rx={w/2 + 14} ry="8" fill="#000" opacity="0.30" />
        {/* right side (depth face) */}
        <polygon points={f.right} fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
        {/* top */}
        <polygon points={f.top} fill={C.paper3} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
        {/* front */}
        <polygon points={f.front} fill={C.paper2} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
        {/* Top band */}
        <rect x={x} y={y} width={w} height="22" fill={C.paper3} />
        <line x1={x} y1={y + 22} x2={x + w} y2={y + 22} stroke={C.ink} strokeWidth={SW_THIN} />
        {/* Windows on front */}
        {Array.from({ length: 8 }).map((_, i) => {
          const col = i % 4, row = Math.floor(i / 4);
          return (
            <rect key={i}
              x={x + 22 + col * 64} y={y + 50 + row * 56}
              width="44" height="34" rx="5"
              fill={lit ? C.solar : 'oklch(0.40 0.04 250)'}
              stroke={C.ink} strokeWidth={SW_THIN}
              style={{ transition: 'fill 400ms' }} />
          );
        })}
        {/* Side windows on right face — projected */}
        {[0,1,2].map(i => {
          const sx = x + w + i * 18 * ISO_DX;
          const sy = y + 60 + i * 18 * ISO_DY;
          // skewed rect → polygon
          const winW = 14, winH = 30;
          const pts = [
            [sx, sy],
            [sx + winW * ISO_DX, sy + winW * ISO_DY],
            [sx + winW * ISO_DX, sy + winW * ISO_DY + winH],
            [sx, sy + winH],
          ].map(p => p.join(',')).join(' ');
          return <polygon key={i} points={pts}
            fill={lit ? C.solar : 'oklch(0.36 0.04 250)'} opacity="0.85"
            stroke={C.ink} strokeWidth={SW_THIN} style={{ transition: 'fill 400ms' }} />;
        })}
      </g>
    );
  }

  // Cozy single-family house — gable roof, rounded soft forms, friendly cottage feel
  const fmt = (a) => a.map(p => p.join(',')).join(' ');

  // Body (front-facing wall + depth)
  const x = 130, y = 450, w = 220, h = 110, d = 80;
  const apexX = x + w / 2;
  const apexY = y - 64;

  const flb = [x, y + h];
  const frb = [x + w, y + h];
  const flt = [x, y];
  const frt = [x + w, y];
  const brb = isoBack(x + w, y + h, d);
  const brt = isoBack(x + w, y, d);
  const blt = isoBack(x, y, d);
  const [apexBx, apexBy] = isoBack(apexX, apexY, d);

  // Eaves overhang (small extension on front)
  const eaveOver = 6;

  return (
    <g>
      {/* Soft ground shadow */}
      <ellipse cx={x + w/2 + d*ISO_DX/2} cy={y + h + 8} rx={w/2 + 28} ry="11" fill="#000" opacity="0.30" />

      {/* Right side wall (depth face) */}
      <polygon points={fmt([frb, brb, brt, frt])}
        fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* Front wall — soft cream with rounded eaves corners hinted */}
      <polygon points={fmt([flb, frb, frt, flt])}
        fill={C.paper2} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* Front gable triangle */}
      <polygon points={fmt([[x - eaveOver, y], [x + w + eaveOver, y], [apexX, apexY]])}
        fill={C.paper3} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* Right roof slope (visible ridge) */}
      <polygon points={fmt([[apexX, apexY], [x + w + eaveOver, y], [x + w + eaveOver + d*ISO_DX, y + d*ISO_DY], [apexBx, apexBy]])}
        fill={C.roof} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* Left/back roof slope */}
      <polygon points={fmt([[apexX, apexY], [apexBx, apexBy], [x - eaveOver + d*ISO_DX, y + d*ISO_DY], [x - eaveOver, y]])}
        fill={C.roofL} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* Roof shingle texture — horizontal lines on right slope */}
      {[0.25, 0.55, 0.85].map((t, i) => {
        const ax = apexX + (x + w + eaveOver - apexX) * t;
        const ay = apexY + (y - apexY) * t;
        const [bx, by] = isoBack(ax, ay, d);
        return <line key={i} x1={ax} y1={ay} x2={bx} y2={by} stroke={C.roofD} strokeWidth="1" opacity="0.4" />;
      })}

      {/* Decorative gable trim (bargeboard) — subtle scallop hint */}
      <path d={`M ${x - eaveOver},${y} L ${apexX},${apexY} L ${x + w + eaveOver},${y}`}
        fill="none" stroke={C.roofD} strokeWidth="1.5" opacity="0.7" />

      {/* Round attic window in gable */}
      <circle cx={apexX} cy={apexY + 28} r="11"
        fill={lit ? C.solar : 'oklch(0.40 0.04 250)'}
        stroke={C.ink} strokeWidth={SW_THIN}
        style={{ transition: 'fill 400ms' }} />
      <line x1={apexX - 11} y1={apexY + 28} x2={apexX + 11} y2={apexY + 28} stroke={C.ink} strokeWidth="1" opacity="0.5" />
      <line x1={apexX} y1={apexY + 17} x2={apexX} y2={apexY + 39} stroke={C.ink} strokeWidth="1" opacity="0.5" />

      {/* Door — rounded arch, warm wood tone */}
      <path d={`M ${apexX - 18},${y + h} L ${apexX - 18},${y + h - 56}
                Q ${apexX - 18},${y + h - 76} ${apexX},${y + h - 76}
                Q ${apexX + 18},${y + h - 76} ${apexX + 18},${y + h - 56}
                L ${apexX + 18},${y + h} Z`}
        fill="oklch(0.42 0.08 50)" stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <circle cx={apexX + 11} cy={y + h - 32} r="2.2" fill={C.solar} />
      {/* Step */}
      <rect x={apexX - 24} y={y + h} width="48" height="6" rx="3" fill={C.paper4} stroke={C.ink} strokeWidth={SW_THIN} />

      {/* Two cozy windows — rounded squares with cross mullions and shutters */}
      {[
        { wx: x + 22, wy: y + 24 },
        { wx: x + w - 22 - 44, wy: y + 24 },
      ].map((win, i) => (
        <g key={i}>
          {/* shutter left */}
          <rect x={win.wx - 8} y={win.wy - 1} width="6" height="46" rx="2"
            fill="oklch(0.42 0.10 150)" stroke={C.ink} strokeWidth="1" />
          {/* shutter right */}
          <rect x={win.wx + 44 + 2} y={win.wy - 1} width="6" height="46" rx="2"
            fill="oklch(0.42 0.10 150)" stroke={C.ink} strokeWidth="1" />
          {/* window pane */}
          <rect x={win.wx} y={win.wy} width="44" height="44" rx="6"
            fill={lit ? C.solar : 'oklch(0.40 0.04 250)'}
            stroke={C.ink} strokeWidth={SW_THIN}
            style={{ transition: 'fill 400ms' }} />
          <line x1={win.wx + 22} y1={win.wy + 4} x2={win.wx + 22} y2={win.wy + 40} stroke={C.ink} strokeWidth="1" opacity="0.55" />
          <line x1={win.wx + 4} y1={win.wy + 22} x2={win.wx + 40} y2={win.wy + 22} stroke={C.ink} strokeWidth="1" opacity="0.55" />
          {/* window sill */}
          <rect x={win.wx - 4} y={win.wy + 44} width="52" height="3" rx="1.5" fill={C.paper5} stroke={C.ink} strokeWidth="1" />
        </g>
      ))}

      {/* Side window on right face */}
      {(() => {
        const sx = x + w + 12 * ISO_DX, sy = y + 30 + 12 * ISO_DY;
        const winW = 26, winH = 36;
        const pts = [
          [sx, sy],
          [sx + winW * ISO_DX, sy + winW * ISO_DY],
          [sx + winW * ISO_DX, sy + winW * ISO_DY + winH],
          [sx, sy + winH],
        ].map(p => p.join(',')).join(' ');
        return <polygon points={pts}
          fill={lit ? C.solar : 'oklch(0.36 0.04 250)'} opacity="0.9"
          stroke={C.ink} strokeWidth={SW_THIN} style={{ transition: 'fill 400ms' }} />;
      })()}

      {/* Brick chimney with rounded cap on right roof slope */}
      {(() => {
        const cx2 = x + w - 40, cy2 = y - 38;
        return (
          <g>
            <rect x={cx2} y={cy2} width="14" height="32" rx="2"
              fill="oklch(0.46 0.08 30)" stroke={C.ink} strokeWidth={SW_THIN} />
            <rect x={cx2 - 3} y={cy2 - 4} width="20" height="6" rx="2"
              fill={C.paper5} stroke={C.ink} strokeWidth={SW_THIN} />
          </g>
        );
      })()}
    </g>
  );
}

// ── PV array (3D tilted panel on a frame) ────────────────────────────────────
function PVArray({ output, panels }) {
  // Panel sits as a tilted parallelogram (cabinet projection) on stand legs.
  const cx = 580;          // center anchor x of the trunk riser
  const baseY = 415;       // base ground plane Y
  const cellsW = panels === 'large' ? 4 : 3;
  const panelW = cellsW * 50;   // width across surface
  const panelH = 60;            // along the tilt
  const intensity = Math.min(1, output / 8);

  // We'll tilt the panel forward: front-bottom near viewer, back-top up & away.
  // Front-bottom-left (FBL), front-bottom-right (FBR) — these are the "ground-side" edge.
  const FBL = [cx - panelW / 2, baseY - 14];
  const FBR = [cx + panelW / 2, baseY - 14];
  // Top edge offset: up & back-right
  const TBL = [cx - panelW / 2 + panelH * 0.45, baseY - 14 - panelH * 0.85];
  const TBR = [cx + panelW / 2 + panelH * 0.45, baseY - 14 - panelH * 0.85];

  // Stand: two posts from front-bottom-left/right going down a bit
  const fmt = (pts) => pts.map(p => p.join(',')).join(' ');
  const cellFill = intensity > 0.05
    ? `oklch(${0.34 + intensity * 0.20} ${0.10 + intensity * 0.06} 250)`
    : C.solarTop;

  // Cell rectangles on the panel surface — use parallelogram divisions
  const cells = [];
  for (let i = 0; i < cellsW; i++) {
    const t0 = i / cellsW, t1 = (i + 1) / cellsW;
    const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
    const fbl = lerp(FBL, FBR, t0);
    const fbr = lerp(FBL, FBR, t1);
    const tbl = lerp(TBL, TBR, t0);
    const tbr = lerp(TBL, TBR, t1);
    // shrink each cell slightly inward
    const inset = 0.05;
    const lerpPair = (a, b, t) => lerp(a, b, t);
    const fblI = lerpPair(fbl, fbr, inset);
    const fbrI = lerpPair(fbr, fbl, inset);
    const tblI = lerpPair(tbl, tbr, inset);
    const tbrI = lerpPair(tbr, tbl, inset);
    cells.push(fmt([fblI, fbrI, tbrI, tblI]));
  }

  return (
    <g>
      {/* shadow */}
      <ellipse cx={cx + panelH * 0.22} cy={baseY + 4} rx={panelW/2 + 12} ry="7" fill="#000" opacity="0.30" />
      {/* legs */}
      <polygon points={fmt([
        [FBL[0] + 10, FBL[1]], [FBL[0] + 16, FBL[1]],
        [FBL[0] + 16, baseY + 8], [FBL[0] + 10, baseY + 8]
      ])} fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <polygon points={fmt([
        [FBR[0] - 16, FBR[1]], [FBR[0] - 10, FBR[1]],
        [FBR[0] - 10, baseY + 8], [FBR[0] - 16, baseY + 8]
      ])} fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* back support strut behind panel */}
      <line x1={cx} y1={baseY + 4} x2={cx + panelH * 0.45 * 0.5} y2={baseY - 14 - panelH * 0.42}
        stroke={C.paper4} strokeWidth={SW + 1} strokeLinecap="round" />
      <line x1={cx} y1={baseY + 4} x2={cx + panelH * 0.45 * 0.5} y2={baseY - 14 - panelH * 0.42}
        stroke={C.ink} strokeWidth={SW_THIN} strokeLinecap="round" opacity="0.6" />

      {/* panel underside (bottom edge thickness) — small parallelogram */}
      <polygon points={fmt([FBL, FBR, [FBR[0] + 4, FBR[1] + 5], [FBL[0] + 4, FBL[1] + 5]])}
        fill={C.solarSide} stroke={C.ink} strokeWidth={SW_THIN} />
      {/* panel surface */}
      <polygon points={fmt([FBL, FBR, TBR, TBL])}
        fill={C.solarTop} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* cells */}
      {cells.map((c, i) => (
        <polygon key={i} points={c} fill={cellFill} stroke={C.ink} strokeWidth={SW_THIN}
          style={{ transition: 'fill 600ms' }} />
      ))}
      {/* glow on top edge when producing */}
      {intensity > 0.1 && (
        <polygon points={fmt([FBL, FBR, TBR, TBL])}
          fill="none" stroke={C.solar} strokeWidth={SW_THIN}
          opacity={0.20 + intensity * 0.55}
          style={{ filter: `drop-shadow(0 0 14px ${C.solar})`, transition: 'opacity 600ms' }} />
      )}
    </g>
  );
}

// ── Battery (3D box cabinet with a glowing fill window) ─────────────────────
function Battery({ soc, charging, discharging }) {
  // Origin: front-left-top of cabinet
  const x = 700, y = 450, w = 120, h = 130, d = 60;
  const f = boxFaces(x, y, w, h, d);
  const rf = roundedBoxFaces(x, y, w, h, d, 14);
  const fillH = (h - 38) * Math.max(0.04, soc);
  const lowSoc = soc < 0.25;

  return (
    <g>
      <ellipse cx={x + w/2 + d*ISO_DX/2} cy={y + h + 6} rx={w/2 + 12} ry="6" fill="#000" opacity="0.30" />
      {/* right face (rounded seam) */}
      <path d={rf.right} fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* top (rounded seam) */}
      <path d={rf.top} fill={C.paper3} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* front — rounded */}
      <path d={rf.front} fill={C.paper2} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* fill window on front */}
      <rect x={x + 14} y={y + 20} width={w - 28} height={h - 38} rx="6"
        fill={C.paper5} stroke={C.ink} strokeWidth={SW_THIN} />
      <rect x={x + 14} y={y + 20 + (h - 38) - fillH}
        width={w - 28} height={fillH}
        fill={lowSoc ? C.warn : C.battery}
        rx="6"
        style={{
          transition: 'all 600ms ease',
          filter: !lowSoc ? `drop-shadow(0 0 8px ${C.battery})` : `drop-shadow(0 0 8px ${C.warn})`,
        }} />
      {/* SoC text — large, prominent */}
      <rect x={x + 14} y={y + h - 40} width={w - 28} height="30" rx="4"
        fill={C.paper5} opacity="0.85" />
      <text x={x + w / 2} y={y + h - 18} textAnchor="middle"
        fontFamily="JetBrains Mono" fontSize="22" fontWeight="800" fill={C.ink}
        style={{ filter: !lowSoc ? `drop-shadow(0 0 6px ${C.battery})` : `drop-shadow(0 0 6px ${C.warn})`, letterSpacing: '-0.02em' }}>
        {Math.round(soc * 100)}%
      </text>
    </g>
  );
}

// ── Generator (3D box with vent and exhaust pipe) ────────────────────────────
function Generator({ running, output }) {
  const x = 970, y = 480, w = 130, h = 80, d = 50;
  const f = boxFaces(x, y, w, h, d);
  const rgenF = roundedBoxFaces(x, y, w, h, d, 14);
  // Top vent/cover as a smaller box on top
  const ventH = 22;
  const ventTop = boxFaces(x + 4, y - ventH, w - 8, ventH, d - 6);
  return (
    <g>
      <ellipse cx={x + w/2 + d*ISO_DX/2} cy={y + h + 6} rx={w/2 + 14} ry="6" fill="#000" opacity="0.30" />

      {/* Exhaust puffs (drawn first so generator overlaps them visually) */}
      {running && (
        <g opacity="0.85">
          <circle cx={x + w - 22 + 14 * ISO_DX} cy={y - ventH - 30 + 14 * ISO_DY} r="6" fill={C.ink2}>
            <animate attributeName="cy"
              values={`${y - ventH - 30 + 14 * ISO_DY};${y - ventH - 60 + 14 * ISO_DY};${y - ventH - 90 + 14 * ISO_DY}`}
              dur="2.4s" repeatCount="indefinite" />
            <animate attributeName="r" values="6;10;14" dur="2.4s" repeatCount="indefinite" />
            <animate attributeName="opacity" values="0.5;0.3;0" dur="2.4s" repeatCount="indefinite" />
          </circle>
        </g>
      )}

      {/* generator body */}
      <path d={rgenF.right} fill={C.genBodyL} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <path d={rgenF.top} fill={C.genBody} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <path d={rgenF.front} fill={C.genBodyD} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />

      {/* vent on top */}
      <polygon points={ventTop.right} fill={C.genBodyL} stroke={C.ink} strokeWidth={SW_THIN} />
      <polygon points={ventTop.top} fill={C.genBody} stroke={C.ink} strokeWidth={SW_THIN} />
      <polygon points={ventTop.front} fill={C.genBodyD} stroke={C.ink} strokeWidth={SW_THIN} />
      {/* louvers on vent front */}
      {[0,1,2].map(i => (
        <line key={i} x1={x + 22 + i * 32} y1={y - ventH + 6}
          x2={x + 22 + i * 32} y2={y - ventH + 18}
          stroke={C.ink} strokeWidth={SW_THIN} strokeLinecap="round" opacity="0.7" />
      ))}

      {/* exhaust pipe — vertical box on top-back */}
      {(() => {
        const pf = boxFaces(x + w - 24, y - ventH - 32, 10, 32, 8);
        return (
          <g>
            <polygon points={pf.right} fill={C.genBodyL} stroke={C.ink} strokeWidth={SW_THIN} />
            <polygon points={pf.top} fill={C.genBody} stroke={C.ink} strokeWidth={SW_THIN} />
            <polygon points={pf.front} fill={C.genBodyD} stroke={C.ink} strokeWidth={SW_THIN} />
          </g>
        );
      })()}

      {/* status LED */}
      <circle cx={x + 22} cy={y + 42} r="6"
        fill={running ? C.gen : C.line}
        stroke={C.ink} strokeWidth={SW_THIN}
        style={{ filter: running ? `drop-shadow(0 0 10px ${C.gen})` : 'none' }} />
      {running && (
        <circle cx={x + 22} cy={y + 42} r="6" fill={C.gen} opacity="0.5">
          <animate attributeName="r" values="6;16;6" dur="1.6s" repeatCount="indefinite" />
          <animate attributeName="opacity" values="0.6;0;0.6" dur="1.6s" repeatCount="indefinite" />
        </circle>
      )}
      {/* fuel cap */}
      <circle cx={x + w - 26} cy={y + 42} r="6"
        fill={C.genBodyL} stroke={C.ink} strokeWidth={SW_THIN} />
    </g>
  );
}

// ── Inverter / controller (3D box) ───────────────────────────────────────────
function Inverter({ status }) {
  const w = 96, h = 72, d = 28;
  const x = ANCHORS.inverter.x - w / 2;
  const y = ANCHORS.inverter.y - h;
  const f = boxFaces(x, y, w, h, d);
  const rinvF = roundedBoxFaces(x, y, w, h, d, 10);
  const dotColor = status === 'gen' ? C.gen : status === 'pv' ? C.solar : status === 'bat' ? C.battery : C.ink3;
  return (
    <g>
      <ellipse cx={x + w/2 + d*ISO_DX/2} cy={y + h + 5} rx={w/2 + 10} ry="5" fill="#000" opacity="0.28" />
      <path d={rinvF.right} fill={C.paper4} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <path d={rinvF.top} fill={C.paper3} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      <path d={rinvF.front} fill={C.paper2} stroke={C.ink} strokeWidth={SW} strokeLinejoin="round" />
      {/* screen */}
      <rect x={x + 10} y={y + 8} width={w - 20} height={h - 30} rx="6"
        fill={C.paper5} stroke={C.ink} strokeWidth={SW_THIN} />
      <text x={x + w / 2} y={y + 38} textAnchor="middle" fontSize="16" fontWeight="700"
        fontFamily="JetBrains Mono" fill={dotColor}
        style={{ filter: `drop-shadow(0 0 6px ${dotColor})` }}>
        {status === 'gen' ? 'GEN' : status === 'pv' ? 'PV' : status === 'idle' ? '——' : 'BAT'}
      </text>
      {[0, 1, 2].map((i) => (
        <circle key={i} cx={x + 22 + i * 26} cy={y + h - 12} r="3.5"
          fill={i === 0 ? C.solar : i === 1 ? C.battery : C.gen} opacity="0.95">
          <animate attributeName="opacity" values="0.4;1;0.4" dur="1.8s" begin={`-${i * 0.5}s`} repeatCount="indefinite" />
        </circle>
      ))}
    </g>
  );
}

// ── Ground (iso plane with rolling hills) ────────────────────────────────────
function Ground() {
  return (
    <g>
      {/* far hill silhouette */}
      <path
        d="M0,640 Q150,605 300,615 T700,605 Q900,595 1200,615 L1200,720 L0,720 Z"
        fill={C.paper2}
      />
      {/* ground plane (iso-ish): slightly lighter rolling band */}
      <path
        d="M0,665 Q200,645 450,655 T900,655 Q1050,645 1200,660 L1200,720 L0,720 Z"
        fill={C.ground}
      />
      {/* highlight strip suggesting iso ground rim */}
      <path
        d="M0,665 Q200,645 450,655 T900,655 Q1050,645 1200,660"
        fill="none" stroke={C.line} strokeWidth={SW_THIN} strokeLinecap="round"
      />
    </g>
  );
}

// ── Trunk (energy bus) ───────────────────────────────────────────────────────
function Trunk() {
  return (
    <g fill="none" stroke={C.line} strokeWidth={SW_THIN} strokeLinecap="round" strokeLinejoin="round">
      <path d={`M${ANCHORS.pvOut.x},${ANCHORS.pvOut.y} L${ANCHORS.pvTrunk.x},${ANCHORS.pvTrunk.y}`} />
      <path d={`M${ANCHORS.batteryIn.x},${ANCHORS.batteryIn.y} L${ANCHORS.batteryTrunk.x},${ANCHORS.batteryTrunk.y}`} />
      <path d={`M${ANCHORS.genOut.x},${ANCHORS.genOut.y} L${ANCHORS.genTrunk.x},${ANCHORS.genTrunk.y}`} />
      <path d={`M${ANCHORS.houseIn.x},${ANCHORS.houseIn.y} L${ANCHORS.houseTrunk.x},${ANCHORS.houseTrunk.y}`} />
      <path d={`M${ANCHORS.inverter.x},${ANCHORS.inverter.y} L${ANCHORS.inverterTrunk.x},${ANCHORS.inverterTrunk.y}`} />
      <path d={`M${ANCHORS.houseTrunk.x},${TRUNK_Y} L${ANCHORS.genTrunk.x},${TRUNK_Y}`}
        strokeDasharray="6 7" opacity="0.65" />
    </g>
  );
}

// ── Main Scene ───────────────────────────────────────────────────────────────
function Scene({ sim, tweaks }) {
  const flows = useMemo(() => buildFlows(sim), [sim]);
  let hubStatus = 'idle';
  if (sim.genToHouse > 0.05 || sim.genToBattery > 0.05) hubStatus = 'gen';
  else if (sim.pvToHouse > 0.05 || sim.pvToBattery > 0.05) hubStatus = 'pv';
  else if (sim.batteryToHouse > 0.05) hubStatus = 'bat';
  const lit = sim.hour < 6 || sim.hour >= 19;
  const isCommercial = tweaks.buildingType === 'commercial';

  return (
    <svg className="scene-svg" viewBox="0 0 1200 720" preserveAspectRatio="xMidYMid meet">
      <defs>
        <linearGradient id="sky-grad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor={C.paper2} stopOpacity="0" />
          <stop offset="1" stopColor={C.paper2} stopOpacity="0.6" />
        </linearGradient>
      </defs>

      <rect x="0" y="0" width="1200" height="640" fill="url(#sky-grad)" />
      <Celestial hour={sim.hour} weather={tweaks.weather} scenario={tweaks.scenario} />
      <Ground />

      {/* Background tree (further back, behind house) */}
      {!isCommercial && <Tree x={70} y={665} scale={0.95} />}
      <Bush x={920} y={668} />

      <Trunk />

      <House lit={lit} type={isCommercial ? 'commercial' : 'house'} />
      <PVArray output={sim.pv} panels={isCommercial ? 'large' : 'small'} />
      <Battery soc={sim.soc}
        charging={sim.pvToBattery > 0.05 || sim.genToBattery > 0.05}
        discharging={sim.batteryToHouse > 0.05} />
      <Generator running={sim.genRunning} output={sim.genToBattery + sim.genToHouse} />
      <Inverter status={hubStatus} />


      {tweaks.showFlows && flows.map((f) => {
        const bucket = Math.round(Math.min(1, Math.max(0, f.intensity)) * 4);
        const speedKey = Math.round(tweaks.speed * 4) / 4;
        // Re-mount only when bucket/speed actually changes — small intensity fluctuations
        // (e.g. clouds rolling in) no longer scramble the dot stream.
        return <FlowPath key={`${f.id}-${bucket}-${speedKey}`} flow={f} speedMult={tweaks.speed} />;
      })}

      {tweaks.showLabels && (
        <g fontFamily="JetBrains Mono" fill={C.ink2}>
          <g transform="translate(280, 695)" textAnchor="middle">
            <text fontSize="10" letterSpacing="0.10em" opacity="0.75">
              {isCommercial ? 'BUDYNEK FIRMOWY' : 'DOM JEDNORODZINNY'}
            </text>
            <text x="0" y="14" fontSize="11.5" fontWeight="600" fill={C.ink}>
              {sim.load.toFixed(1)} kW · pobór
            </text>
          </g>
          <g transform="translate(410, 695)" textAnchor="middle">
            <text fontSize="10" letterSpacing="0.10em" opacity="0.75">STEROWNIK</text>
            <text x="0" y="14" fontSize="11.5" fontWeight="600" fill={C.ink}>auto</text>
          </g>
          <g transform="translate(510, 695)" textAnchor="middle">
            <text fontSize="10" letterSpacing="0.10em" opacity="0.75">PV</text>
            <text x="0" y="14" fontSize="11.5" fontWeight="600" fill={C.ink}>{sim.pv.toFixed(1)} kW</text>
          </g>
          <g transform="translate(760, 695)" textAnchor="middle">
            <text fontSize="10" letterSpacing="0.10em" opacity="0.75">MAGAZYN · 15 kWh</text>
            <text x="0" y="14" fontSize="11.5" fontWeight="600" fill={C.ink}>
              {sim.batteryToHouse > 0.05 ? `−${sim.batteryToHouse.toFixed(1)} kW`
                : (sim.pvToBattery + sim.genToBattery) > 0.05 ? `+${(sim.pvToBattery + sim.genToBattery).toFixed(1)} kW`
                : 'idle'}
            </text>
          </g>
          <g transform="translate(1035, 695)" textAnchor="middle">
            <text fontSize="10" letterSpacing="0.10em" opacity="0.75">GENERATOR DIESEL</text>
            <text x="0" y="14" fontSize="11.5" fontWeight="600" fill={C.ink}>
              {sim.genRunning ? `${(sim.genToBattery + sim.genToHouse).toFixed(1)} kW · pracuje` : 'standby'}
            </text>
          </g>
        </g>
      )}
    </svg>
  );
}

window.Scene = Scene;
window.SCENE_C = C;
