/* global React */
// =============================================================
// Playtest flight recorder — the one non-diegetic overlay
// =============================================================
// A small "FAULT?" chip (top-right). Tapping it snapshots the game
// moment THAT INSTANT (via the getSnapshot prop device.jsx supplies),
// then slides up a sheet: voice note (primary), text, optional +/-
// polarity, send. Empty send = "something felt off here" flag.
//
// Voice is recorded as an audio blob (MediaRecorder) and transcribed
// later at digest time — no live transcription, nothing to review.
//
// Gating: GET /api/feedback/config. If the server has FEEDBACK_KEY set,
// the chip only appears when the playtest link carried ?fbkey=...
// (stashed in sessionStorage). Locally, with no key set, it just shows.
// To ship to production: set FEEDBACK_DISABLED=1 server-side, or remove
// the feedback.jsx script tag from index-device.html.

(() => {
  const { useState, useEffect, useRef } = React;

  const TAG_KEY = "toule_fb_tag";
  const NAME_KEY = "toule_fb_name";
  const HINT_KEY = "toule_fb_hint_shown";
  const SESSION_KEY = "toule_fb_key";
  const MAX_REC_SECS = 120;

  // Stable anonymous identity — survives sessions so repeat voices are
  // recognizable in the digest. No accounts, no PII.
  function testerTag() {
    try {
      let t = localStorage.getItem(TAG_KEY);
      if (!t) {
        t = "tester-" + Math.random().toString(16).slice(2, 6);
        localStorage.setItem(TAG_KEY, t);
      }
      return t;
    } catch (_) { return "tester-anon"; }
  }

  function feedbackKey() {
    try {
      const k = new URLSearchParams(window.location.search).get("fbkey");
      if (k) sessionStorage.setItem(SESSION_KEY, k);
      return sessionStorage.getItem(SESSION_KEY) || "";
    } catch (_) { return ""; }
  }

  function pickMime() {
    if (typeof MediaRecorder === "undefined" || !MediaRecorder.isTypeSupported) return "";
    // Safari records audio/mp4 (AAC); Chrome/Android records webm/opus.
    return ["audio/mp4", "audio/webm;codecs=opus", "audio/webm"]
      .find((m) => MediaRecorder.isTypeSupported(m)) || "";
  }

  function blobToBase64(blob) {
    return new Promise((resolve, reject) => {
      const r = new FileReader();
      r.onload = () => resolve(String(r.result).split(",")[1] || "");
      r.onerror = reject;
      r.readAsDataURL(blob);
    });
  }

  const MONO = 'ui-monospace, "SF Mono", Menlo, monospace';
  const AMBER = "#ffc46b";
  const S = {
    chip: {
      position: "fixed", top: "calc(8px + env(safe-area-inset-top))", right: 8, zIndex: 99997,
      background: "rgba(255,180,60,0.10)", border: "1px solid rgba(255,180,60,0.45)",
      color: AMBER, padding: "4px 10px", fontSize: 10, fontFamily: MONO,
      cursor: "pointer", borderRadius: 4, letterSpacing: "0.12em",
      WebkitTapHighlightColor: "transparent",
    },
    scrim: { position: "fixed", inset: 0, zIndex: 99995, background: "rgba(0,0,0,0.35)" },
    sheet: {
      position: "fixed", left: 0, right: 0, bottom: 0, zIndex: 99996,
      maxWidth: 430, margin: "0 auto", padding: "12px 14px 16px",
      background: "rgba(22,13,24,0.97)", borderTop: "1px solid rgba(255,180,60,0.4)",
      borderLeft: "1px solid rgba(255,180,60,0.15)", borderRight: "1px solid rgba(255,180,60,0.15)",
      borderRadius: "10px 10px 0 0", color: "#e8d4ec",
      fontFamily: MONO, fontSize: 12, animation: "fbSlideUp 180ms ease-out",
      paddingBottom: "calc(16px + env(safe-area-inset-bottom))",
    },
    row: { display: "flex", alignItems: "center", gap: 8 },
    label: { color: AMBER, fontSize: 10, letterSpacing: "0.14em", fontWeight: 600 },
    muted: { color: "#8b7691", fontSize: 10 },
    textarea: {
      width: "100%", minHeight: 64, resize: "vertical", marginTop: 8,
      background: "rgba(255,255,255,0.05)", border: "1px solid rgba(232,212,236,0.2)",
      borderRadius: 5, color: "#e8d4ec", padding: "8px 10px",
      fontFamily: MONO, fontSize: 16, /* 16px stops iOS auto-zoom */
      boxSizing: "border-box",
    },
    smallBtn: {
      background: "rgba(255,255,255,0.06)", border: "1px solid rgba(232,212,236,0.25)",
      color: "#e8d4ec", padding: "6px 12px", fontSize: 12, fontFamily: MONO,
      borderRadius: 5, cursor: "pointer", WebkitTapHighlightColor: "transparent",
    },
    nameInput: {
      flex: 1, minWidth: 0, background: "rgba(255,255,255,0.05)",
      border: "1px solid rgba(232,212,236,0.15)", borderRadius: 5,
      color: "#e8d4ec", padding: "6px 8px", fontFamily: MONO, fontSize: 16,
    },
  };

  function PolarityChip({ active, onTap, children, activeColor }) {
    return (
      <button
        onClick={onTap}
        style={{
          ...S.smallBtn,
          minWidth: 44,
          background: active ? activeColor + "33" : S.smallBtn.background,
          borderColor: active ? activeColor : "rgba(232,212,236,0.25)",
          color: active ? activeColor : "#e8d4ec",
        }}
      >{children}</button>
    );
  }

  function FeedbackOverlay({ getSnapshot }) {
    const [cfg, setCfg] = useState(null);
    const [open, setOpen] = useState(false);
    const [showHint, setShowHint] = useState(false);
    const [comment, setComment] = useState("");
    const [polarity, setPolarity] = useState(null);      // 'up' | 'down' | null
    const [name, setName] = useState(() => { try { return localStorage.getItem(NAME_KEY) || ""; } catch (_) { return ""; } });
    const [recState, setRecState] = useState("idle");    // idle | recording | unavailable
    const [recSecs, setRecSecs] = useState(0);
    const [voice, setVoice] = useState(null);            // { blob, mime, durationMs }
    const [sendState, setSendState] = useState("idle");  // idle | sending | sent | error
    const snapRef = useRef(null);
    const openedAtRef = useRef(null);
    const mediaRef = useRef(null);                       // { rec, timer, startedAt, discard }

    useEffect(() => {
      fetch("/api/feedback/config")
        .then((r) => r.json())
        .then(setCfg)
        .catch(() => setCfg({ enabled: false }));
    }, []);

    const key = feedbackKey();
    if (!cfg || !cfg.enabled || (cfg.keyRequired && !key)) return null;

    const openSheet = () => {
      // Snapshot at button-press, not at send — this is the moment they
      // thought to give feedback; the screen may move on while they type.
      let snap = null;
      try { snap = JSON.parse(JSON.stringify(getSnapshot ? getSnapshot() : null)); }
      catch (_) { snap = { error: "snapshot_failed" }; }
      snapRef.current = snap;
      openedAtRef.current = new Date().toISOString();
      setSendState("idle");
      setOpen(true);
      try {
        if (!localStorage.getItem(HINT_KEY)) {
          setShowHint(true);
          localStorage.setItem(HINT_KEY, "1");
        }
      } catch (_) {}
    };

    const stopRec = (discard) => {
      const m = mediaRef.current;
      if (!m || !m.rec) return;
      m.discard = !!discard;
      clearInterval(m.timer);
      try { if (m.rec.state !== "inactive") m.rec.stop(); } catch (_) {}
      setRecState("idle");
    };

    const startRec = async () => {
      try {
        if (typeof MediaRecorder === "undefined" || !navigator.mediaDevices) {
          setRecState("unavailable");
          return;
        }
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const mime = pickMime();
        const rec = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
        const chunks = [];
        const m = { rec, startedAt: Date.now(), discard: false, timer: null };
        mediaRef.current = m;
        rec.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
        rec.onstop = () => {
          stream.getTracks().forEach((t) => t.stop());
          if (m.discard) return;
          const blob = new Blob(chunks, { type: rec.mimeType || mime || "audio/webm" });
          if (blob.size) setVoice({ blob, mime: blob.type, durationMs: Date.now() - m.startedAt });
        };
        rec.start();
        setVoice(null);
        setRecSecs(0);
        setRecState("recording");
        m.timer = setInterval(() => {
          const s = Math.floor((Date.now() - m.startedAt) / 1000);
          setRecSecs(s);
          if (s >= MAX_REC_SECS) stopRec(false);
        }, 500);
      } catch (_) {
        setRecState("unavailable");
      }
    };

    const closeSheet = () => {
      if (recState === "recording") stopRec(true);
      setOpen(false); // comment/voice/polarity kept — accidental close loses nothing
    };

    const send = async () => {
      if (sendState === "sending") return;
      if (recState === "recording") { stopRec(false); return; } // first tap stops the take
      setSendState("sending");
      try {
        const payload = {
          key: key || undefined,
          comment: comment.trim() || undefined,
          polarity: polarity || undefined,
          tester: { tag: testerTag(), name: name.trim() || undefined },
          client: {
            ua: navigator.userAgent,
            viewport: window.innerWidth + "x" + window.innerHeight,
            openedAt: openedAtRef.current,
            sentAt: new Date().toISOString(),
          },
          snapshot: snapRef.current,
        };
        if (voice) {
          payload.audio = {
            base64: await blobToBase64(voice.blob),
            mime: voice.mime,
            durationMs: voice.durationMs,
          };
        }
        const res = await fetch("/api/feedback", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
        if (!res.ok) throw new Error("HTTP " + res.status);
        setSendState("sent");
        setTimeout(() => {
          setOpen(false);
          setComment(""); setPolarity(null); setVoice(null); setSendState("idle");
        }, 900);
      } catch (_) {
        setSendState("error"); // sheet stays open; nothing is lost
      }
    };

    const saveName = (v) => {
      setName(v);
      try { localStorage.setItem(NAME_KEY, v); } catch (_) {}
    };

    const hasContent = !!(comment.trim() || voice || polarity);
    const fmtSecs = (s) => Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");

    return (
      <React.Fragment>
        <style>{"@keyframes fbSlideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }" +
          "@keyframes fbPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }"}</style>

        {!open && (
          <button style={S.chip} onClick={openSheet} aria-label="Report feedback">
            FAULT?
          </button>
        )}

        {open && (
          <React.Fragment>
            <div style={S.scrim} onClick={closeSheet} />
            <div style={S.sheet}>
              <div style={{ ...S.row, justifyContent: "space-between" }}>
                <span style={S.label}>FIELD REPORT</span>
                <span style={S.muted}>{testerTag()}</span>
                <button onClick={closeSheet} style={{ ...S.smallBtn, border: "none", background: "none", fontSize: 16, padding: "0 4px" }}>×</button>
              </div>

              {showHint && (
                <div style={{ ...S.muted, marginTop: 6 }}>
                  tap this whenever anything strikes you — a thought, a snag, a delight.
                  this moment is captured automatically.
                </div>
              )}

              <div style={{ ...S.row, marginTop: 10 }}>
                <PolarityChip active={polarity === "up"} activeColor="#7fdb8a"
                  onTap={() => setPolarity(polarity === "up" ? null : "up")}>+</PolarityChip>
                <PolarityChip active={polarity === "down"} activeColor="#f85149"
                  onTap={() => setPolarity(polarity === "down" ? null : "down")}>−</PolarityChip>

                <span style={{ flex: 1 }} />

                {recState === "recording" ? (
                  <button onClick={() => stopRec(false)} style={{ ...S.smallBtn, borderColor: "#f85149", color: "#f85149" }}>
                    <span style={{ animation: "fbPulse 1s infinite" }}>●</span> {fmtSecs(recSecs)} — stop
                  </button>
                ) : voice ? (
                  <span style={{ ...S.row, gap: 4 }}>
                    <span style={S.muted}>voice {fmtSecs(Math.round(voice.durationMs / 1000))}</span>
                    <button onClick={() => setVoice(null)} style={{ ...S.smallBtn, padding: "2px 8px" }}>×</button>
                  </span>
                ) : recState === "unavailable" ? (
                  <span style={S.muted}>mic unavailable — type instead</span>
                ) : (
                  <button onClick={startRec} style={{ ...S.smallBtn, borderColor: "rgba(255,180,60,0.45)", color: AMBER }}>
                    ● REC
                  </button>
                )}
              </div>

              <textarea
                style={S.textarea}
                rows={3}
                placeholder="what's on your mind? (optional)"
                value={comment}
                onChange={(e) => setComment(e.target.value)}
              />

              <div style={{ ...S.row, marginTop: 8 }}>
                <input
                  style={S.nameInput}
                  placeholder="sign as (optional)"
                  value={name}
                  onChange={(e) => saveName(e.target.value)}
                />
                <button
                  onClick={send}
                  disabled={sendState === "sending"}
                  style={{
                    ...S.smallBtn,
                    borderColor: "rgba(255,180,60,0.6)", color: AMBER, fontWeight: 600,
                    opacity: sendState === "sending" ? 0.5 : 1, whiteSpace: "nowrap",
                  }}
                >
                  {sendState === "sending" ? "SENDING…"
                    : sendState === "sent" ? "LOGGED ✓"
                    : hasContent ? "SEND" : "FLAG THIS MOMENT"}
                </button>
              </div>

              {sendState === "error" && (
                <div style={{ color: "#f85149", fontSize: 10, marginTop: 6 }}>
                  couldn't send — check connection and try again. your note is still here.
                </div>
              )}
            </div>
          </React.Fragment>
        )}
      </React.Fragment>
    );
  }

  window.FeedbackOverlay = FeedbackOverlay;
})();
