"use client";

// Per-kind renderers for a source's payload. Each takes the (already ok) payload
// and renders a compact card body. Unknown kinds fall back to JSON.

import type { WeatherPayload } from "@/lib/schemas/sources/weather";
import type { NewsPayload } from "@/lib/schemas/sources/news";
import type { BriefsPayload } from "@/lib/schemas/sources/briefs";
import type { OnThisDayPayload } from "@/lib/schemas/sources/onthisday";
import type { ObsidianPayload } from "@/lib/schemas/sources/obsidian";
import type { GitHubPayload } from "@/lib/schemas/sources/github";
import type { VercelPayload } from "@/lib/schemas/sources/vercel";
import type { MastodonPayload } from "@/lib/schemas/sources/mastodon";
import type { BlueskyPayload } from "@/lib/schemas/sources/bluesky";
import type { AqiPayload } from "@/lib/schemas/sources/aqi";
import type { MarketsPayload } from "@/lib/schemas/sources/markets";
import type { HackerNewsPayload } from "@/lib/schemas/sources/hackernews";
import type { UptimePayload } from "@/lib/schemas/sources/uptime";
import type { CalendarPayload } from "@/lib/schemas/sources/calendar";
import type { SportsPayload } from "@/lib/schemas/sources/sports";
import type { LinksPayload } from "@/lib/schemas/sources/links";
import type { CiderPayload } from "@/lib/schemas/sources/cider";
import type { SourceKind } from "@/lib/schemas/source";

const muted = { color: "var(--text-muted)" };
const faint = { color: "var(--text-faint)" };

function dayName(iso: string) {
  return new Date(iso + "T00:00:00").toLocaleDateString(undefined, { weekday: "short" });
}

function fmtTime(iso: string) {
  return new Date(iso).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
}

function aqiColor(v: number | null): string {
  if (v == null) return "inherit";
  if (v <= 50) return "var(--green)";
  if (v <= 100) return "var(--yellow)";
  if (v <= 150) return "var(--orange)";
  if (v <= 200) return "var(--red)";
  return "var(--purple-400)";
}

function uvLabel(v: number): string {
  if (v <= 2) return "Low";
  if (v <= 5) return "Moderate";
  if (v <= 7) return "High";
  if (v <= 10) return "Very High";
  return "Extreme";
}

function WeatherBody({ p, row }: { p: WeatherPayload; row: boolean }) {
  const deg = p.units === "metric" ? "°C" : "°F";
  const wind = p.units === "metric" ? "km/h" : "mph";
  const showFeels =
    p.current.feelsLike != null && Math.abs(p.current.feelsLike - p.current.temp) >= 2;
  const highPollen = p.pollen.filter((pl) => pl.level !== "Low");

  const current = (
    <div className={row ? "shrink-0" : ""}>
      {p.alerts.map((a, i) => (
        <div
          key={i}
          className="text-xs px-2 py-0.5 mb-1.5 rounded"
          style={{ background: "var(--red-200)", color: "var(--text)" }}
        >
          ⚠ {a.event}
        </div>
      ))}
      <div className="flex items-baseline gap-3">
        <span className="text-4xl" style={{ fontFamily: "var(--font-display)" }}>
          {p.current.temp}
          {deg}
        </span>
        <span style={muted}>{p.current.text}</span>
        {showFeels && (
          <span className="text-xs" style={faint}>
            feels {p.current.feelsLike}
            {deg}
          </span>
        )}
      </div>
      <p className="text-xs mt-1" style={faint}>
        {p.location} · {p.current.wind} {wind}
        {p.current.humidity != null ? ` · ${p.current.humidity}% RH` : ""}
        {p.current.uvIndex != null ? ` · UV ${p.current.uvIndex} ${uvLabel(p.current.uvIndex)}` : ""}
        {p.aqi != null && (
          <span style={{ color: aqiColor(p.aqi) }}>
            {" "}· AQI {p.aqi} {p.aqiCategory}
          </span>
        )}
      </p>
      {(p.sunrise || p.sunset || p.moonPhase) && (
        <p className="text-xs mt-0.5" style={faint}>
          {p.sunrise ? `↑ ${fmtTime(p.sunrise)}` : ""}
          {p.sunset ? ` · ↓ ${fmtTime(p.sunset)}` : ""}
          {p.moonPhase ? ` · ${p.moonEmoji} ${p.moonPhase}` : ""}
        </p>
      )}
      {highPollen.length > 0 && (
        <p className="text-xs mt-0.5" style={faint}>
          Pollen: {highPollen.map((pl) => `${pl.label} ${pl.level}`).join(" · ")}
        </p>
      )}
    </div>
  );
  const forecast = (
    <div className="flex gap-4">
      {p.daily.map((d) => (
        <div key={d.date} className="text-center">
          <div className="text-xs" style={muted}>{dayName(d.date)}</div>
          <div className="text-sm font-medium">{d.max}°</div>
          <div className="text-xs" style={faint}>{d.min}°</div>
          {d.precipProb != null && d.precipProb > 0 && (
            <div className="text-xs" style={{ color: "var(--blue)" }}>{d.precipProb}%</div>
          )}
        </div>
      ))}
    </div>
  );
  // Short/wide tiles: current condition and forecast sit side by side.
  return row ? (
    <div className="flex items-center justify-between gap-6 h-full">
      {current}
      {forecast}
    </div>
  ) : (
    <div>
      {current}
      <div className="mt-3">{forecast}</div>
    </div>
  );
}

