// Sam — Colleen's single-screen voice COS.
// Realtime v1: Max-style WebRTC transport with OpenAI Ash voice.

const CL_TWEAK_DEFAULTS = {
  mood: 'auto',
  motion: 1.0,
  intensity: 1.0,
  showCaptions: true,
};

function resolveMood(mood) {
  if (mood !== 'auto') return mood;
  const h = new Date().getHours();
  return (h >= 7 && h < 17) ? 'midday' : 'sunset';
}

const SAM_TOOL_DEFINITIONS = [
  {
    type: 'function',
    name: 'sam_remember',
    description: 'Save something Colleen wants Sam to remember.',
    parameters: {
      type: 'object',
      properties: {
        text: { type: 'string' },
        category: { type: 'string', enum: ['general', 'family', 'friends', 'health', 'kids', 'household', 'preference', 'password_hint'] },
        sensitivity: { type: 'string', enum: ['normal', 'private', 'sensitive'] },
      },
      required: ['text'],
    },
  },
  {
    type: 'function',
    name: 'sam_recall',
    description: 'Search Sam memory for something Colleen previously saved.',
    parameters: {
      type: 'object',
      properties: { query: { type: 'string' } },
    },
  },
  {
    type: 'function',
    name: 'sam_log_medication',
    description: 'Log medication information Colleen tells Sam. Do not give dosage advice.',
    parameters: {
      type: 'object',
      properties: {
        medication: { type: 'string' },
        amount: { type: 'string' },
        taken_at: { type: 'string', description: 'ISO date/time if known.' },
        note: { type: 'string' },
      },
    },
  },
  {
    type: 'function',
    name: 'sam_medication_log',
    description: 'Retrieve recent medication logs.',
    parameters: {
      type: 'object',
      properties: { days: { type: 'number' } },
    },
  },
  {
    type: 'function',
    name: 'sam_daily_briefing',
    description: 'Recall Colleen daily briefing that Sam compiled, optionally searching for a specific story or phrase.',
    parameters: {
      type: 'object',
      properties: {
        date: { type: 'string', description: 'YYYY-MM-DD. Omit for latest briefing.' },
        query: { type: 'string', description: 'Story, topic, or phrase Colleen wants to hear more about.' },
      },
    },
  },
  {
    type: 'function',
    name: 'sam_contact_max',
    description: 'Hand off a request from Colleen/Sam to Max, Randy chief of staff. Use for Randy reminders, schedule requests involving Randy, support/fix requests, and household tasks Max may handle.',
    parameters: {
      type: 'object',
      properties: {
        title: { type: 'string' },
        message: { type: 'string' },
        request_type: { type: 'string', enum: ['message_for_randy', 'calendar_for_randy', 'support_request', 'household_request', 'task_for_max', 'quiet_safety_backup'] },
        urgency: { type: 'string', enum: ['low', 'normal', 'timely', 'urgent'] },
      },
      required: ['message'],
    },
  },
  {
    type: 'function',
    name: 'sam_calendar_list',
    description: 'List Colleen calendar events.',
    parameters: {
      type: 'object',
      properties: {
        days: { type: 'number' },
        max_results: { type: 'number' },
      },
    },
  },
  {
    type: 'function',
    name: 'sam_calendar_create',
    description: 'Create an event on Colleen calendar and optionally invite attendees.',
    parameters: {
      type: 'object',
      properties: {
        summary: { type: 'string' },
        description: { type: 'string' },
        start: { type: 'string', description: 'ISO date/time.' },
        end: { type: 'string', description: 'ISO date/time.' },
        time_zone: { type: 'string' },
        attendees: { type: 'array', items: { type: 'string' } },
      },
      required: ['summary', 'start', 'end'],
    },
  },
  {
    type: 'function',
    name: 'sam_calendar_delete',
    description: 'Delete a calendar event by ID when Colleen asks to remove an event.',
    parameters: {
      type: 'object',
      properties: {
        id: { type: 'string' },
      },
      required: ['id'],
    },
  },
  {
    type: 'function',
    name: 'sam_gmail_search',
    description: 'Search Colleen Gmail for a short query.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        max_results: { type: 'number' },
      },
      required: ['query'],
    },
  },
  {
    type: 'function',
    name: 'sam_gmail_triage',
    description: 'Group cluttered Gmail messages by sender for Colleen to review before cleanup.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Gmail search query, such as is:unread older_than:30d.' },
        max_results: { type: 'number' },
        group_limit: { type: 'number' },
      },
    },
  },
  {
    type: 'function',
    name: 'sam_gmail_bulk_clean',
    description: 'Archive and/or mark read matching Gmail messages only after Colleen clearly confirms. Never deletes.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        mode: { type: 'string', enum: ['archive_mark_read', 'mark_read', 'archive'] },
        max_results: { type: 'number' },
        confirmed: { type: 'boolean' },
      },
      required: ['query', 'confirmed'],
    },
  },
  {
    type: 'function',
    name: 'sam_gmail_unsubscribe_candidates',
    description: 'Find senders with unsubscribe headers so Colleen can decide what to stop receiving.',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        max_results: { type: 'number' },
        group_limit: { type: 'number' },
      },
    },
  },
  {
    type: 'function',
    name: 'sam_gmail_send',
    description: 'Send an email from Colleen Gmail only after explicit confirmation.',
    parameters: {
      type: 'object',
      properties: {
        to: { type: 'string' },
        subject: { type: 'string' },
        body: { type: 'string' },
        confirmed: { type: 'boolean' },
      },
      required: ['to', 'subject', 'body', 'confirmed'],
    },
  },
];

