// aelred.jsx — Brother Aelred, the website's chat porter
//
// A floating affordance at the lower-right of the viewport opens to a
// parchment-styled chat panel. The panel POSTs to /api/ask and renders
// Brother Aelred's reply. Returning visitors are recognized by a
// localStorage-stored first name (no server-side transcripts).
//
// Voice mode is included (Web Speech API): tap the microphone to speak,
// tap the speaker to hear Aelred's reply read aloud.

(function () {
  const STORAGE_NAME       = 'mumc.aelred.name.v1';
  const STORAGE_PASTOR_URL = 'mumc.aelred.lasturl.v1';

  // ── A short synthesized bell tone, played when the affordance is
  // tapped (only if sound is enabled in the accessibility settings).
  // Two pure sine tones — A5 (880 Hz) and its perfect-fifth E6 (1318.5
  // Hz) — with an exponential decay over ~1.2 seconds. Low volume.
  function ringTheBell() {
    try {
      if (typeof localStorage !== 'undefined' &&
          localStorage.getItem('mumc.sound') === 'off') return;
    } catch (e) {}
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (!AC) return;
      const ctx = new AC();
      const now = ctx.currentTime;
      const gain = ctx.createGain();
      gain.gain.setValueAtTime(0, now);
      gain.gain.linearRampToValueAtTime(0.16, now + 0.008);
      gain.gain.exponentialRampToValueAtTime(0.0001, now + 1.2);
      gain.connect(ctx.destination);
      const o1 = ctx.createOscillator();
      o1.type = 'sine';
      o1.frequency.setValueAtTime(880, now);
      o1.connect(gain);
      const o2 = ctx.createOscillator();
      o2.type = 'sine';
      o2.frequency.setValueAtTime(1318.5, now);
      const g2 = ctx.createGain();
      g2.gain.value = 0.55;
      o2.connect(g2).connect(gain);
      o1.start(now); o2.start(now);
      o1.stop(now + 1.3); o2.stop(now + 1.3);
      setTimeout(() => { try { ctx.close(); } catch (e) {} }, 1500);
    } catch (e) {}
  }

  // ── Floating affordance ─────────────────────────────────────────
  // A bare bell, 40×40 to match the accessibility button. No circular
  // background — the bell hangs in the margin and reads as something
  // you ring to summon the porter.
  function AelredAffordance({ theme, open, onOpen }) {
    if (open) return null;
    return (
      <button
        onClick={() => { ringTheBell(); onOpen(); }}
        aria-label="Ring for Brother Aelred"
        style={{
          position: 'fixed',
          right:  'calc(14px + env(safe-area-inset-right))',
          bottom: 'calc(64px + env(safe-area-inset-bottom))',
          width: 40, height: 40,
          background: 'transparent', border: 'none', padding: 0,
          cursor: 'pointer', zIndex: 95,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}
      >
        {/* Bell — gold body with darker rim and clapper, no enclosing
            circle. A subtle drop-shadow on the SVG itself gives it
            depth without a hard border. */}
        <svg width="34" height="34" viewBox="0 0 40 40" aria-hidden="true"
             style={{ filter: 'drop-shadow(0 1px 2px rgba(60,40,20,0.30))' }}>
          {/* Yoke / crown loop at top */}
          <path d="M17 5 Q20 2 23 5" fill="none" stroke="#7a5a30" strokeWidth="1.4" strokeLinecap="round"/>
          {/* Crown disc */}
          <ellipse cx="20" cy="7.5" rx="2.6" ry="1.6" fill={theme.gold}/>
          {/* Bell body — wider at the bottom, curves in toward the crown */}
          <path d="M10.5 28 C 10.5 18, 14.5 11, 20 9 C 25.5 11, 29.5 18, 29.5 28 Z"
                fill={theme.gold} stroke="#7a5a30" strokeWidth="1"/>
          {/* Highlight stroke on the left for a touch of dimension */}
          <path d="M14 22 C 14 17, 16 13, 18.5 11" stroke="#f0d090"
                strokeWidth="1.2" fill="none" opacity="0.75" strokeLinecap="round"/>
          {/* Rim at the bottom of the bell */}
          <ellipse cx="20" cy="28" rx="9.5" ry="1.8" fill="#9a7320"
                   stroke="#7a5a30" strokeWidth="0.8"/>
          {/* Clapper just under the rim */}
          <line x1="20" y1="28" x2="20" y2="32" stroke="#7a5a30" strokeWidth="0.8"/>
          <circle cx="20" cy="33" r="1.8" fill="#7a5a30"/>
        </svg>
      </button>
    );
  }

  // ── The chat panel ──────────────────────────────────────────────
  function AelredChat({ theme, onClose }) {
    const [messages, setMessages] = React.useState(() => {
      const name = (() => {
        try { return localStorage.getItem(STORAGE_NAME) || ''; }
        catch (e) { return ''; }
      })();
      const greeting = name
        ? `Peace to you, ${name}. How are you?`
        : `Peace be with you, friend.  I am Brother Aelred, this church's porter at the gate.  How may I help?`;
      return [{ role: 'assistant', content: greeting }];
    });
    const [input, setInput]       = React.useState('');
    const [thinking, setThinking] = React.useState(false);
    const [voiceMode, setVoiceMode] = React.useState(false);
    const [listening, setListening] = React.useState(false);
    const inputRef    = React.useRef(null);
    const scrollRef   = React.useRef(null);
    // Forward-declared refs used by handlers and effects below.
    const recogRef     = React.useRef(null);
    const sendRef      = React.useRef(null);
    const voiceModeRef = React.useRef(voiceMode);
    voiceModeRef.current = voiceMode;

    React.useEffect(() => {
      if (scrollRef.current) {
        scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      }
    }, [messages, thinking]);

    // Track Aelred's speaking state so the UI can show "Brother Aelred
    // is speaking…" instead of leaving the visitor wondering if anything
    // is happening.
    const [speaking, setSpeaking] = React.useState(false);

    // Wait for the speechSynthesis voices to load. iOS Safari often
    // returns an empty list on first call to getVoices(); voices populate
    // asynchronously and fire the `voiceschanged` event.
    const ensureVoicesLoaded = () => new Promise((resolve) => {
      if (!('speechSynthesis' in window)) { resolve([]); return; }
      const initial = window.speechSynthesis.getVoices();
      if (initial && initial.length > 0) { resolve(initial); return; }
      const handler = () => {
        const v = window.speechSynthesis.getVoices();
        if (v && v.length > 0) {
          window.speechSynthesis.removeEventListener('voiceschanged', handler);
          resolve(v);
        }
      };
      window.speechSynthesis.addEventListener('voiceschanged', handler);
      // Safety timeout — resolve with whatever we have after 1.5s
      setTimeout(() => {
        window.speechSynthesis.removeEventListener('voiceschanged', handler);
        resolve(window.speechSynthesis.getVoices());
      }, 1500);
    });

    const pickBritishMaleVoice = (voices) =>
         voices.find(v => /en[-_]GB/i.test(v.lang) && /Daniel/i.test(v.name))
      || voices.find(v => /en[-_]GB/i.test(v.lang) && /Arthur|Oliver|Reed/i.test(v.name))
      || voices.find(v => /en[-_]GB/i.test(v.lang))
      || voices.find(v => /^en/i.test(v.lang))
      || voices[0];

    // Speech-synthesis helper. Returns a Promise that resolves when the
    // utterance finishes — important for conversational mode, which
    // auto-starts listening again after Aelred has stopped speaking.
    const speakReply = async (text) => {
      if (!('speechSynthesis' in window)) return;
      try {
        const voices = await ensureVoicesLoaded();
        const preferred = pickBritishMaleVoice(voices);
        return new Promise((resolve) => {
          window.speechSynthesis.cancel();
          const utter = new SpeechSynthesisUtterance(text);
          utter.rate = 0.94;
          utter.pitch = 0.92;
          utter.volume = 1.0;
          if (preferred) utter.voice = preferred;
          setSpeaking(true);
          utter.onend = () => { setSpeaking(false); resolve(); };
          utter.onerror = () => { setSpeaking(false); resolve(); };
          window.speechSynthesis.speak(utter);
        });
      } catch (e) { setSpeaking(false); }
    };

    // On first mount in voice mode, prime the voices list. Some browsers
    // (iOS) require the synth to be touched in a user-gesture handler
    // before voices are populated.
    React.useEffect(() => {
      if (!voiceMode) return;
      if ('speechSynthesis' in window) {
        try {
          const ping = new SpeechSynthesisUtterance(' ');
          ping.volume = 0;
          window.speechSynthesis.speak(ping);
          window.speechSynthesis.cancel();
        } catch (e) {}
      }
    }, [voiceMode]);

    // Toggle voice mode. When turning ON, Brother Aelred introduces
    // himself, explains the conversational mode, then opens the mic so
    // the visitor can speak naturally. When turning OFF, we stop any
    // in-flight speech and cancel listening.
    const handleVoiceToggle = () => {
      const turningOn = !voiceMode;
      setVoiceMode(turningOn);
      if (turningOn) {
        // Prime the synth (must happen in user gesture for iOS)
        try {
          const ping = new SpeechSynthesisUtterance(' ');
          ping.volume = 0;
          window.speechSynthesis.speak(ping);
          window.speechSynthesis.cancel();
        } catch (e) {}

        const intro = "Peace to you. I am Brother Aelred. Voice mode is now on; you may simply speak to me, and I will speak back. When I am done speaking I will listen for you again. If you'd rather type, tap the voice button to turn this off.";
        setMessages(prev => [...prev, { role: 'assistant', content: intro }]);
        (async () => {
          await speakReply(intro);
          if (recogRef.current && !listening) {
            try { recogRef.current.start(); setListening(true); }
            catch (e) {}
          }
        })();
      } else {
        try { window.speechSynthesis.cancel(); } catch (e) {}
        try { if (recogRef.current && listening) recogRef.current.stop(); } catch (e) {}
        setListening(false);
      }
    };

    const send = async () => {
      const text = input.trim();
      if (!text || thinking) return;
      setInput('');
      const next = [...messages, { role: 'user', content: text }];
      setMessages(next);
      setThinking(true);
      try {
        const resp = await fetch('/api/ask', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            messages: next,
            season:     theme.season && theme.season.name,
            sundayName: theme.season && theme.season.label,
          }),
        });
        const data = await resp.json();
        const reply = data.reply
          || (data.error ? 'Forgive me, the well is not yielding water just now.  Please try again in a moment.' : 'Peace to you.  Will you ask me again?');
        setMessages([...next, { role: 'assistant', content: reply }]);

        // Voice mode = conversational: speak the reply, then auto-start
        // listening again so the visitor can simply talk back without
        // tapping the microphone each turn.
        if (voiceMode) {
          await speakReply(reply);
          if (recogRef.current && !listening) {
            try { recogRef.current.start(); setListening(true); }
            catch (e) { /* already started, or unsupported */ }
          }
        }
      } catch (e) {
        setMessages([...next, { role: 'assistant', content: 'I could not reach the well.  Please try again in a moment.' }]);
      } finally {
        setThinking(false);
        setTimeout(() => inputRef.current && inputRef.current.focus(), 50);
      }
    };
    // Keep a stable reference to the latest send() so the speech
    // recognition handler (set up once on mount) can call it.
    sendRef.current = send;

    // ── Speech recognition (voice input) ─────────────────────────
    // In voice mode, transcripts auto-send so the visitor can have a
    // hands-free conversation. Outside voice mode, they're inserted
    // into the text field for the visitor to review before sending.
    React.useEffect(() => {
      const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
      if (!SR) return;
      const r = new SR();
      r.continuous = false; r.lang = 'en-GB'; r.interimResults = false;
      r.onresult = (e) => {
        const transcript = Array.from(e.results).map(r => r[0].transcript).join(' ').trim();
        if (!transcript) return;
        if (voiceModeRef.current) {
          // Conversational mode: send immediately
          setInput(transcript);
          setTimeout(() => sendRef.current && sendRef.current(), 60);
        } else {
          setInput(prev => (prev ? prev + ' ' : '') + transcript);
        }
      };
      r.onend = () => setListening(false);
      r.onerror = () => setListening(false);
      recogRef.current = r;
    }, []);
    const toggleListening = () => {
      if (!recogRef.current) return;
      if (listening) { recogRef.current.stop(); }
      else { try { recogRef.current.start(); setListening(true); } catch (e) {} }
    };

    // Track the *visible* viewport height. iOS Safari's `100vh` includes
    // the area behind the on-screen keyboard, so the panel appears to
    // "grow" when the keyboard pops up — but really the content below is
    // pushed off-screen. We read window.visualViewport.height and clamp
    // the panel to it.
    const [vvHeight, setVvHeight] = React.useState(() =>
      (window.visualViewport && window.visualViewport.height) || window.innerHeight
    );
    React.useEffect(() => {
      if (!window.visualViewport) return;
      const onResize = () => setVvHeight(window.visualViewport.height);
      window.visualViewport.addEventListener('resize', onResize);
      window.visualViewport.addEventListener('scroll', onResize);
      return () => {
        window.visualViewport.removeEventListener('resize', onResize);
        window.visualViewport.removeEventListener('scroll', onResize);
      };
    }, []);

    const isPhone = window.innerWidth < 480;
    // Reserve vertical space for chrome (60 top, 80 bottom for tab nav)
    const phoneHeight = Math.max(360, vvHeight - 140);

    return (
      <div style={{
        position: 'fixed',
        right:  'calc(8px + env(safe-area-inset-right))',
        left:   isPhone ? 'calc(8px + env(safe-area-inset-left))' : 'auto',
        bottom: 'calc(68px + env(safe-area-inset-bottom))',
        width:  isPhone ? 'auto' : 'min(380px, calc(100vw - 24px))',
        height: isPhone ? phoneHeight : Math.min(540, vvHeight - 200),
        background: theme.paper,
        border: `1px solid ${theme.rule}`,
        boxShadow: '0 8px 30px rgba(60,40,20,0.25)',
        display: 'flex', flexDirection: 'column',
        zIndex: 96, fontFamily: theme.fonts.body,
        animation: 'aelredOpen 0.3s ease',
        overflow: 'hidden',
      }}>
        <style>{`
          @keyframes aelredOpen {
            from { opacity: 0; transform: translateY(8px); }
            to   { opacity: 1; transform: translateY(0); }
          }
        `}</style>

        {/* Header */}
        <div style={{
          padding: '12px 14px',
          background: theme.paper,
          borderBottom: `1px solid ${theme.rule}`,
          display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <div style={{ color: theme.accent, fontFamily: theme.fonts.display, fontSize: 22, lineHeight: 1 }}>†</div>
          <div style={{ flex: 1 }}>
            <div style={{ fontFamily: theme.fonts.display, fontSize: 17, fontStyle: 'italic', color: theme.ink, lineHeight: 1.1 }}>
              Brother Aelred
            </div>
            <div style={{ ...smallCaps(theme, 8), color: theme.inkMute, marginTop: 2 }}>
              the porter at the gate
            </div>
          </div>
          {/* Voice toggle — large enough to read state at a glance */}
          <button onClick={handleVoiceToggle}
            aria-label={voiceMode ? 'Turn voice mode off' : 'Turn voice mode on'}
            style={{
              background: voiceMode ? theme.gold : 'transparent',
              border: `1px solid ${voiceMode ? theme.gold : theme.rule}`,
              color: voiceMode ? theme.paper : theme.inkMute,
              padding: '6px 10px', cursor: 'pointer',
              fontFamily: theme.fonts.mono, fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
            }}>
            {voiceMode ? '♪ voice on' : '♪ voice'}
          </button>
          <button onClick={onClose} aria-label="Close" style={{
            background: 'transparent', border: 'none', cursor: 'pointer',
            color: theme.inkMute, fontFamily: theme.fonts.mono, fontSize: 16, padding: 0, marginLeft: 4,
          }}>×</button>
        </div>

        {/* Voice mode status strip — large, unmistakable when on. Tells
            the visitor what's happening right now. */}
        {voiceMode && (
          <div style={{
            padding: '10px 14px',
            background: speaking ? theme.gold + '33' : (listening ? theme.accent + '33' : theme.bgAlt),
            borderBottom: `1px solid ${theme.rule}`,
            display: 'flex', alignItems: 'center', gap: 10,
            fontFamily: theme.fonts.mono, fontSize: 11,
            letterSpacing: '0.16em', textTransform: 'uppercase',
            color: speaking ? theme.gold : (listening ? theme.accent : theme.inkMute),
          }}>
            <span style={{
              width: 10, height: 10, borderRadius: '50%',
              background: speaking ? theme.gold : (listening ? theme.accent : theme.inkMute),
              animation: (speaking || listening) ? 'aelredPulse 1.4s ease-in-out infinite' : 'none',
              flexShrink: 0,
            }}/>
            <span style={{ flex: 1 }}>
              {speaking  ? 'Brother Aelred is speaking…'
               : listening ? 'Listening — speak when you’re ready'
               : 'Voice mode on — tap the microphone to speak'}
            </span>
            <style>{`
              @keyframes aelredPulse {
                0%, 100% { opacity: 1.0; transform: scale(1.0); }
                50%      { opacity: 0.5; transform: scale(1.3); }
              }
            `}</style>
          </div>
        )}

        {/* Always-visible "Talk to Pastor" escape hatch */}
        <button onClick={() => {
          setInput('I would like to talk to Pastor Jonathan directly.');
          setTimeout(() => send(), 50);
        }} style={{
          padding: '8px 14px',
          background: theme.bgAlt,
          border: 'none', borderBottom: `1px solid ${theme.rule}`,
          cursor: 'pointer', textAlign: 'left',
          fontFamily: theme.fonts.body, fontStyle: 'italic',
          fontSize: 12, color: theme.accent,
        }}>
          → Talk to Pastor Jonathan instead
        </button>

        {/* Conversation */}
        <div ref={scrollRef} style={{
          flex: 1, padding: '14px 14px 8px', overflowY: 'auto',
          fontFamily: theme.fonts.body,
        }}>
          {messages.map((m, i) => (
            <div key={i} style={{
              marginBottom: 12,
              display: 'flex',
              justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start',
            }}>
              <div style={{
                maxWidth: '85%',
                padding: '8px 12px',
                background: m.role === 'user' ? theme.bgAlt : 'transparent',
                border: m.role === 'user' ? `1px solid ${theme.rule}` : 'none',
                fontSize: 14, lineHeight: 1.5, color: theme.ink,
                fontStyle: m.role === 'assistant' ? 'italic' : 'normal',
                fontFamily: m.role === 'assistant' ? theme.fonts.display : theme.fonts.body,
                whiteSpace: 'pre-wrap',
              }}>
                {m.content}
              </div>
            </div>
          ))}
          {thinking && (
            <div style={{
              display: 'flex', alignItems: 'center', gap: 10,
              fontFamily: theme.fonts.display, fontStyle: 'italic',
              fontSize: 14, color: theme.inkMute, marginLeft: 4, marginTop: 8,
            }}>
              {/* A small candle flickering — the porter consulting his
                  books by candlelight while he composes his reply. The
                  flame animates entirely in SVG (no CSS keyframes
                  required) so it runs even on devices with reduce-motion
                  preferences turned on for the global ornament. */}
              <svg width="14" height="22" viewBox="0 0 20 30" aria-hidden="true">
                {/* candle body */}
                <rect x="7.5" y="14" width="5" height="14"
                      fill={theme.paper} stroke={theme.inkMute} strokeWidth="0.5"/>
                {/* wick */}
                <line x1="10" y1="14" x2="10" y2="11.5" stroke={theme.inkMute} strokeWidth="0.7"/>
                {/* outer flame — warm orange/gold, flickering */}
                <ellipse cx="10" cy="8" rx="2.2" ry="4" fill="#f4a83d" opacity="0.9">
                  <animate attributeName="opacity"
                           values="0.85;1.0;0.7;0.95;0.88;1.0;0.85"
                           dur="1.6s" repeatCount="indefinite"/>
                  <animate attributeName="ry"
                           values="4.0;4.3;3.6;4.1;3.8;4.2;4.0"
                           dur="1.6s" repeatCount="indefinite"/>
                  <animate attributeName="cx"
                           values="10;10.2;9.8;10.1;9.9;10"
                           dur="1.6s" repeatCount="indefinite"/>
                </ellipse>
                {/* inner flame — pale cream core */}
                <ellipse cx="10" cy="9" rx="0.9" ry="2.4" fill="#fff5d8">
                  <animate attributeName="opacity"
                           values="0.8;1.0;0.6;0.95;0.8"
                           dur="1.3s" repeatCount="indefinite"/>
                  <animate attributeName="cx"
                           values="10;10.1;9.9;10.2;10"
                           dur="1.3s" repeatCount="indefinite"/>
                </ellipse>
                {/* faint warm glow behind the flame */}
                <circle cx="10" cy="9" r="6" fill="#f4a83d" opacity="0.10"/>
              </svg>

              <span>Brother Aelred is considering<span style={{ animation: 'aelredEllipsis 1.4s infinite' }}>…</span></span>

              <style>{`
                @keyframes aelredEllipsis {
                  0%, 100% { opacity: 0.4; }
                  50%      { opacity: 1.0; }
                }
              `}</style>
            </div>
          )}
        </div>

        {/* Input */}
        <div style={{
          padding: '8px 10px',
          borderTop: `1px solid ${theme.rule}`,
          display: 'flex', gap: 6, alignItems: 'center',
        }}>
          {recogRef.current && (
            <button onClick={toggleListening}
              aria-label={listening ? 'Stop listening' : 'Speak'}
              title={listening ? 'Tap to stop' : 'Tap to speak'}
              style={{
                padding: '8px 12px',
                background: listening ? theme.accent : (voiceMode ? theme.gold + '33' : 'transparent'),
                color: listening ? theme.paper : (voiceMode ? theme.gold : theme.inkSoft),
                border: `1px solid ${listening ? theme.accent : (voiceMode ? theme.gold : theme.rule)}`,
                cursor: 'pointer', fontFamily: theme.fonts.mono, fontSize: 14,
                minWidth: 40,
                animation: (voiceMode && !listening && !speaking) ? 'aelredPulse 2s ease-in-out infinite' : 'none',
              }}>
              {listening ? '● rec' : '🎤'}
            </button>
          )}
          <input
            ref={inputRef}
            value={input}
            onChange={e => setInput(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') send(); }}
            placeholder="Ask Brother Aelred…"
            style={{
              flex: 1, padding: '8px 10px',
              background: theme.bgAlt,
              border: `1px solid ${theme.rule}`,
              fontFamily: theme.fonts.body,
              // iOS Safari auto-zooms in on inputs below 16px. Keeping it
              // at exactly 16 prevents the whole chat panel from bloating
              // when the input gets focused.
              fontSize: 16,
              color: theme.ink,
              outline: 'none',
            }}
          />
          <button onClick={send} disabled={!input.trim() || thinking} style={{
            padding: '8px 12px',
            background: (!input.trim() || thinking) ? theme.inkMute : theme.ink,
            color: theme.paper, border: 'none',
            fontFamily: theme.fonts.mono, fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase',
            cursor: (!input.trim() || thinking) ? 'not-allowed' : 'pointer',
          }}>Send</button>
        </div>

        {/* Disclosure footer */}
        <div style={{
          padding: '6px 14px 8px',
          fontFamily: theme.fonts.body, fontStyle: 'italic',
          fontSize: 10, color: theme.inkMute, textAlign: 'center',
          borderTop: `1px solid ${theme.ruleSoft || theme.rule}`,
        }}>
          An AI servant of this church. Not human, not the pastor.
        </div>
      </div>
    );
  }

  // ── Wrapper that holds open/closed state ───────────────────────
  function BrotherAelred({ theme }) {
    const [open, setOpen] = React.useState(false);
    return (
      <>
        <AelredAffordance theme={theme} open={open} onOpen={() => setOpen(true)} />
        {open && <AelredChat theme={theme} onClose={() => setOpen(false)} />}
      </>
    );
  }

  Object.assign(window, { BrotherAelred });
})();
