/* global React, ReactDOM, L */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ----- Helpers -----
const initials = (name) => {
  if (!name) return "?";
  const words = name.replace(/["']/g, "").split(/\s+/).filter(Boolean);
  if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
  return (words[0][0] + (words[words.length - 1][0] || "")).toUpperCase();
};

const monoColor = (str) => {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0;
  const hue = Math.abs(h) % 360;
  return `oklch(0.78 0.06 ${hue})`;
};

// Avatar that falls back to monogram when IG CDN url expires
function Avatar({ url, name, size = 36 }) {
  const [err, setErr] = useState(!url);
  const style = {
    width: size, height: size,
    borderRadius: "50%",
    background: monoColor(name || ""),
    color: "#1a1a1a",
    display: "flex", alignItems: "center", justifyContent: "center",
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: Math.round(size * 0.36),
    fontWeight: 600,
    overflow: "hidden",
    flexShrink: 0,
    border: "1px solid rgba(0,0,0,0.08)",
  };
  if (err || !url) {
    return <div style={style}>{initials(name)}</div>;
  }
  return (
    <div style={style}>
      <img
        src={url} alt={name}
        loading="lazy"
        onError={() => setErr(true)}
        style={{ width: "100%", height: "100%", objectFit: "cover" }}
      />
    </div>
  );
}

// ----- Map view (Leaflet) -----
function MapView({ data, locAggregates, onPickLocation, focusLocation }) {
  const mapRef = useRef(null);
  const layerRef = useRef(null);
  const containerRef = useRef(null);

  useEffect(() => {
    if (mapRef.current) return;
    const map = L.map(containerRef.current, {
      center: [-2.5, 118],
      zoom: 5,
      minZoom: 4,
      maxZoom: 12,
      zoomControl: false,
      attributionControl: false,
      preferCanvas: true,
    });
    L.control.zoom({ position: "bottomright" }).addTo(map);
    L.tileLayer(
      "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png",
      { subdomains: "abcd", maxZoom: 19 }
    ).addTo(map);
    L.tileLayer(
      "https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png",
      { subdomains: "abcd", maxZoom: 19, pane: "shadowPane" }
    ).addTo(map);
    L.control.attribution({ position: "bottomleft", prefix: false })
      .addAttribution("© OpenStreetMap · CARTO")
      .addTo(map);
    mapRef.current = map;
  }, []);

  // Render markers
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (layerRef.current) layerRef.current.remove();

    const layer = L.layerGroup();
    const maxCount = Math.max(...locAggregates.map(l => l.count));

    locAggregates.forEach((loc) => {
      if (loc.lat == null) return;
      const r = 4 + Math.sqrt(loc.count / maxCount) * 22;
      const marker = L.circleMarker([loc.lat, loc.lng], {
        radius: r,
        fillColor: "#c2410c",
        color: "#7c2d12",
        weight: 1,
        fillOpacity: 0.55,
        opacity: 0.9,
      });
      marker.on("click", () => onPickLocation(loc.name));
      marker.on("mouseover", function () {
        this.setStyle({ fillOpacity: 0.85, weight: 2 });
        this.bindTooltip(
          `<strong>${loc.name}</strong><br/>${loc.count} kampus · ${loc.igCount} IG`,
          { direction: "top", offset: [0, -r], className: "map-tip" }
        ).openTooltip();
      });
      marker.on("mouseout", function () {
        this.setStyle({ fillOpacity: 0.55, weight: 1 });
      });
      marker.addTo(layer);
    });
    layer.addTo(map);
    layerRef.current = layer;
  }, [locAggregates, onPickLocation]);

  // Fly to focus
  useEffect(() => {
    if (!focusLocation || !mapRef.current) return;
    const agg = locAggregates.find(l => l.name === focusLocation);
    if (agg && agg.lat != null) {
      mapRef.current.flyTo([agg.lat, agg.lng], 9, { duration: 0.8 });
    }
  }, [focusLocation, locAggregates]);

  return <div ref={containerRef} className="map-canvas" />;
}