function ColleenApp() {
  const [muted, setMuted] = React.useState(false);
  const [ceremony, setCeremony] = React.useState(true);
  const [localState, setLocalState] = React.useState('idle');
  const [lines, setLines] = React.useState([{ who: 'sam', text: CL_INTRO_REPLY }]);
  const [lastReply, setLastReply] = React.useState(CL_INTRO_REPLY);
  const pcRef = React.useRef(null);
  const dcRef = React.useRef(null);
  const micRef = React.useRef(null);
  const audioRef = React.useRef(null);
  const activeRef = React.useRef(false);
  const assistantDraftRef = React.useRef('');
  const toolCallsRef = React.useRef(new Set());
  const listenTimerRef = React.useRef(null);

  React.useEffect(() => {
    const tid = setTimeout(() => setCeremony(false), 1700);
    return () => clearTimeout(tid);
  }, []);

  function upsertLine(who, text) {
    if (!text) return;
    setLines(prev => {
      const next = prev.filter(l => !(l.who === who && l.live));
      return [...next, { who, text, live: true }].slice(-8);
    });
    if (who === 'sam') {
      setLastReply(text);
    }
  }

  function finalizeLine(who, text) {
    if (!text) return;
    setLines(prev => {
      const next = prev.filter(l => !(l.who === who && l.live));
      return [...next, { who, text }].slice(-8);
    });
    if (who === 'sam') setLastReply(text);
  }

  function setMicCapture(enabled) {
    const stream = micRef.current;
    if (!stream) return;
    stream.getAudioTracks().forEach(track => {
      track.enabled = Boolean(enabled) && !muted;
    });
  }

  function queueListening(delay = 900) {
    if (listenTimerRef.current) clearTimeout(listenTimerRef.current);
    listenTimerRef.current = setTimeout(() => {
      listenTimerRef.current = null;
      setMicCapture(true);
      if (activeRef.current) setLocalState(muted ? 'muted' : 'listening');
    }, delay);
  }

  function closeChannel() {
    activeRef.current = false;
    if (listenTimerRef.current) clearTimeout(listenTimerRef.current);
    listenTimerRef.current = null;
    try { dcRef.current?.close(); } catch {}
    try { pcRef.current?.close(); } catch {}
    try { micRef.current?.getTracks().forEach(track => track.stop()); } catch {}
    if (audioRef.current) audioRef.current.srcObject = null;
    dcRef.current = null;
    pcRef.current = null;
    micRef.current = null;
    assistantDraftRef.current = '';
    toolCallsRef.current.clear();
    setMuted(false);
    setLocalState('idle');
  }

  async function authenticateIfNeeded() {
    const res = await fetch('/sam/start', { method: 'POST' });
    if (!res.ok) throw new Error('Sam could not open the private channel.');
  }

  async function getRealtimeToken() {
    let res = await fetch('/api/realtime/token?mode=best', { method: 'POST' });
    if (res.status === 401) {
      await authenticateIfNeeded();
      res = await fetch('/api/realtime/token?mode=best', { method: 'POST' });
    }
    const text = await res.text();
    if (!res.ok) {
      let message = 'Realtime token failed.';
      try { message = JSON.parse(text).error || message; } catch {}
      throw new Error(message);
    }
    const data = JSON.parse(text);
    const key = data.value || data.client_secret?.value;
    if (!key) throw new Error('Realtime token did not include a client secret.');
    return key;
  }

  function sendRealtime(event) {
    const dc = dcRef.current;
    if (!dc || dc.readyState !== 'open') return false;
    dc.send(JSON.stringify(event));
    return true;
  }

  function applySamSession() {
    sendRealtime({
      type: 'session.update',
      session: {
        type: 'realtime',
        instructions: SAM_REALTIME_INSTRUCTIONS,
        output_modalities: ['audio'],
        tools: SAM_TOOL_DEFINITIONS,
        tool_choice: 'auto',
        audio: {
          input: {
            turn_detection: {
              type: 'semantic_vad',
              eagerness: 'low',
              create_response: true,
              interrupt_response: false,
            },
          },
          output: {
            voice: 'ash',
            speed: 1.08,
          },
        },
      },
    });
  }

  async function runToolCall(name, rawArguments, callId) {
    if (!name || !callId || toolCallsRef.current.has(callId)) return;
    toolCallsRef.current.add(callId);
    setLocalState('thinking');
    try {
      const res = await fetch('/api/realtime/tool', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ name, arguments: rawArguments }),
      });
      const result = await res.json().catch(() => ({ ok: false, error: 'Tool returned invalid JSON.' }));
      sendRealtime({
        type: 'conversation.item.create',
        item: { type: 'function_call_output', call_id: callId, output: JSON.stringify(result) },
      });
      sendRealtime({ type: 'response.create' });
    } catch {
      sendRealtime({
        type: 'conversation.item.create',
        item: { type: 'function_call_output', call_id: callId, output: JSON.stringify({ ok: false, error: 'Tool failed.' }) },
      });
      sendRealtime({ type: 'response.create' });
    }
  }

  function inspectResponseForTools(response) {
    const output = response && Array.isArray(response.output) ? response.output : [];
    output.forEach(item => {
      if (item?.type === 'function_call') runToolCall(item.name, item.arguments, item.call_id);
    });
  }

  function handleRealtimeEvent(event) {
    switch (event.type) {
      case 'input_audio_buffer.speech_started':
        setLocalState(muted ? 'muted' : 'listening');
        break;
      case 'input_audio_buffer.speech_stopped':
        setLocalState('thinking');
        break;
      case 'conversation.item.input_audio_transcription.completed':
        if (event.transcript) finalizeLine('you', event.transcript);
        break;
      case 'response.created':
        assistantDraftRef.current = '';
        setMicCapture(false);
        setLocalState('thinking');
        break;
      case 'response.audio.delta':
        setLocalState('speaking');
        break;
      case 'response.audio_transcript.delta':
      case 'response.output_text.delta':
        if (event.delta) {
          assistantDraftRef.current += event.delta;
          upsertLine('sam', assistantDraftRef.current.trim());
        }
        break;
      case 'response.audio_transcript.done':
        if (event.transcript) {
          assistantDraftRef.current = event.transcript;
          finalizeLine('sam', event.transcript);
        }
        break;
      case 'response.function_call_arguments.done':
        runToolCall(event.name, event.arguments, event.call_id);
        break;
      case 'response.done':
        inspectResponseForTools(event.response);
        if (assistantDraftRef.current) finalizeLine('sam', assistantDraftRef.current.trim());
        if (activeRef.current) queueListening();
        break;
      case 'error':
        console.error('[sam-realtime-error]', event.error || event);
        finalizeLine('sam', 'That did not go through cleanly. I saved the conversation here so we do not lose it.');
        setLocalState('idle');
        break;
      default:
        break;
    }
  }

  async function openChannel() {
    if (activeRef.current) return;
    activeRef.current = true;
    setMuted(false);
    setLocalState('connecting');
    finalizeLine('sam', 'Hi Colleen. I\u2019m opening the channel now.');
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      micRef.current = stream;

      const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
      pcRef.current = pc;
      stream.getTracks().forEach(track => pc.addTrack(track, stream));

      pc.ontrack = (event) => {
        let audio = audioRef.current;
        if (!audio) {
          audio = document.createElement('audio');
          audio.autoplay = true;
          audio.playsInline = true;
          audio.style.display = 'none';
          document.body.appendChild(audio);
          audioRef.current = audio;
        }
        audio.srcObject = event.streams[0];
        audio.play().catch(() => {});
      };
      pc.onconnectionstatechange = () => {
        if (pc.connectionState === 'connected') {
          setLocalState(muted ? 'muted' : 'listening');
        }
        if (['failed', 'closed', 'disconnected'].includes(pc.connectionState) && activeRef.current) {
          closeChannel();
        }
      };

      const dc = pc.createDataChannel('oai-events');
      dcRef.current = dc;
      dc.addEventListener('message', message => {
        try { handleRealtimeEvent(JSON.parse(message.data)); } catch {}
      });
      dc.addEventListener('open', () => {
        applySamSession();
        setLocalState(muted ? 'muted' : 'listening');
      });

      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);
      const token = await getRealtimeToken();
      const res = await fetch('https://api.openai.com/v1/realtime/calls', {
        method: 'POST',
        headers: {
          authorization: `Bearer ${token}`,
          'content-type': 'application/sdp',
        },
        body: pc.localDescription?.sdp || offer.sdp,
      });
      const answerSdp = await res.text();
      if (!res.ok) throw new Error('Realtime session failed.');
      await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
    } catch (error) {
      console.error('[sam-open-channel]', error);
      activeRef.current = false;
      try { micRef.current?.getTracks().forEach(track => track.stop()); } catch {}
      micRef.current = null;
      finalizeLine('sam', error?.message || 'Sam did not open cleanly.');
      setLocalState('idle');
    }
  }

  React.useEffect(() => {
    const reset = () => {
      closeChannel();
      setLines([{ who: 'sam', text: CL_INTRO_REPLY }]);
      setLastReply(CL_INTRO_REPLY);
    };
    window.addEventListener('sam-new-chat', reset);
    return () => window.removeEventListener('sam-new-chat', reset);
  }, []);

  React.useEffect(() => {
    setMicCapture(!muted && localState === 'listening');
  }, [muted, localState]);

  const toggle = () => {
    if (localState === 'offline' || localState === 'idle') openChannel();
    else closeChannel();
  };

  const replay = () => {
    if (!lastReply || !dcRef.current || dcRef.current.readyState !== 'open') return;
    sendRealtime({
      type: 'conversation.item.create',
      item: {
        type: 'message',
        role: 'user',
        content: [{ type: 'input_text', text: `Please repeat this exactly in your voice: ${lastReply}` }],
      },
    });
    sendRealtime({ type: 'response.create' });
    setLocalState('thinking');
  };

  const state = muted && localState !== 'idle' ? 'muted' : localState;
  const mood = resolveMood(CL_TWEAK_DEFAULTS.mood);
  const showCaptionsNow = CL_TWEAK_DEFAULTS.showCaptions && localState !== 'idle' && lines.length > 0;
  const showTaglineNow = state === 'idle' || state === 'offline';

  return (
    <div style={{
      position: 'fixed', inset: 0, display: 'flex',
      alignItems: 'center', justifyContent: 'center', background: '#06201f',
    }}>
      <IOSDevice width={390} height={844} dark>
        <div style={{ position: 'relative', width: '100%', height: '100%', background: '#06201f', overflow: 'hidden' }}>
          <SeaGlassOrb state={state} intensity={CL_TWEAK_DEFAULTS.intensity} motionLevel={CL_TWEAK_DEFAULTS.motion} mood={mood} ceremony={ceremony} />
          <ColleenHeader state={state} />
          {showCaptionsNow && <ColleenCaptions state={state} lines={lines} onReplay={state === 'speaking' ? replay : null} />}
          <ColleenTagline visible={showTaglineNow} />
          <ColleenDock state={state} onToggleState={toggle} muted={muted} onMute={() => setMuted(x => !x)} />
        </div>
      </IOSDevice>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<ColleenApp />);