function NewsBody({ p }: { p: NewsPayload }) {
  return (
    <ul className="space-y-2">
      {p.items.map((it, i) => (
        <li key={i} className="text-sm leading-snug">
          <a href={it.link} target="_blank" rel="noreferrer" style={{ color: "var(--text)" }}>
            {it.title}
          </a>
          <span className="ml-1 text-xs" style={faint}>· {it.source}</span>
        </li>
      ))}
    </ul>
  );
}

function BriefsBody({ p }: { p: BriefsPayload }) {
  const live = p.categories.filter((c) => c.stories.length > 0);
  if (live.length === 0) return <p className="text-sm" style={faint}>No briefs yet.</p>;
  return (
    <div className="space-y-3">
      {live.map((c) => (
        <div key={c.key}>
          <h4 className="text-xs uppercase tracking-widest mb-1" style={muted}>
            {c.name}
          </h4>
          <ul className="space-y-1">
            {c.stories.map((s, i) => (
              <li key={i} className="text-sm leading-snug">
                <span className="font-medium">{s.title}</span>
                {s.sources.length > 0 && (
                  <span className="ml-1 text-xs" style={faint}>· {s.sources[0]}</span>
                )}
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

function Entries({ list }: { list: OnThisDayPayload["events"] }) {
  return (
    <ul className="space-y-1">
      {list.map((e, i) => (
        <li key={i} className="text-sm leading-snug">
          {e.year != null && (
            <span className="font-medium" style={{ fontFamily: "var(--font-mono)" }}>
              {e.year}{" "}
            </span>
          )}
          {e.link ? (
            <a href={e.link} target="_blank" rel="noreferrer" style={{ color: "var(--text)" }}>
              {e.text}
            </a>
          ) : (
            e.text
          )}
        </li>
      ))}
    </ul>
  );
}

function OnThisDayBody({ p }: { p: OnThisDayPayload }) {
  return (
    <div className="space-y-3">
      <Entries list={p.events} />
      {p.births.length > 0 && (
        <div>
          <h4 className="text-xs uppercase tracking-widest mb-1" style={muted}>Born</h4>
          <Entries list={p.births} />
        </div>
      )}
      {p.deaths.length > 0 && (
        <div>
          <h4 className="text-xs uppercase tracking-widest mb-1" style={muted}>Died</h4>
          <Entries list={p.deaths} />
        </div>
      )}
    </div>
  );
}

function ObsidianBody({ p }: { p: ObsidianPayload }) {
  return (
    <ul className="space-y-1">
      {p.notes.map((n) => (
        <li key={n.path} className="flex justify-between gap-3 text-sm">
          <span className="truncate">{n.title}</span>
          <span className="text-xs whitespace-nowrap" style={faint}>
            {new Date(n.modified).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
          </span>
        </li>
      ))}
    </ul>
  );
}

function shortTime(iso: string) {
  return new Date(iso).toLocaleString(undefined, {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });
}

function GitHubBody({ p }: { p: GitHubPayload }) {
  return (
    <div>
      <p className="text-xs mb-2" style={faint}>
        @{p.login} · {p.publicRepos} repos · {p.followers} followers
      </p>
      <ul className="space-y-1">
        {p.events.slice(0, 8).map((e, i) => (
          <li key={i} className="text-sm flex justify-between gap-2">
            <span className="truncate">
              <span style={muted}>{e.type}</span> {e.repo}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

function VercelBody({ p }: { p: VercelPayload }) {
  return (
    <ul className="space-y-1">
      {p.deployments.map((d, i) => (
        <li key={i} className="text-sm flex justify-between gap-2">
          <span className="truncate">
            {d.url ? (
              <a href={d.url} target="_blank" rel="noreferrer" style={{ color: "var(--text)" }}>
                {d.name}
              </a>
            ) : (
              d.name
            )}
            <span className="ml-1 text-xs" style={faint}>· {d.target}</span>
          </span>
          <span className="text-xs whitespace-nowrap" style={muted}>{d.state}</span>
        </li>
      ))}
    </ul>
  );
}

function SocialBody({ items }: { items: Array<{ who: string; text: string; tag: string; link?: string | null }> }) {
  if (items.length === 0) return <p className="text-sm" style={faint}>Nothing recent.</p>;
  return (
    <ul className="space-y-2">
      {items.map((n, i) => (
        <li key={i} className="text-sm leading-snug">
          <span className="font-medium">{n.who}</span>
          <span className="ml-1 text-xs" style={faint}>{n.tag}</span>
          {n.text && (
            <div style={muted}>
              {n.link ? (
                <a href={n.link} target="_blank" rel="noreferrer" style={{ color: "var(--text)" }}>
                  {n.text}
                </a>
              ) : (
                n.text
              )}
            </div>
          )}
        </li>
      ))}
    </ul>
  );
}

function calEventLabel(iso: string, allDay: boolean): string {
  const d = new Date(iso);
  const now = new Date();
  const todayStr = now.toDateString();
  const isToday = d.toDateString() === todayStr;
  const datePart = isToday
    ? "Today"
    : d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
  if (allDay) return datePart;
  const timePart = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
  return `${datePart} ${timePart}`;
}

function CalendarBody({ p }: { p: CalendarPayload }) {
  const cutoff = Date.now() + 7 * 24 * 60 * 60 * 1000;
  const events = p.events.filter((e) => new Date(e.start).getTime() <= cutoff);
  if (events.length === 0) return <p className="text-sm" style={faint}>No upcoming events.</p>;
  return (
    <ul className="space-y-1">
      {events.map((e, i) => (
        <li key={i} className="text-sm flex gap-2">
          <span className="text-xs whitespace-nowrap shrink-0" style={faint}>
            {calEventLabel(e.start, e.allDay)}
          </span>
          <span className="truncate">{e.summary}</span>
        </li>
      ))}
    </ul>
  );
}

function leagueName(path: string) {
  return (path.split("/").pop() ?? path).toUpperCase();
}

type Game = SportsPayload["games"][number];

function gameBucket(g: Game): "live" | "today" | "upcoming" {
  if (g.state === "in" || g.state === "post") return "live";
  if (g.startTime) {
    const d = new Date(g.startTime);
    return d.toDateString() === new Date().toDateString() ? "today" : "upcoming";
  }
  return "today";
}

function gameLabel(g: Game): { text: string; isLive: boolean; isFinal: boolean } {
  const isLive = g.state === "in";
  const isFinal = g.state === "post";
  if (isLive || isFinal) {
    const score = g.awayScore != null && g.homeScore != null ? `${g.awayScore}–${g.homeScore}` : "—";
    const suffix = isFinal
      ? (g.status.includes("OT") ? " F/OT" : " F")
      : `  ${g.status}`;
    return { text: score + suffix, isLive, isFinal };
  }
  if (g.startTime) {
    const d = new Date(g.startTime);
    const isToday = d.toDateString() === new Date().toDateString();
    const datePart = isToday
      ? ""
      : d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }) + "  ";
    return { text: datePart + d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }), isLive, isFinal };
  }
  return { text: g.status, isLive, isFinal };
}

const BUCKET_LABELS = { live: "Live / Final", today: "Today", upcoming: "Upcoming" } as const;

function SportsSection({ label, games }: { label: string; games: Game[] }) {
  const groups: Record<string, Game[]> = {};
  for (const g of games) (groups[g.league] ||= []).push(g);
  return (
    <div className="space-y-2">
      <h4 className="text-xs font-bold uppercase tracking-widest" style={muted}>{label}</h4>
      {Object.entries(groups).map(([league, gs]) => (
        <div key={league}>
          <h5 className="text-xs uppercase tracking-widest mb-1" style={muted}>{leagueName(league)}</h5>
          <ul className="space-y-1">
            {gs.map((g, i) => {
              const { text, isLive } = gameLabel(g);
              return (
                <li key={i} className="text-sm flex gap-2">
                  <span
                    className="text-xs whitespace-nowrap shrink-0"
                    style={isLive ? { color: "var(--text-accent)" } : faint}
                  >{text}</span>
                  <span className="truncate">{g.away} @ {g.home}</span>
                </li>
              );
            })}
          </ul>
        </div>
      ))}
    </div>
  );
}

function SportsBody({ p }: { p: SportsPayload }) {
  const cutoff = Date.now() + 7 * 24 * 60 * 60 * 1000;
  const games = p.games.filter((g) => !g.startTime || new Date(g.startTime).getTime() <= cutoff);
  if (games.length === 0) return <p className="text-sm" style={faint}>No games.</p>;

  const buckets: Record<"live" | "today" | "upcoming", Game[]> = { live: [], today: [], upcoming: [] };
  for (const g of games) buckets[gameBucket(g)].push(g);

  const sections = (["live", "today", "upcoming"] as const).filter((k) => buckets[k].length > 0);
  return (
    <div className="space-y-3">
      {sections.map((k) => (
        <SportsSection key={k} label={BUCKET_LABELS[k]} games={buckets[k]} />
      ))}
    </div>
  );
}

function LinksBody({ p }: { p: LinksPayload }) {
  if (p.links.length === 0) return <p className="text-sm" style={faint}>No links yet.</p>;
  return (
    <div className="flex flex-wrap gap-2">
      {p.links.map((l, i) => (
        <a
          key={i}
          href={l.url}
          target="_blank"
          rel="noreferrer"
          className="px-3 py-1.5 text-sm"
          style={{
            background: "var(--bg-elevated)",
            color: "var(--text)",
            border: "1px solid var(--border-strong)",
            borderRadius: 6,
          }}
        >
          {l.label}
        </a>
      ))}
    </div>
  );
}

function AqiBody({ p }: { p: AqiPayload }) {
  return (
    <div className="space-y-2">
      <div className="flex items-baseline gap-3">
        <span className="text-4xl" style={{ fontFamily: "var(--font-display)" }}>
          {p.aqi ?? "—"}
        </span>
        <span style={muted}>{p.category}</span>
      </div>
      <p className="text-xs" style={faint}>
        {p.location}
        {p.pm25 != null ? ` · PM2.5 ${p.pm25}` : ""}
        {p.pm10 != null ? ` · PM10 ${p.pm10}` : ""}
        {p.ozone != null ? ` · O₃ ${Math.round(p.ozone)}` : ""}
      </p>
      {p.pollen.length > 0 && (
        <ul className="space-y-0.5">
          {p.pollen.map((pl) => (
            <li key={pl.label} className="text-sm flex justify-between">
              <span>{pl.label}</span>
              <span style={muted}>{pl.level}</span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

function MarketsBody({ p }: { p: MarketsPayload }) {
  if (p.quotes.length === 0) return <p className="text-sm" style={faint}>No quotes.</p>;
  return (
    <ul className="space-y-1">
      {p.quotes.map((q) => {
        const up = (q.changePct ?? 0) >= 0;
        return (
          <li key={q.symbol} className="flex items-baseline justify-between text-sm">
            <span className="font-medium">{q.symbol}</span>
            <span className="flex items-baseline gap-2">
              <span>{q.price.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
              {q.changePct != null && (
                <span className="text-xs tabular-nums" style={{ color: up ? "var(--green)" : "var(--red)" }}>
                  {up ? "+" : ""}
                  {q.changePct}%
                </span>
              )}
            </span>
          </li>
        );
      })}
    </ul>
  );
}

function HackerNewsBody({ p }: { p: HackerNewsPayload }) {
  if (p.stories.length === 0) return <p className="text-sm" style={faint}>Nothing yet.</p>;
  return (
    <ul className="space-y-2">
      {p.stories.map((s, i) => (
        <li key={i} className="text-sm leading-snug">
          {s.url ? (
            <a href={s.url} target="_blank" rel="noreferrer" style={{ color: "var(--text)" }}>
              {s.title}
            </a>
          ) : (
            s.title
          )}
          <span className="ml-1 text-xs" style={faint}>
            · {s.source}
            {s.points != null ? ` ▲${s.points}` : ""}
          </span>
        </li>
      ))}
    </ul>
  );
}

function UptimeBody({ p }: { p: UptimePayload }) {
  if (p.sites.length === 0) return <p className="text-sm" style={faint}>No URLs configured.</p>;
  return (
    <ul className="space-y-1">
      {p.sites.map((s) => (
        <li key={s.url} className="flex items-baseline justify-between text-sm">
          <span className="truncate" style={{ maxWidth: "70%" }}>
            {s.url.replace(/^https?:\/\//, "")}
          </span>
          <span className="text-xs" style={{ color: s.ok ? "var(--green)" : "var(--red)" }}>
            {s.ok ? `▲ ${s.ms}ms` : `▼ ${s.status ?? "DOWN"}`}
          </span>
        </li>
      ))}
    </ul>
  );
}

function CiderBody({ p }: { p: CiderPayload }) {
  if (!p.playing || !p.track) {
    return <p className="text-sm" style={faint}>Nothing playing.</p>;
  }
  const { name, artist, album, artworkUrl, durationMs, currentMs } = p.track;
  const pct = durationMs > 0 ? Math.min(100, (currentMs / durationMs) * 100) : 0;

  function fmtDuration(ms: number) {
    const s = Math.floor(ms / 1000);
    return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
  }

  async function skipNext() {
    await fetch("/api/cider/next", { method: "POST" });
  }

  return (
    <div className="flex gap-3 h-full items-start">
      {artworkUrl && (
        // eslint-disable-next-line @next/next/no-img-element
        <img
          src={artworkUrl}
          alt={album}
          width={72}
          height={72}
          className="rounded shrink-0"
          style={{ objectFit: "cover" }}
        />
      )}
      <div className="flex flex-col gap-0.5 min-w-0 flex-1">
        <p className="font-semibold text-sm leading-snug truncate">{name}</p>
        <p className="text-sm truncate" style={muted}>{artist}</p>
        <p className="text-xs truncate" style={faint}>{album}</p>
        <div className="mt-2 h-1 rounded-full overflow-hidden" style={{ background: "var(--border)" }}>
          <div className="h-full rounded-full" style={{ width: `${pct}%`, background: "var(--text-accent)" }} />
        </div>
        <div className="flex justify-between items-center mt-1">
          <span className="text-xs tabular-nums" style={faint}>{fmtDuration(currentMs)}</span>
          <button
            onClick={skipNext}
            className="text-xs px-2 py-0.5 rounded"
            style={{ background: "var(--bg-elevated)", color: "var(--text-muted)", border: "1px solid var(--border)" }}
          >
            next ›
          </button>
          <span className="text-xs tabular-nums" style={faint}>{fmtDuration(durationMs)}</span>
        </div>
      </div>
    </div>
  );
}

export function CardBody({
  kind,
  payload,
  rows,
}: {
  kind: SourceKind;
  payload: unknown;
  rows: number;
}) {
  const shortTile = rows <= 1;
  switch (kind) {
    case "weather":
      return <WeatherBody p={payload as WeatherPayload} row={shortTile} />;
    case "news":
    case "feeds":
      return <NewsBody p={payload as NewsPayload} />;
    case "briefs":
      return <BriefsBody p={payload as BriefsPayload} />;
    case "onthisday":
      return <OnThisDayBody p={payload as OnThisDayPayload} />;
    case "obsidian":
      return <ObsidianBody p={payload as ObsidianPayload} />;
    case "github":
      return <GitHubBody p={payload as GitHubPayload} />;
    case "vercel":
      return <VercelBody p={payload as VercelPayload} />;
    case "mastodon":
      return (
        <SocialBody
          items={(payload as MastodonPayload).notifications.map((n) => ({
            who: n.account,
            text: n.text,
            tag: n.type,
            link: n.url,
          }))}
        />
      );
    case "bluesky":
      return (
        <SocialBody
          items={(payload as BlueskyPayload).notifications.map((n) => ({
            who: n.author,
            text: n.text,
            tag: n.reason,
          }))}
        />
      );
    case "aqi":
      return <AqiBody p={payload as AqiPayload} />;
    case "markets":
      return <MarketsBody p={payload as MarketsPayload} />;
    case "hackernews":
      return <HackerNewsBody p={payload as HackerNewsPayload} />;
    case "uptime":
      return <UptimeBody p={payload as UptimePayload} />;
    case "calendar":
      return <CalendarBody p={payload as CalendarPayload} />;
    case "sports":
      return <SportsBody p={payload as SportsPayload} />;
    case "links":
      return <LinksBody p={payload as LinksPayload} />;
    case "cider":
      return <CiderBody p={payload as CiderPayload} />;
    default:
      return (
        <pre className="text-xs overflow-auto" style={faint}>
          {JSON.stringify(payload, null, 2)}
        </pre>
      );
  }
}
