/* ============================================================
   Living bento widgets (right column)
   ============================================================ */
const { useState: useS, useEffect: useE, useRef: useR } = React;

const fmt = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;

/* count-up number animation */
function useCountUp(target, on, dur = 1100) {
  const [v, setV] = useS(on ? 0 : target);
  useE(() => {
    if (!on) { setV(target); return; }
    let raf, t0;
    const tick = (t) => {
      if (!t0) t0 = t;
      const p = Math.min(1, (t - t0) / dur);
      const e = 1 - Math.pow(1 - p, 3);
      setV(Math.round(target * e));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, on]);
  return v;
}

/* shared card wrapper with tilt */
function Card({ className = '', area, motion, style, children, label }) {
  const ref = useTilt(motion);
  const cls = `card lift ${area ? 'b-' + area : ''} ${className}`;
  return <div ref={ref} className={cls} style={style} data-screen-label={label}>{children}</div>;
}

/* ---------------- Now Playing (live Spotify, Discord-style card) ---------------- */
function NowPlaying({ data, motion }) {
  const cfg = data.spotify || {};
  // state.status: 'loading' | 'playing' | 'idle'
  const [st, setSt] = useS({ status: 'loading' });
  const [, tick] = useS(0);
  const pullRef = useR(null);
  const lastEndPoll = useR(0);

  // poll the real endpoint
  useE(() => {
    const url = cfg.nowPlayingEndpoint;
    if (!url) { setSt({ status: 'idle' }); return; }
    let alive = true;
    const pull = async () => {
      try {
        const r = await fetch(url, { cache: 'no-store' });
        if (!r.ok) throw new Error(r.status);
        const j = await r.json();
        if (!alive) return;
        if (j && j.isPlaying) {
          setSt({
            status: 'playing',
            track: j.title || j.track || 'Unknown',
            artist: j.artist || '',
            album: j.album || j.albumName || '',
            art: j.albumImageUrl || j.albumArt || null,
            url: j.songUrl || j.url || null,
            durationMs: j.durationMs || 0,
            progressMs: j.progressMs || 0,
            at: Date.now(),    // anchor: real clock time this progress was measured
          });
        } else {
          setSt({ status: 'idle' });
        }
      } catch (e) { if (alive) setSt({ status: 'idle' }); }
    };
    pullRef.current = pull;
    pull();
    const id = setInterval(pull, cfg.pollMs || 15000);
    return () => { alive = false; clearInterval(id); };
  }, [cfg.nowPlayingEndpoint]);

  // smooth real-time ticker — re-derives progress from the anchor (no drift)
  useE(() => {
    if (st.status !== 'playing') return;
    const id = setInterval(() => {
      const el = st.progressMs + (Date.now() - st.at);
      // when the track should be over, re-poll promptly so the next song appears
      if (el >= (st.durationMs || 0) && Date.now() - lastEndPoll.current > 2500) {
        lastEndPoll.current = Date.now();
        pullRef.current && pullRef.current();
      }
      tick((t) => t + 1);
    }, 500);
    return () => clearInterval(id);
  }, [st.status, st.at, st.progressMs, st.durationMs]);

  const playing = st.status === 'playing';
  const durationMs = Math.max(1, st.durationMs || 1);
  const elapsedMs = playing ? Math.min(durationMs, st.progressMs + (Date.now() - st.at)) : 0;
  const pct = playing ? Math.min(100, (elapsedMs / durationMs) * 100) : 0;

  // ---- Idle / not listening ----
  if (!playing) {
    return (
      <Card area="now" motion={motion} label="Now playing" className="np np-idle">
        <div className="card-head">
          <span className="np-brand"><Icon.spotify /> Spotify</span>
          <span className="np-dot-off"></span>
        </div>
        <div className="np-idle-body">
          <div className="np-art np-art-idle"><Icon.spotify width={26} height={26} /></div>
          <div className="np-track">
            <b>{st.status === 'loading' ? 'Checking…' : 'Not listening right now'}</b>
            <span>{st.status === 'loading' ? 'one sec' : 'Offline · no track playing'}</span>
          </div>
        </div>
      </Card>
    );
  }

  // ---- Playing (Discord-style, horizontal) ----
  const Wrapper = st.url ? 'a' : 'div';
  const wrapProps = st.url ? { href: st.url, target: '_blank', rel: 'noreferrer' } : {};
  return (
    <Card area="now" motion={motion} label="Now playing" className="np np-live has-art">
      {st.art && <div className={`np-bg ${motion ? 'drift' : ''}`} style={{ backgroundImage: `url(${st.art})` }}></div>}
      <div className="np-scrim"></div>
      <div className="np-live-grid">
        <Wrapper className="np-art np-art-link" {...wrapProps} style={st.art ? { backgroundImage: `url(${st.art})`, backgroundSize: 'cover' } : undefined}></Wrapper>
        <div className="np-live-info">
          <div className="np-live-top">
            <span className="np-brand on"><Icon.spotify /> Listening on Spotify</span>
            <div className="eq on"><i /><i /><i /><i /><i /></div>
          </div>
          <Wrapper className="np-live-meta" {...wrapProps}>
            <b>{st.track}</b>
            <span className="np-artist">{st.artist}</span>
            {st.album && <span className="np-album">{st.album}</span>}
          </Wrapper>
          <div className="np-live-bottom">
            <div className="np-bar"><i style={{ width: pct + '%' }} /></div>
            <div className="np-time"><span>{fmt(Math.floor(elapsedMs / 1000))}</span><span>{fmt(Math.floor(durationMs / 1000))}</span></div>
          </div>
        </div>
      </div>
    </Card>
  );
}

/* ---------------- Weather / location ---------------- */
function Weather({ data, motion }) {
  const clk = useClock(data.location.tzOffset);
  return (
    <Card area="weather" motion={motion} label="Weather">
      <div className="wx-sun"></div>
      <span className="t-label">{data.location.city} · {data.location.tz}</span>
      <div className="wx-temp">{data.location.temp}°</div>
      <div className="wx-row"><Icon.cloudSun /> {data.location.sky}</div>
      <div className="wx-time">{clk.hh}:{clk.mm} local</div>
    </Card>
  );
}

/* ---------------- Status (LIVE Discord presence via Lanyard) ---------------- */
const STATUS_TEXT = { online: 'Online', idle: 'Idle', dnd: 'Do Not Disturb', offline: 'Offline' };
const ACT_VERB = { 0: 'Playing', 1: 'Streaming', 2: 'Listening to', 3: 'Watching', 5: 'Competing in' };

function lanyardAsset(appId, img) {
  if (!img) return null;
  if (img.startsWith('mp:external/')) return 'https://media.discordapp.net/external/' + img.slice('mp:external/'.length);
  if (img.startsWith('mp:')) return 'https://media.discordapp.net/' + img.slice(3);
  if (img.startsWith('spotify:')) return 'https://i.scdn.co/image/' + img.slice('spotify:'.length);
  if (/^https?:\/\//.test(img)) return img;
  return appId ? `https://cdn.discordapp.com/app-assets/${appId}/${img}.png` : null;
}
function fmtElapsed(ms) {
  if (ms < 0) ms = 0;
  const s = Math.floor(ms / 1000);
  const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
  const pad = (n) => String(n).padStart(2, '0');
  return h > 0 ? `${h}:${pad(m)}:${pad(sec)}` : `${m}:${pad(sec)}`;
}

function StatusCard({ data, motion }) {
  const DISCORD_ID = data.discord.userId || '373411032191991808';
  const [ly, setLy] = useS(null);
  const [now, setNow] = useS(Date.now());

  // poll Lanyard for live presence (no token needed; user must be in discord.gg/lanyard)
  useE(() => {
    let alive = true;
    const pull = async () => {
      try {
        const j = await fetch(`https://api.lanyard.rest/v1/users/${DISCORD_ID}`).then((r) => r.json());
        if (alive && j && j.success) setLy(j.data);
      } catch (e) { /* offline — fall back to static */ }
    };
    pull();
    const id = setInterval(pull, 20000);
    return () => { alive = false; clearInterval(id); };
  }, [DISCORD_ID]);

  // tick every second so the elapsed timer counts up
  useE(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);

  // pick the activity to feature: a game / non-music activity (Spotify is handled by Now Playing)
  let activity = null, kind = null;
  if (ly) {
    const a = (ly.activities || []).find((x) => x.type !== 4 && x.type !== 2 && x.name !== 'Spotify');
    if (a) {
      kind = 'game';
      activity = {
        verb: ACT_VERB[a.type] || 'Playing', name: a.name, line1: a.details, line2: a.state,
        big: a.assets && lanyardAsset(a.application_id, a.assets.large_image),
        small: a.assets && lanyardAsset(a.application_id, a.assets.small_image),
        start: a.timestamps && a.timestamps.start,
      };
    }
  }
  const custom = ly && (ly.activities || []).find((x) => x.type === 4);
  const state = ly ? ly.discord_status : null;
  const dotColor = state ? (STATUS_COLOR[state] || 'var(--status-offline)') : STATUS_COLOR[data.status.state];

  // ---- Rich activity view ----
  if (activity && activity.name) {
    const elapsed = activity.start ? fmtElapsed(now - activity.start) : null;
    return (
      <Card area="status" motion={motion} label="Status" className="act-card">
        {activity.big && <div className="act-bg" style={{ backgroundImage: `url(${activity.big})` }}></div>}
        <div className="act-scrim"></div>
        <div className="act-head">
          <span className="act-verb">{activity.verb}</span>
          <span className="dot pulse" style={{ background: dotColor }}></span>
        </div>
        <div className="act-body">
          <div className="act-thumb">
            {activity.big
              ? <img src={activity.big} alt="" />
              : <div className="act-thumb-fallback"><Icon.gamepad /></div>}
            {activity.small && <img className="act-thumb-sm" src={activity.small} alt="" />}
          </div>
          <div className="act-info">
            <div className="act-name">{activity.name}</div>
            {activity.line1 && <div className="act-line">{activity.line1}</div>}
            {activity.line2 && <div className="act-line dim">{activity.line2}</div>}
            {elapsed && <div className="act-time"><Icon.clock /> {elapsed} elapsed</div>}
          </div>
        </div>
      </Card>
    );
  }

  // ---- Plain presence view (no activity) ----
  const label = state ? STATUS_TEXT[state] : data.status.label;
  const sub = state
    ? (custom ? custom.state : (state === 'offline' ? 'Catch me on Discord' : data.status.sub))
    : data.status.sub;
  return (
    <Card area="status" motion={motion} label="Status">
      <div className="card-head">
        <span className="t-label">{ly ? 'Discord status' : 'Status'}</span>
        <span className="dot pulse" style={{ background: dotColor }}></span>
      </div>
      <div className="status-big">{label}</div>
      <div className="status-sub">{sub}</div>
    </Card>
  );
}

/* ---------------- Discord ---------------- */
const fmtCount = (n) => (typeof n !== 'number') ? n
  : n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(n);

function Discord({ data, motion }) {
  const d = data.discord;
  const ref = useR(null);
  const [seen, setSeen] = useS(false);
  const [live, setLive] = useS(null);

  // pull real server icon, counts & online member avatars (no bot token needed)
  useE(() => {
    const code = d.inviteCode;
    if (!code) return;
    let alive = true;
    (async () => {
      try {
        const inv = await fetch(`https://discord.com/api/v10/invites/${code}?with_counts=true`).then((r) => r.json());
        const g = inv.guild || {};
        const iconUrl = g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.${g.icon.startsWith('a_') ? 'gif' : 'png'}?size=160` : null;
        const bannerUrl = g.splash
          ? `https://cdn.discordapp.com/splashes/${g.id}/${g.splash}.jpg?size=1024`
          : g.banner
            ? `https://cdn.discordapp.com/banners/${g.id}/${g.banner}.jpg?size=1024`
            : null;
        let avatars = [];
        try {
          const w = await fetch(`https://discord.com/api/guilds/${g.id}/widget.json`).then((r) => r.json());
          avatars = (w.members || []).filter((m) => m.avatar_url).slice(0, 6).map((m) => m.avatar_url);
        } catch (e) { /* widget disabled — fall back to gradient avatars */ }
        if (!alive) return;
        setLive({
          name: g.name || d.server,
          iconUrl,
          bannerUrl,
          members: inv.approximate_member_count,
          online: inv.approximate_presence_count,
          avatars,
        });
      } catch (e) { /* offline — keep static fallback */ }
    })();
    return () => { alive = false; };
  }, [d.inviteCode]);

  useE(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver(([e]) => e.isIntersecting && setSeen(true), { threshold: 0.4 });
    io.observe(ref.current);
    return () => io.disconnect();
  }, []);

  const name = (live && live.name) || d.server;
  const membersStr = live ? fmtCount(live.members) : d.members;
  const onlineTarget = live ? live.online : d.online;
  const online = useCountUp(onlineTarget, seen && motion);
  const realAvatars = live && live.avatars && live.avatars.length ? live.avatars : null;
  const moreCount = live ? Math.max(0, live.online - (realAvatars ? realAvatars.length : 0)) : 9;

  return (
    <Card area="discord" motion={motion} label="Discord" className="dc">
      {live && (live.bannerUrl || live.iconUrl) && <div className={`dc-banner ${motion ? 'drift' : ''}`} style={{ backgroundImage: `url(${live.bannerUrl || live.iconUrl})` }}></div>}
      <div className="dc-scrim"></div>
      <div ref={ref} className="dc-top">
        <div className="dc-ico-wrap">
          <div className="dc-ico" style={live && live.iconUrl ? { background: 'none' } : undefined}>
            {live && live.iconUrl
              ? <img className="dc-ico-img" src={live.iconUrl} alt="" />
              : <img src="https://cdn.simpleicons.org/discord/ffffff" width="34" alt="" />}
          </div>
          <span className="dc-ico-ring"></span>
        </div>
        <div className="dc-id">
          <span className="dc-server">{name}</span>
          <span className="dc-handle">discord.gg/{d.inviteCode || 'invite'}</span>
          <div className="dc-stats">
            <span className="dc-on"><span className="dot pulse" style={{ background: 'var(--status-online)' }}></span><b>{online}</b> online</span>
            <span className="dc-sep">·</span>
            <span className="dc-mem"><b>{membersStr}</b> members</span>
          </div>
        </div>
        <a className="dc-join" href={d.invite} target="_blank" rel="noreferrer">Join</a>
      </div>
      <div className="dc-foot">
        <div className="dc-avis">
          {realAvatars
            ? realAvatars.map((u, i) => <div key={i} className="av" style={{ backgroundImage: `url(${u})` }} />)
            : d.avatars.map((g, i) => <div key={i} className="av" style={{ background: g }} />)}
          {moreCount > 0 && <div className="dc-more">+{fmtCount(moreCount)}</div>}
        </div>
        <span className="dc-foot-label">members hanging out</span>
      </div>
    </Card>
  );
}

/* ---------------- Currently building ---------------- */
function Building({ data, motion }) {
  const b = data.building;
  const ref = useTilt(motion);
  return (
    <a className="card lift b-building proj-card" data-screen-label="Building"
       href={b.link} target="_blank" rel="noopener" ref={ref} title={`Visit ${b.title}`}>
      <div className="card-head">
        <span className="t-label" style={{ color: 'var(--accent)' }}><Icon.sparkles style={{ width: 12, height: 12, verticalAlign: '-2px' }} /> Currently building</span>
        <Icon.arrowUpRight className="proj-go" />
      </div>
      <h3 className="proj-x" style={{ margin: '10px 0 0' }}><span className="t-title" style={{ fontSize: 19 }}>{b.title}</span></h3>
      <p style={{ margin: '6px 0 0', fontSize: 12.5, color: 'var(--fg-muted)', lineHeight: 1.45 }}>{b.desc}</p>
      <div className="tags">
        {b.tags.map((t, i) => <span key={t} className={`tag ${i === 0 ? 'hot' : ''}`}>{t}</span>)}
      </div>
      <span className="proj-link">{b.link.replace(/^https?:\/\//, '').replace(/\/$/, '')}</span>
    </a>
  );
}

/* ---------------- GitHub profile ---------------- */
function GitHub({ data, motion }) {
  const g = data.github;
  const ref = useR(null);
  const [seen, setSeen] = useS(false);
  const [live, setLive] = useS(null);
  useE(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver(([e]) => e.isIntersecting && setSeen(true), { threshold: 0.3 });
    io.observe(ref.current);
    return () => io.disconnect();
  }, []);
  // pull REAL GitHub stats from the public API (no auth needed)
  useE(() => {
    let alive = true;
    (async () => {
      try {
        const u = await fetch(`https://api.github.com/users/${g.user}`).then((r) => r.json());
        if (!alive || !u || u.message) return;
        let totalStars = 0;
        try {
          const repos = await fetch(`https://api.github.com/users/${g.user}/repos?per_page=100&type=owner`).then((r) => r.json());
          if (Array.isArray(repos)) totalStars = repos.reduce((s, r) => s + (r.stargazers_count || 0), 0);
        } catch (e) {}
        if (!alive) return;
        setLive({
          repos: u.public_repos != null ? u.public_repos : g.repos,
          followers: u.followers != null ? u.followers : g.followers,
          stars: totalStars,
          since: u.created_at ? new Date(u.created_at).getFullYear() : null,
        });
      } catch (e) { /* offline — keep fallback numbers */ }
    })();
    return () => { alive = false; };
  }, [g.user]);
  const repos = useCountUp(live ? live.repos : g.repos, seen && motion, 900);
  const followers = useCountUp(live ? live.followers : g.followers, seen && motion, 1100);
  const stars = useCountUp(live ? live.stars : g.stars, seen && motion, 1300);
  // deterministic contribution levels (18 weeks × 7 days, square cells)
  const cells = useR(Array.from({ length: 18 * 7 }, (_, i) => {
    const r = (Math.sin(i * 12.9898) * 43758.5453) % 1;
    const x = Math.abs(r);
    return x > 0.78 ? 4 : x > 0.6 ? 3 : x > 0.42 ? 2 : x > 0.24 ? 1 : 0;
  })).current;
  return (
    <Card area="github" motion={motion} label="GitHub" className="gh-card">
      <div ref={ref} className="card-head">
        <div className="icotile"><Icon.github /></div>
        <div className="gh-head-r">{live && live.since ? <><Icon.flame className="flame" /> Since {live.since}</> : <><Icon.flame className="flame" /> {g.streak} day streak</>}</div>
      </div>
      <div className="gh-user"><b>@{g.user}</b><span>github.com/{g.user}</span></div>
      <div className="gh-stats">
        <div><div className="n">{repos}</div><div className="l">Repos</div></div>
        <div><div className="n">{followers}</div><div className="l">Followers</div></div>
        <div><div className="n">{stars}</div><div className="l">★ Stars</div></div>
      </div>
      <div className="gh-graph">
        {cells.map((lvl, i) => (
          <div key={i} className={`gh-cell ${lvl ? 'l' + lvl : ''}`}
               style={motion ? { animationDelay: (i * 4) + 'ms' } : undefined} />
        ))}
      </div>
    </Card>
  );
}

/* ---------------- Photo (shows latest posted photo, else drop slot) ---------------- */
function Photo({ photo, motion, area, post }) {
  if (post) {
    return (
      <a className={`card photo lift posted b-${area}`} data-screen-label="Photo"
         href="Photos.html" title={post.title}
         ref={useTilt(motion)}>
        <img src={post.img} alt={post.title || ''} className="ph-img" />
        <div className="ph-overlay">
          <span className="ph-title">{post.title || photo.caption}</span>
          {post.location ? <span className="ph-loc">{post.location}</span> : null}
        </div>
      </a>
    );
  }
  return (
    <Card className="photo" area={area} motion={motion} label="Photo">
      <image-slot id={`photo-${photo.id}`} shape="rect" placeholder={`Drop a photo · ${photo.caption}`}
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}></image-slot>
      <span className="ph-cap">{photo.caption}</span>
    </Card>
  );
}

/* ---------------- Blog ---------------- */
function Blog({ data, motion }) {
  return (
    <Card area="blog" motion={motion} label="Blog">
      <div className="card-head">
        <span className="t-label">From the blog</span>
        <Icon.rss style={{ width: 15, height: 15, color: 'var(--fg-subtle)' }} />
      </div>
      <div className="blog-list">
        {data.blog.map((p) => (
          <div className="blog-item" key={p.title}>
            <div>
              <b>{p.title}</b>
              <div className="d">{p.date}</div>
            </div>
            <span className="blog-arrow"><Icon.arrowUpRight /></span>
          </div>
        ))}
      </div>
    </Card>
  );
}

/* ---------------- Ko-fi ---------------- */
function Kofi({ motion }) {
  return (
    <Card area="kofi" motion={motion} label="Ko-fi" style={{ background: 'linear-gradient(150deg, color-mix(in srgb, var(--kofi) 12%, var(--surface)), var(--surface))', justifyContent: 'space-between' }}>
      <div className="card-head">
        <div className="icotile" style={{ background: 'color-mix(in srgb, var(--kofi) 16%, transparent)', color: 'var(--kofi)' }}><Icon.coffee /></div>
      </div>
      <div className="kofi-body">
        <div>
          <div className="kofi-big">Buy me a coffee <span className="kofi-heart">☕</span></div>
          <div className="kofi-sub">Support the late-night builds</div>
        </div>
        <a className="btn btn-primary" style={{ background: 'var(--kofi)' }} href="https://ko-fi.com/mashowmagic" target="_blank" rel="noreferrer">Ko-fi</a>
      </div>
    </Card>
  );
}

Object.assign(window, { Card, NowPlaying, Weather, StatusCard, Discord, Building, GitHub, Photo, Blog, Kofi, useCountUp });
