/* ============================================================
   觀察者 v2.1 — Core charts (race / share / network / hex / spark)
   ============================================================ */
(function(){
  const C_KKY = '#4ba3d4', C_LWT = '#1f3a68', C_YJM = '#c8362a';
  const COLOR = { kky: C_KKY, lwt: C_LWT, yjm: C_YJM };

  /* ---------- ShareStack — single horizontal stacked bar ---------- */
  function ShareStack({ data, height=40 }){
    const total = data.kky + data.lwt + data.yjm;
    return (
      <div className="share-stack" style={{height}}>
        {['kky','lwt','yjm'].map(k => (
          <div key={k} className="seg" style={{
            flex: data[k] / total,
            background: COLOR[k],
          }}>
            {data[k].toFixed(1)}%
          </div>
        ))}
      </div>
    );
  }

  /* ---------- ShareTimeline — stacked area showing share over 30 days ---------- */
  function ShareTimeline({ data, width=720, height=240, showAbs=false }){
    const pad = { l: 36, r: 14, t: 16, b: 22 };
    const W = width - pad.l - pad.r, H = height - pad.t - pad.b;
    const N = data.length;
    const x = i => pad.l + (i/(N-1)) * W;

    if(showAbs){
      // raw absolute lines
      const all = data.flatMap(d => [d.kkyAbs, d.lwtAbs, d.yjmAbs]);
      const yMax = Math.max(...all) * 1.1;
      const yMin = 0;
      const y = v => pad.t + H - ((v - yMin) / (yMax - yMin)) * H;
      return (
        <svg viewBox={`0 0 ${width} ${height}`} width="100%" preserveAspectRatio="xMidYMid meet">
          {[0, 0.25, 0.5, 0.75, 1].map((p,i) => (
            <g key={i}>
              <line x1={pad.l} x2={pad.l+W} y1={y(yMax*p)} y2={y(yMax*p)} stroke="#e6e1d4" strokeWidth="0.6"/>
              <text x={pad.l-6} y={y(yMax*p)+3} fontSize="9" textAnchor="end" fill="#968670" fontFamily="IBM Plex Mono">{Math.round(yMax*p).toLocaleString()}</text>
            </g>
          ))}
          {['kky','lwt','yjm'].map(k => {
            const path = data.map((d,i) => `${i===0?'M':'L'} ${x(i)} ${y(d[k+'Abs'])}`).join(' ');
            return (
              <g key={k}>
                <path d={path} stroke={COLOR[k]} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
                <circle cx={x(N-1)} cy={y(data[N-1][k+'Abs'])} r="4" fill={COLOR[k]} stroke="#fff" strokeWidth="1.5"/>
              </g>
            );
          })}
          {data.map((d,i) => i % 5 === 0 || i === N-1 ? (
            <text key={i} x={x(i)} y={height-6} fontSize="9" textAnchor="middle" fill="#968670" fontFamily="IBM Plex Mono">{d.d}</text>
          ) : null)}
        </svg>
      );
    }

    // stacked area (always sums to 100)
    const y = v => pad.t + H - (v/100) * H;
    // For each point, compute cumulative
    const stack = data.map(d => {
      const a = d.kky, b = a + d.lwt, c = b + d.yjm; // c==100
      return { kkyTop: a, lwtTop: b, yjmTop: c };
    });
    const areaPath = (topKey, botKey) => {
      const top = data.map((d,i) => `${i===0?'M':'L'} ${x(i)} ${y(stack[i][topKey])}`).join(' ');
      const bot = data.slice().reverse().map((d,k) => {
        const i = N-1-k;
        const v = botKey ? stack[i][botKey] : 0;
        return `L ${x(i)} ${y(v)}`;
      }).join(' ');
      return top + ' ' + bot + ' Z';
    };
    return (
      <svg viewBox={`0 0 ${width} ${height}`} width="100%" preserveAspectRatio="xMidYMid meet">
        {[0, 25, 50, 75, 100].map((v,i) => (
          <g key={i}>
            <line x1={pad.l} x2={pad.l+W} y1={y(v)} y2={y(v)} stroke="#e6e1d4" strokeWidth="0.6"/>
            <text x={pad.l-6} y={y(v)+3} fontSize="9" textAnchor="end" fill="#968670" fontFamily="IBM Plex Mono">{v}%</text>
          </g>
        ))}
        <path d={areaPath('kkyTop')} fill={C_KKY} fillOpacity="0.85"/>
        <path d={areaPath('lwtTop','kkyTop')} fill={C_LWT} fillOpacity="0.85"/>
        <path d={areaPath('yjmTop','lwtTop')} fill={C_YJM} fillOpacity="0.85"/>
        {/* end labels */}
        {['kky','lwt','yjm'].map((k,i) => {
          const mid = i===0 ? data[N-1][k]/2
                    : i===1 ? data[N-1].kky + data[N-1][k]/2
                    : data[N-1].kky + data[N-1].lwt + data[N-1][k]/2;
          return (
            <text key={k} x={x(N-1)+6} y={y(mid)+3} fontSize="11" fill={COLOR[k]} fontWeight="600" fontFamily="IBM Plex Mono">
              {data[N-1][k].toFixed(0)}%
            </text>
          );
        })}
        {data.map((d,i) => i % 5 === 0 || i === N-1 ? (
          <text key={i} x={x(i)} y={height-6} fontSize="9" textAnchor="middle" fill="#fff" fontFamily="IBM Plex Mono" opacity="0.7">{d.d}</text>
        ) : null)}
      </svg>
    );
  }

  /* ---------- PollLines — like v2 head-to-head ---------- */
  function PollLines({ polls, width=720, height=260, highlight=null }){
    const pad = { l: 38, r: 50, t: 18, b: 26 };
    const W = width - pad.l - pad.r, H = height - pad.t - pad.b;
    const N = polls.length;
    const allV = polls.flatMap(p => [p.kky, p.lwt, p.yjm]);
    const yMax = Math.ceil(Math.max(...allV) / 5) * 5;
    const yMin = Math.floor(Math.min(...allV) / 5) * 5;
    const x = i => pad.l + (i/(N-1)) * W;
    const y = v => pad.t + H - ((v - yMin)/(yMax - yMin)) * H;
    const lines = [
      { k:'kky', label:'김관영', color:C_KKY },
      { k:'lwt', label:'이원택', color:C_LWT },
      { k:'yjm', label:'양정무', color:C_YJM },
    ];
    return (
      <svg viewBox={`0 0 ${width} ${height}`} width="100%" preserveAspectRatio="xMidYMid meet">
        {[yMin, yMin+(yMax-yMin)/3, yMin+(yMax-yMin)*2/3, yMax].map((v,i) => (
          <g key={i}>
            <line x1={pad.l} x2={pad.l+W} y1={y(v)} y2={y(v)} stroke="#e6e1d4" strokeWidth="0.6"/>
            <text x={pad.l-6} y={y(v)+3} fontSize="9" textAnchor="end" fill="#968670" fontFamily="IBM Plex Mono">{v.toFixed(0)}%</text>
          </g>
        ))}
        {lines.map(L => {
          const path = polls.map((p,i) => `${i===0?'M':'L'} ${x(i)} ${y(p[L.k])}`).join(' ');
          const isHl = highlight === L.k;
          const isFade = highlight && highlight !== L.k;
          return (
            <g key={L.k} opacity={isFade?0.3:1}>
              <path d={path} stroke={L.color} strokeWidth={isHl?3:2} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
              {polls.map((p,i) => (
                <circle key={i} cx={x(i)} cy={y(p[L.k])} r={i===N-1?4.5:2.5} fill={L.color} stroke="#fff" strokeWidth="1.2"/>
              ))}
              <text x={x(N-1)+8} y={y(polls[N-1][L.k])+3} fontSize="11" fontWeight="600" fill={L.color} fontFamily="IBM Plex Mono">
                {polls[N-1][L.k].toFixed(1)}
              </text>
            </g>
          );
        })}
        {polls.map((p,i) => i % 1 === 0 ? (
          <text key={i} x={x(i)} y={height-8} fontSize="9" textAnchor="middle" fill="#968670" fontFamily="IBM Plex Mono">{p.date}</text>
        ) : null)}
      </svg>
    );
  }

  /* ---------- HexShareMap — 14 시군 with candidate dominance + bar ---------- */
  function HexShareMap({ regions, regionShare, width=560, height=420, focus=null }){
    // Place hexes by their q = [col,row]
    const cols = 6, rows = 5;
    const r = 38;
    const dx = r * Math.sqrt(3);
    const dy = r * 1.5;
    const offX = 30, offY = 20;
    const hex = (cx, cy) => {
      const pts = [];
      for(let i=0;i<6;i++){
        const a = Math.PI/180 * (60*i - 30);
        pts.push([cx + r*Math.cos(a), cy + r*Math.sin(a)]);
      }
      return pts.map(p => p.join(',')).join(' ');
    };
    const W = offX + cols*dx + r;
    const H = offY + rows*dy + r;

    const dominantColor = (sh) => {
      if(!sh) return '#e6e1d4';
      const max = Math.max(sh.kky, sh.lwt, sh.yjm);
      const winner = sh.kky===max?'kky': sh.lwt===max?'lwt':'yjm';
      const intensity = (max - 33) / 30; // 33% (tied) → 0, 63% → 1
      const c = COLOR[winner];
      return c;
    };
    const dominantShare = (sh) => {
      if(!sh) return 0;
      return Math.max(sh.kky, sh.lwt, sh.yjm);
    };

    return (
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" preserveAspectRatio="xMidYMid meet">
        {regions.map(reg => {
          const [c, rr] = reg.q;
          const cx = offX + c*dx + (rr%2===1 ? dx/2 : 0);
          const cy = offY + rr*dy;
          const sh = regionShare[reg.code];
          const dom = dominantColor(sh);
          const ds = dominantShare(sh);
          const opacity = focus ? (sh && sh[focus]/60) : ((ds-33)/30);
          const fill = focus ? COLOR[focus] : dom;
          return (
            <g key={reg.code} className="hex-cell">
              <polygon
                points={hex(cx,cy)}
                fill={fill}
                fillOpacity={Math.max(0.25, Math.min(1, opacity))}
                stroke="#fff"
                strokeWidth="2"
              />
              <text x={cx} y={cy-6} fontSize="11" textAnchor="middle" fontFamily="Noto Serif KR" fontWeight="700" fill="#fff">
                {reg.name}
              </text>
              <text x={cx} y={cy+8} fontSize="10" textAnchor="middle" fill="#fff" fontFamily="IBM Plex Mono" opacity="0.9">
                {focus ? `${sh?.[focus]||0}%` : `${ds}%`}
              </text>
              {sh && (
                <g>
                  <rect x={cx-22} y={cy+14} width={14} height={3} fill={C_KKY} opacity={(sh.kky/60)}/>
                  <rect x={cx-7} y={cy+14} width={14} height={3} fill={C_LWT} opacity={(sh.lwt/60)}/>
                  <rect x={cx+8} y={cy+14} width={14} height={3} fill={C_YJM} opacity={(sh.yjm/60)}/>
                </g>
              )}
            </g>
          );
        })}
      </svg>
    );
  }

  /* ---------- CoMentionNetwork — force-free radial layout ---------- */
  function CoMentionNetwork({ data, width=640, height=460 }){
    const cx = width/2, cy = height/2;
    // Place candidates at three vertices, topics radially around center
    const candPos = {
      kky: [cx - 180, cy - 80],
      lwt: [cx + 180, cy - 80],
      yjm: [cx, cy + 160],
    };
    const topicPos = {};
    const topics = data.nodes.filter(n => n.kind==='topic');
    topics.forEach((t, i) => {
      const a = (i / topics.length) * Math.PI * 2 - Math.PI/2;
      const rad = 110 + (i%3)*22;
      topicPos[t.id] = [cx + Math.cos(a)*rad, cy + Math.sin(a)*rad];
    });
    const pos = (id) => candPos[id] || topicPos[id];
    const COLOR_NODE = (n) => {
      if(n.kind==='cand') return COLOR[n.id];
      return '#3a2f25';
    };
    const maxW = Math.max(...data.edges.map(e=>e.w));
    return (
      <svg viewBox={`0 0 ${width} ${height}`} width="100%" preserveAspectRatio="xMidYMid meet">
        {/* edges */}
        {data.edges.map((e,i) => {
          const [x1,y1] = pos(e.s);
          const [x2,y2] = pos(e.t);
          const w = (e.w / maxW) * 5 + 0.5;
          const colorEdge = COLOR[e.s] || COLOR[e.t] || '#968670';
          return (
            <line key={i}
              x1={x1} y1={y1} x2={x2} y2={y2}
              stroke={colorEdge}
              strokeWidth={w}
              strokeOpacity="0.36"
              strokeLinecap="round"
            />
          );
        })}
        {/* nodes */}
        {data.nodes.map(n => {
          const [x,y] = pos(n.id);
          const r = n.kind==='cand' ? n.size/2 + 6 : n.size/2 + 2;
          const fontSize = n.kind==='cand' ? 13 : 10.5;
          return (
            <g key={n.id}>
              <circle cx={x} cy={y} r={r} fill={n.kind==='cand'?COLOR_NODE(n):'#fbfaf6'} stroke={COLOR_NODE(n)} strokeWidth={n.kind==='cand'?0:1.5}/>
              <text x={x} y={y+4} textAnchor="middle"
                fontFamily={n.kind==='cand'?'Noto Serif KR':'Pretendard'}
                fontSize={fontSize}
                fontWeight={n.kind==='cand'?700:500}
                fill={n.kind==='cand'?'#fff':'#1a1612'}
              >
                {n.label}
              </text>
            </g>
          );
        })}
      </svg>
    );
  }

  /* ---------- Sparkline ---------- */
  function Sparkline({ values, width=80, height=22, color='#1f3a68', fill=true }){
    if(!values.length) return null;
    const min = Math.min(...values), max = Math.max(...values);
    const range = max - min || 1;
    const x = i => (i/(values.length-1)) * width;
    const y = v => height - ((v - min)/range) * (height-2) - 1;
    const line = values.map((v,i) => `${i===0?'M':'L'} ${x(i)} ${y(v)}`).join(' ');
    const area = `${line} L ${x(values.length-1)} ${height} L 0 ${height} Z`;
    return (
      <svg viewBox={`0 0 ${width} ${height}`} width={width} height={height} className="spark">
        {fill && <path d={area} fill={color} fillOpacity="0.18"/>}
        <path d={line} stroke={color} strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
        <circle cx={x(values.length-1)} cy={y(values[values.length-1])} r="2" fill={color}/>
      </svg>
    );
  }

  /* ---------- IssueWatchBar — horizontal bar with mention + delta ---------- */
  function IssueWatchBar({ issues, width=640 }){
    const max = Math.max(...issues.map(i=>i.mentions7d));
    const sentColor = (s) => s > 0.15 ? '#4a6041' : s < -0.15 ? '#a8392c' : '#b88321';
    const ownerColor = (o) => o ? COLOR[o] : '#968670';
    return (
      <div style={{display:'flex', flexDirection:'column', gap:6}}>
        {issues.map((it,i) => (
          <div key={i} style={{display:'grid', gridTemplateColumns:'140px 1fr 80px 60px', gap:10, alignItems:'center', padding:'4px 0', borderBottom:'1px dashed #e6e1d4'}}>
            <div style={{fontFamily:'Noto Serif KR', fontWeight:500, fontSize:13}}>
              {it.issue}
              <div style={{fontFamily:'IBM Plex Mono', fontSize:9.5, color:'#968670', letterSpacing:'0.06em'}}>{it.cat}</div>
            </div>
            <div style={{height:18, background:'#efede6', borderRadius:4, position:'relative', overflow:'hidden'}}>
              <div style={{position:'absolute', inset:'0 auto 0 0', width:`${(it.mentions7d/max)*100}%`, background:ownerColor(it.owner), borderRadius:4, opacity:0.85}}/>
              <div style={{position:'absolute', inset:0, padding:'0 8px', display:'flex', alignItems:'center', justifyContent:'space-between', fontSize:10.5, fontFamily:'IBM Plex Mono', color:'#fff', fontWeight:600, mixBlendMode:'multiply'}}>
                <span style={{color:'#fff'}}>{it.mentions7d.toLocaleString()}</span>
                <span style={{color:'#fff', opacity:0.8}}>{it.owner ? `소유: ${it.owner.toUpperCase()}` : '경합'}</span>
              </div>
            </div>
            <div style={{fontFamily:'IBM Plex Mono', fontSize:11, color:it.delta>0?'#4a6041':'#a8392c', fontWeight:600}}>
              {it.delta>0?'▲':'▼'} {Math.abs(it.delta).toFixed(1)}%
            </div>
            <div style={{fontFamily:'IBM Plex Mono', fontSize:11, color:sentColor(it.sentiment), fontWeight:600, textAlign:'right'}}>
              {it.sentiment>0?'+':''}{it.sentiment.toFixed(2)}
            </div>
          </div>
        ))}
      </div>
    );
  }

  /* ---------- MomentumStrip — per candidate WoW change radial bars ---------- */
  function MomentumStrip({ candId, momentum }){
    const m = momentum[candId];
    const items = [
      { k:'search', l:'검색량' },
      { k:'news', l:'뉴스' },
      { k:'youtube', l:'유튜브' },
      { k:'sns', l:'SNS' },
      { k:'polls', l:'여론조사' },
    ];
    const max = Math.max(...items.map(i => Math.abs(m[i.k])));
    return (
      <div style={{display:'flex', gap:14, padding:'4px 0'}}>
        {items.map(it => {
          const v = m[it.k];
          const w = (Math.abs(v) / Math.max(max, 4)) * 100;
          const color = v >= 0 ? '#4a6041' : '#a8392c';
          return (
            <div key={it.k} style={{flex:1}}>
              <div style={{fontFamily:'IBM Plex Mono', fontSize:9.5, letterSpacing:'.12em', color:'#6b5d4a', textTransform:'uppercase', marginBottom:4}}>{it.l}</div>
              <div style={{display:'flex', alignItems:'center', gap:8}}>
                <div style={{flex:1, height:6, background:'#efede6', borderRadius:3, position:'relative'}}>
                  <div style={{position:'absolute', inset:0, width:`${w}%`, background:color, borderRadius:3, transformOrigin:'left'}}/>
                </div>
                <div style={{fontFamily:'IBM Plex Mono', fontSize:11, fontWeight:600, color, minWidth:38, textAlign:'right'}}>
                  {v>=0?'+':''}{v.toFixed(1)}
                </div>
              </div>
            </div>
          );
        })}
      </div>
    );
  }

  /* ---------- Donut ---------- */
  function Donut({ value, max=100, color='#4ba3d4', size=80, thickness=10, label }){
    const r = (size-thickness)/2;
    const cx = size/2, cy = size/2;
    const circ = 2*Math.PI*r;
    const off = circ - (value/max) * circ;
    return (
      <div style={{position:'relative', width:size, height:size}}>
        <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size}>
          <circle cx={cx} cy={cy} r={r} fill="none" stroke="#efede6" strokeWidth={thickness}/>
          <circle cx={cx} cy={cy} r={r} fill="none" stroke={color}
            strokeWidth={thickness}
            strokeDasharray={circ}
            strokeDashoffset={off}
            strokeLinecap="round"
            transform={`rotate(-90 ${cx} ${cy})`}/>
        </svg>
        <div style={{position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column'}}>
          <span style={{fontFamily:'Fraunces', fontWeight:600, fontSize:size/3.5}}>{value.toFixed(0)}<span style={{fontSize:size/8, color:'#968670'}}>%</span></span>
          {label && <span style={{fontSize:9, color:'#968670', fontFamily:'IBM Plex Mono', letterSpacing:'.1em'}}>{label}</span>}
        </div>
      </div>
    );
  }

  window.V21_CHARTS = {
    ShareStack, ShareTimeline, PollLines,
    HexShareMap, CoMentionNetwork,
    Sparkline, IssueWatchBar, MomentumStrip, Donut,
    COLOR,
  };
})();
