// avatar-modal.jsx — profile picture cropper.
//
// Flow:
//   1. User clicks "Change profile picture" in the topbar avatar menu.
//      The parent renders <AvatarUploadModal open={true} ...>.
//   2. The modal auto-opens the file picker on first mount.
//   3. Once the user picks an image, the modal swaps to the crop stage:
//      a circular viewport with the image inside, draggable to pan,
//      with a zoom slider (1×–4×).
//   4. On Save, the modal renders the visible crop region into a
//      256x256 hidden canvas, encodes it as a WebP blob, and POSTs it
//      to /api/users/me/avatar. The server stores the bytes and
//      writes the URL into users.avatar.
//   5. The parent receives the new URL via onSaved and patches its
//      own state immediately. SSE fans the change out to other tabs.
//
// Why client-side resize:
//   We never need a 4 MB phone photo on the server — the avatar
//   renders at most 36px in the topbar and 56px in the drawer. A
//   256x256 WebP @ 0.9 quality lands at ~12-25 KB, which is what
//   actually goes over the wire. The dyno never has to call sharp /
//   jimp — saves CPU on the free tier.

function AvatarUploadModal({ open, onClose, onSaved, currentUser, forceUpload = false }) {
  const VIEW = 320;   // viewport size in CSS px (also the crop area)
  const OUT  = 256;   // exported PNG/WebP size

  const [stage, setStage]     = React.useState("pick");   // pick | crop
  const [src, setSrc]         = React.useState(null);     // object URL
  const [imgEl, setImgEl]     = React.useState(null);     // HTMLImageElement
  const [zoom, setZoom]       = React.useState(1);
  const [pan, setPan]         = React.useState({ x: 0, y: 0 });
  const [error, setError]     = React.useState("");
  const [busy, setBusy]       = React.useState(false);

  const fileInput = React.useRef(null);
  const dragRef   = React.useRef(null);

  // Reset on close so the next open shows a fresh picker.
  function reset() {
    setStage("pick");
    if (src) { try { URL.revokeObjectURL(src); } catch {} }
    setSrc(null); setImgEl(null);
    setZoom(1); setPan({ x: 0, y: 0 });
    setError(""); setBusy(false);
  }
  function close() {
    // In forceUpload mode the modal cannot be dismissed — the user must
    // either save a photo or sign out (handled by the parent screen).
    if (forceUpload) return;
    reset(); onClose && onClose();
  }

  // Auto-open the system file picker the first time the modal mounts
  // in the "pick" stage. If the user dismisses it (no file chosen),
  // we close the modal so they're not staring at an empty placeholder.
  React.useEffect(() => {
    if (!open) return;
    if (stage !== "pick") return;
    const t = setTimeout(() => {
      if (fileInput.current) fileInput.current.click();
    }, 60);
    return () => clearTimeout(t);
  }, [open, stage]);

  function onPick(e) {
    const f = e.target.files && e.target.files[0];
    // Allow re-selecting the same file later by clearing the input value.
    e.target.value = "";
    if (!f) {
      // User canceled the picker. If we're still in the initial pick
      // stage, just close the modal so they're not stuck — UNLESS we're
      // in forceUpload mode, in which case we keep the modal open and
      // wait for them to actually pick a file.
      if (stage === "pick" && !forceUpload) close();
      return;
    }
    if (!/^image\//.test(f.type)) {
      setError("Please pick an image file."); return;
    }
    if (f.size > 5 * 1024 * 1024) {
      setError("Image must be 5 MB or smaller."); return;
    }
    const url = URL.createObjectURL(f);
    const img = new Image();
    img.onload = () => {
      setImgEl(img);
      setSrc(url);
      setZoom(1);
      setPan({ x: 0, y: 0 });
      setError("");
      setStage("crop");
    };
    img.onerror = () => {
      try { URL.revokeObjectURL(url); } catch {}
      setError("Could not read that image.");
    };
    img.src = url;
  }

  // Pan via pointer drag.
  function onPointerDown(e) {
    e.preventDefault();
    dragRef.current = {
      startX: e.clientX, startY: e.clientY,
      panX: pan.x, panY: pan.y,
    };
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
  }
  function onPointerMove(e) {
    if (!dragRef.current) return;
    const dx = e.clientX - dragRef.current.startX;
    const dy = e.clientY - dragRef.current.startY;
    setPan({ x: dragRef.current.panX + dx, y: dragRef.current.panY + dy });
  }
  function onPointerUp(e) {
    dragRef.current = null;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
  }

  // Geometry — derived on every render. Cheap, no need to memoize.
  const fitScale = imgEl
    ? VIEW / Math.min(imgEl.naturalWidth, imgEl.naturalHeight)
    : 1;
  const effScale = fitScale * zoom;
  const displayW = imgEl ? imgEl.naturalWidth  * effScale : 0;
  const displayH = imgEl ? imgEl.naturalHeight * effScale : 0;

  async function save() {
    if (!imgEl || busy) return;
    setBusy(true); setError("");
    try {
      const canvas = document.createElement("canvas");
      canvas.width = OUT; canvas.height = OUT;
      const ctx = canvas.getContext("2d");
      // Map viewport-space pan/zoom back into source-image coordinates.
      // The viewport shows a square region of size (VIEW / effScale)
      // pixels from the natural image, centered around the natural
      // image center offset by pan.
      const srcSize = VIEW / effScale;
      const srcCx = imgEl.naturalWidth  / 2 - pan.x / effScale;
      const srcCy = imgEl.naturalHeight / 2 - pan.y / effScale;
      const srcX = srcCx - srcSize / 2;
      const srcY = srcCy - srcSize / 2;
      ctx.drawImage(imgEl, srcX, srcY, srcSize, srcSize, 0, 0, OUT, OUT);
      // Prefer WebP — broad support and ~30% smaller than PNG. Fall
      // back to PNG if the browser doesn't support WebP encoding.
      let blob = await new Promise(r => canvas.toBlob(r, "image/webp", 0.9));
      if (!blob) blob = await new Promise(r => canvas.toBlob(r, "image/png"));
      if (!blob) { throw new Error("Could not encode the cropped image."); }
      const r = await api.users.uploadAvatar(blob);
      onSaved && onSaved(r);
      close();
    } catch (e) {
      setError((e && e.message) || "Upload failed. Try again.");
      setBusy(false);
    }
  }

  async function removePhoto() {
    if (busy) return;
    setBusy(true); setError("");
    try {
      const r = await api.users.deleteAvatar();
      onSaved && onSaved(r);
      close();
    } catch (e) {
      setError((e && e.message) || "Could not remove photo.");
      setBusy(false);
    }
  }

  if (!open) return null;

  // In forceUpload mode there is no existing avatar (that's the whole
  // reason we're here), so the "Remove current photo" affordance is
  // hidden regardless of currentUser state.
  const hasCurrent = !forceUpload && !!(currentUser && currentUser.avatar);

  return (
    <div className="av-modal-backdrop" onClick={forceUpload ? undefined : close}>
      <div className="av-modal" onClick={e => e.stopPropagation()}>
        <div className="av-modal-head">
          <h3>{forceUpload ? "Add a profile picture to continue" : "Profile picture"}</h3>
          {!forceUpload && (
            <button className="av-close" onClick={close} aria-label="Close" type="button">×</button>
          )}
        </div>

        <input
          ref={fileInput}
          type="file"
          accept="image/png, image/jpeg, image/webp, image/gif"
          style={{ display: "none" }}
          onChange={onPick}
        />

        {stage === "pick" && (
          <div className="av-pick">
            <div className="av-pick-empty">
              {forceUpload
                ? "ZeroProject requires every member to have a profile picture. Choose a square-ish photo to continue — you can pan and zoom in the next step."
                : "Choose a square-ish photo. You can pan and zoom in the next step."}
            </div>
            <div className="av-actions">
              <button className="av-btn av-primary" type="button"
                      onClick={() => fileInput.current && fileInput.current.click()}>
                Choose image
              </button>
              {hasCurrent && (
                <button className="av-btn av-danger" type="button" onClick={removePhoto} disabled={busy}>
                  {busy ? "Removing…" : "Remove current photo"}
                </button>
              )}
            </div>
            {error && <div className="av-error">{error}</div>}
          </div>
        )}

        {stage === "crop" && imgEl && (
          <div className="av-crop">
            <div className="av-crop-wrap">
              <div className="av-viewport"
                   style={{ width: VIEW, height: VIEW }}
                   onPointerDown={onPointerDown}
                   onPointerMove={onPointerMove}
                   onPointerUp={onPointerUp}
                   onPointerCancel={onPointerUp}>
                <img src={src} alt="" draggable={false}
                     style={{
                       position: "absolute",
                       width:  Math.round(displayW),
                       height: Math.round(displayH),
                       left:   Math.round(VIEW / 2 - displayW / 2 + pan.x),
                       top:    Math.round(VIEW / 2 - displayH / 2 + pan.y),
                       pointerEvents: "none",
                       userSelect: "none",
                     }}/>
              </div>
            </div>
            <div className="av-zoom">
              <span className="av-zoom-label">Zoom</span>
              <input type="range" min="1" max="4" step="0.01"
                     value={zoom}
                     onChange={e => setZoom(parseFloat(e.target.value))}/>
              <span className="av-zoom-val">{Math.round(zoom * 100)}%</span>
            </div>
            {error && <div className="av-error">{error}</div>}
            <div className="av-actions">
              <button className="av-btn" type="button"
                      onClick={() => fileInput.current && fileInput.current.click()}
                      disabled={busy}>
                Choose different
              </button>
              {hasCurrent && (
                <button className="av-btn av-danger" type="button" onClick={removePhoto} disabled={busy}>
                  Remove
                </button>
              )}
              <button className="av-btn av-primary" type="button" onClick={save} disabled={busy}>
                {busy ? "Saving…" : "Save photo"}
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ── NameEditModal ────────────────────────────────────────────────
// Self-service display-name editor. Opened from the topbar avatar
// menu's "Edit display name" item. Reuses the existing Modal shell
// so the visual stays consistent with sprint / project / new-task
// modals. POSTs through api.users.updateMe and bubbles the fresh
// user row up to the parent (Root in index.html), which patches:
//   • api.setUser cache
//   • Root's `user` state
//   • the matching PEOPLE global entry (so drawer assignees,
//     command palette, mention rendering all pick up the new name
//     without a full reload).
function NameEditModal({ open, onClose, onSaved, currentUser }) {
  const [name, setName] = React.useState(currentUser?.name || "");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr]   = React.useState("");

  React.useEffect(() => {
    if (!open) return;
    setName(currentUser?.name || "");
    setErr(""); setBusy(false);
  }, [open, currentUser?.name]);

  if (!open) return null;

  async function save() {
    const trimmed = String(name || "").trim();
    if (!trimmed) { setErr("Name can't be empty."); return; }
    if (trimmed === (currentUser?.name || "")) { onClose && onClose(); return; }
    if (trimmed.length > 120) { setErr("120 characters max."); return; }
    setBusy(true); setErr("");
    try {
      const r = await api.users.updateMe({ name: trimmed });
      onSaved && onSaved(r && r.user ? r.user : { ...currentUser, name: trimmed });
      onClose && onClose();
    } catch (e) {
      const fromBody = e && e.body && (e.body.message || e.body.error);
      setErr(fromBody || (e && e.message) || "Could not save name.");
      setBusy(false);
    }
  }
  function onKey(e) {
    if (e.key === "Enter") { e.preventDefault(); save(); }
    if (e.key === "Escape") { e.preventDefault(); onClose && onClose(); }
  }

  // Reuse the global Modal component (sprint-modals.jsx) so styling
  // matches the rest of the app — single source of truth.
  if (typeof Modal === "undefined") {
    // Defensive fallback: if the Modal helper isn't loaded yet,
    // render a barebones inline dialog so the user isn't stuck.
    return (
      <div className="av-modal-backdrop" onClick={busy ? undefined : onClose}>
        <div className="av-modal" onClick={e => e.stopPropagation()}>
          <div className="av-modal-head"><h3>Edit display name</h3></div>
          <div style={{ padding: 16 }}>
            <input className="ms-input" value={name} autoFocus
                   onChange={e => setName(e.target.value)} onKeyDown={onKey}/>
            {err && <div style={{ color: "var(--prio-critical, #c93636)", fontSize: 12, marginTop: 8 }}>{err}</div>}
            <div style={{ marginTop: 14, display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <button className="av-btn" onClick={onClose} disabled={busy}>Cancel</button>
              <button className="av-btn av-primary" onClick={save} disabled={busy || !name.trim()}>
                {busy ? "Saving…" : "Save"}
              </button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <Modal open={true} onClose={busy ? undefined : onClose}
           title="Edit display name"
           subtitle="This is how teammates see you across the app."
           width={420}
           footer={
             <>
               <button className="btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
               <button className="btn-primary" onClick={save} disabled={busy || !name.trim()}>
                 {busy ? "Saving…" : "Save"}
               </button>
             </>
           }>
      <div className="ms-row">
        <label className="ms-label">Display name</label>
        <input className="ms-input"
               value={name} autoFocus
               maxLength={120}
               onChange={e => setName(e.target.value)}
               onKeyDown={onKey}
               placeholder="Your name"/>
        {err && (
          <div style={{ color: "var(--prio-critical, #c93636)", fontSize: 12, marginTop: 6 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// ── TimezoneEditModal ────────────────────────────────────────────
// Per-user timezone picker. Opened from the topbar avatar menu's
// "Set time zone" item. The home clock and any future per-user
// time-of-day display reads users.timezone via PATCH /api/users/me.
//
// We keep the picker pragmatic — a curated short list of the zones
// our actual user base lives in, plus a "Use my device's timezone"
// detect button (Intl.DateTimeFormat().resolvedOptions().timeZone)
// and a manual text field for power users in zones we didn't list.
function TimezoneEditModal({ open, onClose, onSaved, currentUser }) {
  // Curated list — extend freely. The values are IANA zone IDs
  // (Region/City), labels are human-readable. Keep IST first since
  // that's the workspace default.
  const COMMON_TZS = [
    { id: "Asia/Kolkata",       label: "India Standard Time (IST · UTC+5:30)" },
    { id: "Asia/Dubai",         label: "Gulf Standard Time (GST · UTC+4)" },
    { id: "Asia/Singapore",     label: "Singapore (SGT · UTC+8)" },
    { id: "Asia/Tokyo",         label: "Japan (JST · UTC+9)" },
    { id: "Australia/Sydney",   label: "Sydney (AEST · UTC+10/11)" },
    { id: "Europe/London",      label: "London (GMT/BST · UTC+0/1)" },
    { id: "Europe/Berlin",      label: "Central Europe (CET · UTC+1/2)" },
    { id: "America/New_York",   label: "New York (ET · UTC−5/4)" },
    { id: "America/Chicago",    label: "Chicago (CT · UTC−6/5)" },
    { id: "America/Los_Angeles",label: "Los Angeles (PT · UTC−8/7)" },
    { id: "UTC",                label: "UTC" },
  ];

  const initial = (currentUser && currentUser.timezone) || "Asia/Kolkata";
  const [tz, setTz] = React.useState(initial);
  const [custom, setCustom] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr]   = React.useState("");

  const inList = COMMON_TZS.some(z => z.id === tz);
  const showingCustom = !inList;

  React.useEffect(() => {
    if (!open) return;
    setTz(initial);
    setCustom(inList ? "" : initial);
    setErr(""); setBusy(false);
  }, [open, initial]);

  if (!open) return null;

  // What time does it look like in the currently-picked zone right
  // now? Helps the user visually confirm they picked the right zone.
  const previewText = (() => {
    try {
      return new Intl.DateTimeFormat("en-US", {
        timeZone: tz,
        weekday: "short", hour: "2-digit", minute: "2-digit", hour12: true,
      }).format(new Date());
    } catch {
      return "—";
    }
  })();

  function detectFromDevice() {
    try {
      const detected = (Intl.DateTimeFormat().resolvedOptions().timeZone) || "";
      if (detected) { setTz(detected); setCustom(""); setErr(""); }
      else setErr("Could not detect your device's timezone.");
    } catch (e) {
      setErr("Could not detect your device's timezone.");
    }
  }

  function tryValidate(zone) {
    try {
      // Will throw RangeError on invalid IANA strings.
      new Intl.DateTimeFormat("en-US", { timeZone: zone }).format(new Date());
      return true;
    } catch { return false; }
  }

  async function save() {
    const next = (showingCustom ? custom : tz).trim();
    if (!next) { setErr("Pick a zone."); return; }
    if (!tryValidate(next)) { setErr("That doesn't look like a valid IANA zone (try Asia/Kolkata)."); return; }
    if (next === initial) { onClose && onClose(); return; }
    setBusy(true); setErr("");
    try {
      const r = await api.users.updateMe({ timezone: next });
      onSaved && onSaved(r && r.user ? r.user : { ...currentUser, timezone: next });
      onClose && onClose();
    } catch (e) {
      const fromBody = e && e.body && (e.body.message || e.body.error);
      setErr(fromBody || (e && e.message) || "Could not save timezone.");
      setBusy(false);
    }
  }

  // Reuse global Modal helper for visual consistency (same fall-back
  // pattern as NameEditModal).
  if (typeof Modal === "undefined") return null;
  return (
    <Modal open={true} onClose={busy ? undefined : onClose}
           title="Set time zone"
           subtitle="Used by the home clock and any time-of-day display tied to you."
           width={460}
           footer={
             <>
               <button className="btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
               <button className="btn-primary" onClick={save} disabled={busy}>
                 {busy ? "Saving…" : "Save"}
               </button>
             </>
           }>
      <div style={{ display: "grid", gap: 14 }}>
        <div className="ms-row">
          <label className="ms-label">Time zone</label>
          <select className="ms-input" value={inList ? tz : "__custom__"}
                  onChange={(e) => {
                    if (e.target.value === "__custom__") {
                      setCustom(tz); setTz("__custom__"); setErr("");
                    } else {
                      setTz(e.target.value); setCustom(""); setErr("");
                    }
                  }}>
            {COMMON_TZS.map(z => <option key={z.id} value={z.id}>{z.label}</option>)}
            <option value="__custom__">Custom…</option>
          </select>
        </div>

        {(tz === "__custom__" || showingCustom) && (
          <div className="ms-row">
            <label className="ms-label">Custom IANA zone</label>
            <input className="ms-input" value={custom}
                   placeholder="e.g. Asia/Singapore"
                   autoFocus
                   onChange={(e) => { setCustom(e.target.value); setErr(""); }}/>
            <span className="ms-optional">
              Examples: Europe/Paris, America/Toronto, Pacific/Auckland.
            </span>
          </div>
        )}

        <div className="ms-row">
          <span className="ms-label">Preview</span>
          <div style={{ fontSize: 13, color: "var(--ink-muted, #676879)" }}>
            Right now in {showingCustom ? (custom || tz) : tz}: <b style={{ color: "var(--ink-strong, #0f1729)" }}>{previewText}</b>
          </div>
        </div>

        <div>
          <button type="button" className="btn-ghost"
                  onClick={detectFromDevice}
                  style={{ fontSize: 12 }}>
            Use my device's timezone
          </button>
        </div>

        {err && <div style={{ color: "var(--prio-critical, #c93636)", fontSize: 12 }}>{err}</div>}
      </div>
    </Modal>
  );
}

Object.assign(window, { AvatarUploadModal, NameEditModal, TimezoneEditModal });