// ----- Matrix view -----
function MatrixView({ data, onPickKampus }) {
  return (
    <div className="matrix-grid">
      {data.map((k, i) => (
        <button key={i} className="matrix-card" onClick={() => onPickKampus(k)}>
          <div className="mc-head">
            <div className="mc-name" title={k.n}>{k.n}</div>
            <div className="mc-loc">{k.l}</div>
          </div>
          <div className="mc-stack">
            {k.ig.slice(0, 5).map((p, j) => (
              <div className="mc-av" key={j} style={{ zIndex: 10 - j }}>
                <Avatar url={p.p} name={p.f || p.u} size={32} />
              </div>
            ))}
            {k.ig.length === 0 && <div className="mc-empty">— belum terindeks —</div>}
            {k.ig.length > 5 && <div className="mc-more">+{k.ig.length - 5}</div>}
          </div>
          <div className="mc-foot">
            <span className="mc-count">{k.ig.length} akun</span>
            <span className="mc-prov">{k.p}</span>
          </div>
        </button>
      ))}
    </div>
  );
}

// ----- Provinsi view -----
function ProvinsiView({ data, onPickKampus, onPickLocation }) {
  const groups = useMemo(() => {
    const g = {};
    data.forEach(k => {
      if (!g[k.p]) g[k.p] = { kampus: [], locs: {}, ig: 0 };
      g[k.p].kampus.push(k);
      g[k.p].locs[k.l] = (g[k.p].locs[k.l] || 0) + 1;
      g[k.p].ig += k.ig.length;
    });
    return Object.entries(g).sort((a, b) => b[1].kampus.length - a[1].kampus.length);
  }, [data]);

  return (
    <div className="prov-list">
      {groups.map(([prov, info]) => (
        <section className="prov-block" key={prov}>
          <header className="prov-head">
            <h2 className="prov-name">{prov}</h2>
            <div className="prov-stats">
              <span><b>{info.kampus.length}</b> kampus</span>
              <span><b>{Object.keys(info.locs).length}</b> kota</span>
              <span><b>{info.ig}</b> akun IG</span>
            </div>
          </header>
          <div className="prov-cities">
            {Object.entries(info.locs).sort((a, b) => b[1] - a[1]).map(([city, n]) => (
              <button key={city} className="prov-chip" onClick={() => onPickLocation(city)}>
                <span className="chip-name">{city}</span>
                <span className="chip-num">{n}</span>
              </button>
            ))}
          </div>
        </section>
      ))}
    </div>
  );
}

// ----- Detail panel (slide-over) -----
function LocationPanel({ name, data, onClose, onPickKampus }) {
  if (!name) return null;
  const list = data.filter(k => k.l === name);
  const totalIG = list.reduce((s, k) => s + k.ig.length, 0);
  return (
    <aside className="side-panel">
      <header className="sp-head">
        <div>
          <div className="sp-eyebrow">Lokasi</div>
          <h2 className="sp-title">{name}</h2>
          <div className="sp-meta">{list.length} kampus · {totalIG} akun Instagram</div>
        </div>
        <button className="sp-x" onClick={onClose} aria-label="Tutup">×</button>
      </header>
      <div className="sp-body">
        {list.map((k, i) => (
          <div className="sp-row" key={i}>
            <button className="sp-row-head" onClick={() => onPickKampus(k)}>
              <div className="sp-name">{k.n}</div>
              <div className="sp-count">{k.ig.length} IG</div>
            </button>
            {k.ig.length > 0 && (
              <div className="sp-igs">
                {k.ig.slice(0, 8).map((p, j) => (
                  <a key={j} href={`https://instagram.com/${p.u}`} target="_blank" rel="noopener" className="sp-ig">
                    <Avatar url={p.p} name={p.f || p.u} size={28} />
                    <span className="sp-u">@{p.u}</span>
                  </a>
                ))}
                {k.ig.length > 8 && <span className="sp-more">+{k.ig.length - 8} lagi</span>}
              </div>
            )}
          </div>
        ))}
      </div>
    </aside>
  );
}

function KampusPanel({ kampus, onClose }) {
  if (!kampus) return null;
  return (
    <aside className="side-panel side-panel--kampus">
      <header className="sp-head">
        <div>
          <div className="sp-eyebrow">Kampus · {kampus.p}</div>
          <h2 className="sp-title">{kampus.n}</h2>
          <div className="sp-meta">{kampus.l} · {kampus.ig.length} akun terindeks</div>
        </div>
        <button className="sp-x" onClick={onClose} aria-label="Tutup">×</button>
      </header>
      <div className="sp-body">
        {kampus.ig.length === 0 ? (
          <div className="sp-noig">Belum ada akun Instagram terindeks untuk kampus ini.</div>
        ) : (
          <div className="kampus-igs">
            {kampus.ig.map((p, j) => (
              <a key={j} href={`https://instagram.com/${p.u}`} target="_blank" rel="noopener" className="kig-row">
                <Avatar url={p.p} name={p.f || p.u} size={48} />
                <div className="kig-text">
                  <div className="kig-full">{p.f || p.u}</div>
                  <div className="kig-handle">@{p.u}</div>
                </div>
                <div className="kig-arrow">↗</div>
              </a>
            ))}
          </div>
        )}
      </div>
    </aside>
  );
}

// ----- Main App -----
function App() {
  const [data, setData] = useState(null);
  const [view, setView] = useState("map"); // map | matrix | prov
  const [query, setQuery] = useState("");
  const [provFilter, setProvFilter] = useState("all");
  const [igFilter, setIgFilter] = useState("all"); // all | with | without
  const [pickedLoc, setPickedLoc] = useState(null);
  const [pickedKampus, setPickedKampus] = useState(null);
  const [tweaks, setTweaks] = useTweaks(/*EDITMODE-BEGIN*/{
    "accent": "#c2410c",
    "density": "comfortable",
    "showStats": true
  }/*EDITMODE-END*/);

  useEffect(() => {
    const inline = document.getElementById("__data");
    if (inline) {
      try { setData(JSON.parse(inline.textContent)); return; } catch (e) {}
    }
    const url = (window.__resources && window.__resources.data) || "data.json";
    fetch(url).then(r => r.json()).then(setData);
  }, []);

  useEffect(() => {
    document.documentElement.style.setProperty("--accent", tweaks.accent);
  }, [tweaks.accent]);

  const provinces = useMemo(() => {
    if (!data) return [];
    const s = new Set(data.map(d => d.p));
    return Array.from(s).sort();
  }, [data]);

  const filtered = useMemo(() => {
    if (!data) return [];
    const q = query.trim().toLowerCase();
    return data.filter(k => {
      if (provFilter !== "all" && k.p !== provFilter) return false;
      if (igFilter === "with" && k.ig.length === 0) return false;
      if (igFilter === "without" && k.ig.length > 0) return false;
      if (q) {
        const hay = (k.n + " " + k.l + " " + k.k + " " + k.ig.map(i => i.u).join(" ")).toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
  }, [data, query, provFilter, igFilter]);

  const locAggregates = useMemo(() => {
    const m = new Map();
    filtered.forEach(k => {
      if (k.lat == null) return;
      const cur = m.get(k.l) || { name: k.l, lat: k.lat, lng: k.lng, count: 0, igCount: 0 };
      cur.count += 1;
      cur.igCount += k.ig.length;
      m.set(k.l, cur);
    });
    return Array.from(m.values());
  }, [filtered]);

  const stats = useMemo(() => {
    if (!data) return null;
    const total = filtered.length;
    const withIG = filtered.filter(k => k.ig.length > 0).length;
    const igTotal = filtered.reduce((s, k) => s + k.ig.length, 0);
    const cities = new Set(filtered.map(k => k.l)).size;
    return { total, withIG, igTotal, cities };
  }, [filtered, data]);

  if (!data) {
    return <div className="loading">memuat 4.305 kampus…</div>;
  }

  return (
    <div className="app">
      {/* Header */}
      <header className="app-head">
        <div className="brand">
          <div className="brand-mark" aria-hidden>◐</div>
          <div className="brand-text">
            <div className="brand-title">Direktori Kampus Indonesia</div>
            <div className="brand-sub">Index Instagram · {data.length.toLocaleString("id-ID")} kampus · 420 lokasi</div>
          </div>
        </div>

        <div className="search-wrap">
          <input
            className="search"
            placeholder="cari kampus, kota, atau @username…"
            value={query}
            onChange={e => setQuery(e.target.value)}
          />
          {query && <button className="search-clear" onClick={() => setQuery("")}>×</button>}
        </div>

        <div className="view-toggle" role="tablist">
          {[
            { id: "map", label: "Peta" },
            { id: "matrix", label: "Matriks" },
            { id: "prov", label: "Provinsi" },
          ].map(t => (
            <button
              key={t.id}
              role="tab"
              aria-selected={view === t.id}
              className={"vt-btn " + (view === t.id ? "is-on" : "")}
              onClick={() => setView(t.id)}
            >
              {t.label}
            </button>
          ))}
        </div>
      </header>

      {/* Filter strip */}
      <div className="filter-strip">
        <label className="filt">
          <span className="filt-label">Provinsi</span>
          <select value={provFilter} onChange={e => setProvFilter(e.target.value)}>
            <option value="all">Semua ({data.length.toLocaleString("id-ID")})</option>
            {provinces.map(p => (
              <option key={p} value={p}>{p}</option>
            ))}
          </select>
        </label>
        <div className="seg">
          {[
            { id: "all", label: "Semua" },
            { id: "with", label: "Dengan IG" },
            { id: "without", label: "Tanpa IG" },
          ].map(o => (
            <button
              key={o.id}
              className={"seg-btn " + (igFilter === o.id ? "is-on" : "")}
              onClick={() => setIgFilter(o.id)}
            >{o.label}</button>
          ))}
        </div>

        {tweaks.showStats && stats && (
          <div className="stats">
            <div className="stat"><span className="sn">{stats.total.toLocaleString("id-ID")}</span><span className="sl">kampus</span></div>
            <div className="stat"><span className="sn">{stats.cities}</span><span className="sl">lokasi</span></div>
            <div className="stat"><span className="sn">{stats.withIG.toLocaleString("id-ID")}</span><span className="sl">terindeks IG</span></div>
            <div className="stat"><span className="sn">{stats.igTotal.toLocaleString("id-ID")}</span><span className="sl">akun</span></div>
          </div>
        )}
      </div>

      {/* Body */}
      <main className={"app-body view-" + view}>
        {view === "map" && (
          <MapView
            data={filtered}
            locAggregates={locAggregates}
            onPickLocation={setPickedLoc}
            focusLocation={pickedLoc}
          />
        )}
        {view === "matrix" && (
          <MatrixView data={filtered} onPickKampus={setPickedKampus} />
        )}
        {view === "prov" && (
          <ProvinsiView
            data={filtered}
            onPickKampus={setPickedKampus}
            onPickLocation={setPickedLoc}
          />
        )}
      </main>

      {/* Side panels */}
      {pickedLoc && !pickedKampus && (
        <LocationPanel
          name={pickedLoc}
          data={data}
          onClose={() => setPickedLoc(null)}
          onPickKampus={setPickedKampus}
        />
      )}
      {pickedKampus && (
        <KampusPanel
          kampus={pickedKampus}
          onClose={() => setPickedKampus(null)}
        />
      )}

      {/* Tweaks panel */}
      <TweaksPanel title="Tweaks">
        <TweakSection title="Tampilan">
          <TweakColor
            label="Aksen"
            value={tweaks.accent}
            onChange={v => setTweak("accent", v)}
            options={["#c2410c", "#1d4ed8", "#15803d", "#7c3aed"]}
          />
          <TweakRadio
            label="Kepadatan"
            value={tweaks.density}
            onChange={v => setTweak("density", v)}
            options={[
              { value: "compact", label: "Padat" },
              { value: "comfortable", label: "Nyaman" },
            ]}
          />
          <TweakToggle
            label="Tampilkan statistik"
            value={tweaks.showStats}
            onChange={v => setTweak("showStats", v)}
          />
        </TweakSection>
      </TweaksPanel>
    </div>
  );

  function setTweak(key, value) { setTweaks({ [key]: value }); }
}

// expose
window.App = App;
