<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { apiGet, transcribeAudio } from '@/api/client'
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
import { useVad } from '@/composables/useVad'
import { useStreamingTts } from '@/composables/useStreamingTts'
import { useVoiceAudio, setVoiceVolume } from '@/composables/useVoiceAudio'
import { useListenMode } from '@/composables/useListenMode'
import { useChatStore } from '@/stores/chat'
import { useSettingsStore } from '@/stores/settings'
import { useToastStore } from '@/stores/toast'
import type { Note } from '@/types/note'
import {
  Paperclip,
  Volume2,
  VolumeX,
  Square,
  Mic,
  Loader2,
  ArrowUp,
} from 'lucide-vue-next'

const props = withDefaults(defineProps<{
  /** Textarea placeholder */
  placeholder?: string
  /** When true, hides the note picker (briefing mode) */
  briefingMode?: boolean
  /** Pill shape — compact rounded style for widget */
  pill?: boolean
}>(), {
  placeholder: 'Type a message… (Enter to send, Shift+Enter for new line)',
  briefingMode: false,
  pill: false,
})

const emit = defineEmits<{
  submit: [payload: { content: string; contextNoteId?: number }]
  abort: []
}>()

const store = useChatStore()
const settingsStore = useSettingsStore()
const voiceEnabled = computed(() => settingsStore.voiceSttReady)
const voiceTtsEnabled = computed(() => settingsStore.voiceTtsReady)

// ── Streaming TTS (listen mode) ───────────────────────────────────────────────
const listenMode = useListenMode()
const audio = useVoiceAudio()
const speakerPopoverOpen = ref(false)

const tts = useStreamingTts({
  streamingContent: computed(() => store.streamingContent),
  streaming: computed(() => store.streaming),
  enabled: computed(() => listenMode.value && voiceTtsEnabled.value),
})

function lastAssistantContent(): string {
  const msgs = store.currentConversation?.messages ?? []
  return [...msgs].reverse().find((m) => m.role === 'assistant')?.content ?? ''
}

function toggleListen() {
  listenMode.value = !listenMode.value
  if (listenMode.value) {
    tts.speak(lastAssistantContent())
  } else {
    tts.stop()
  }
}

// ── Core input ────────────────────────────────────────────────────────────────
const messageInput = ref('')
const inputEl = ref<HTMLTextAreaElement | null>(null)
const wrapperEl = ref<HTMLElement | null>(null)

function autoResize() {
  const el = inputEl.value
  if (!el) return
  el.style.height = 'auto'
  el.style.height = Math.min(el.scrollHeight, 150) + 'px'
}

function resetTextareaHeight() {
  const el = inputEl.value
  if (!el) return
  el.style.height = 'auto'
}

function onInputKeydown(e: KeyboardEvent) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    onSubmit()
  }
}

function onSubmit() {
  const content = messageInput.value.trim()
  if (!content) return
  emit('submit', { content, contextNoteId: attachedNote.value?.id })
  messageInput.value = ''
  attachedNote.value = null
  resetTextareaHeight()
}

// ── Note picker ───────────────────────────────────────────────────────────────
const attachedNote = ref<{ id: number; title: string } | null>(null)
const showNotePicker = ref(false)
const noteSearchQuery = ref('')
const noteSearchResults = ref<{ id: number; title: string }[]>([])
const noteSearchLoading = ref(false)
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null

function toggleNotePicker() {
  showNotePicker.value = !showNotePicker.value
  if (showNotePicker.value) {
    noteSearchQuery.value = ''
    noteSearchResults.value = []
    nextTick(() => {
      (wrapperEl.value?.querySelector('.note-picker-search') as HTMLInputElement)?.focus()
    })
  }
}

function onNoteSearchInput() {
  if (noteSearchTimer) clearTimeout(noteSearchTimer)
  noteSearchTimer = setTimeout(async () => {
    const q = noteSearchQuery.value.trim()
    if (!q) { noteSearchResults.value = []; return }
    noteSearchLoading.value = true
    try {
      const data = await apiGet<{ notes: Note[] }>(`/api/notes?q=${encodeURIComponent(q)}&all=true&limit=5`)
      noteSearchResults.value = data.notes.map((n) => ({ id: n.id, title: n.title }))
    } catch {
      noteSearchResults.value = []
    } finally {
      noteSearchLoading.value = false
    }
  }, 250)
}

function selectNote(note: { id: number; title: string }) {
  attachedNote.value = note
  showNotePicker.value = false
}

function removeAttachedNote() {
  attachedNote.value = null
}

// ── Voice (click-to-toggle + VAD speech detection) ─────────────────────────
const transcribingVoice = ref(false)
const recorder = useVoiceRecorder()
// Tracks whether VAD detected speech during the current recording session.
// Used to skip the Whisper round-trip when the user manually stops without
// ever speaking — the recording is ambient noise and will transcribe to "".
const vadSawSpeech = ref(false)

const vad = useVad({
  onSpeechEnd: () => {
    // VAD auto-stop: speech was detected and the user has paused. Proceed
    // to transcribe the MediaRecorder output.
    void stopRecording(false)
  },
  onNoSpeech: () => {
    useToastStore().show('No speech detected', 'warning')
  },
  onVadError: (msg) => {
    useToastStore().show(`VAD failed: ${msg}`, 'error')
  },
})

// Live mic halo. A solid red disc sits behind the mic button and scales
// with `vad.amplitude` (0..1 RMS, already amplified for visibility).
const micGlowStyle = computed(() => {
  if (!recorder.recording.value) return { display: 'none' }
  const pulse = 0.2 + Math.min(vad.amplitude.value, 1) * 0.8
  return {
    transform: `translate(-50%, -50%) scale(${1 + pulse * 1.6})`,
    opacity: String(0.45 + pulse * 0.4),
  }
})

// vad.speaking flips true on speech-start. Watch it once per session to
// capture that speech was detected at some point, without clearing when
// speech ends. This drives the no-speech guard in stopRecording.
watch(() => vad.speaking.value, (on) => {
  if (on) vadSawSpeech.value = true
})

async function toggleVoice() {
  if (transcribingVoice.value) return
  if (recorder.recording.value) {
    await stopRecording(true)
  } else {
    await startRecording()
  }
}

async function startRecording() {
  if (!voiceEnabled.value || recorder.recording.value) return
  if (!recorder.isSupported) {
    useToastStore().show('Microphone requires HTTPS or localhost', 'error')
    return
  }
  vadSawSpeech.value = false
  await recorder.startRecording()
  if (recorder.error.value) {
    useToastStore().show(recorder.error.value, 'error')
    return
  }
  if (recorder.stream.value) {
    await vad.start(recorder.stream.value)
  }
}

async function stopRecording(manual: boolean) {
  if (manual) {
    await vad.stopAndCheck()
  } else {
    await vad.stop()
  }
  if (!recorder.recording.value) return
  transcribingVoice.value = true
  try {
    const blob = await recorder.stopRecording()
    // No-speech guard: user manually stopped without VAD seeing speech.
    // Skip Whisper — the toast was already shown by onNoSpeech.
    if (manual && !vadSawSpeech.value) {
      return
    }
    // Pass last assistant message as context to reduce STT mishearings
    const msgs = store.currentConversation?.messages ?? []
    const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')?.content
    const { transcript } = await transcribeAudio(blob, lastAssistant)
    if (transcript.trim()) {
      messageInput.value = transcript.trim()
      await nextTick()
      autoResize()
      onSubmit()
    }
  } catch { /* transcription failed silently */ }
  finally { transcribingVoice.value = false }
}

// ── Click-outside to dismiss speaker popover ──────────────────────────────────
function onDocumentMousedown(e: MouseEvent) {
  if (!speakerPopoverOpen.value) return
  const target = e.target as HTMLElement | null
  if (!target?.closest('.speaker-wrapper')) speakerPopoverOpen.value = false
}
onMounted(() => document.addEventListener('mousedown', onDocumentMousedown))
onUnmounted(() => document.removeEventListener('mousedown', onDocumentMousedown))

// ── Exposed interface ─────────────────────────────────────────────────────────
function focus() {
  inputEl.value?.focus()
}

function prefill(text: string) {
  messageInput.value = text
  nextTick(() => {
    autoResize()
    inputEl.value?.focus()
  })
}

defineExpose({ focus, prefill })
</script>

<template>
  <div ref="wrapperEl" class="chat-input-bar" :class="{ 'chat-input-bar--pill': pill }">
    <!-- Attached note pill -->
    <div v-if="attachedNote" class="attached-note">
      <span class="attached-note-pill">
        {{ attachedNote.title }}
        <button class="attached-note-remove" aria-label="Remove" @click="removeAttachedNote">&times;</button>
      </span>
    </div>

    <div class="input-row">
      <!-- Note picker -->
      <div class="note-picker-wrapper">
        <button
          class="btn-icon"
          @click="toggleNotePicker"
          :disabled="!store.chatReady"
          title="Attach a note"
        >
          <Paperclip :size="16" />
        </button>
        <div v-if="showNotePicker" class="note-picker-dropdown">
          <input
            class="note-picker-search"
            v-model="noteSearchQuery"
            @input="onNoteSearchInput"
            placeholder="Search notes..."
          />
          <div class="note-picker-results">
            <div
              v-for="note in noteSearchResults"
              :key="note.id"
              class="note-picker-item"
              @click="selectNote(note)"
            >{{ note.title || 'Untitled' }}</div>
            <div v-if="noteSearchLoading" class="note-picker-empty">Searching...</div>
            <div v-else-if="noteSearchQuery && !noteSearchResults.length" class="note-picker-empty">No notes found</div>
          </div>
        </div>
      </div>

      <!-- Textarea -->
      <textarea
        ref="inputEl"
        v-model="messageInput"
        @keydown="onInputKeydown"
        @input="autoResize"
        :placeholder="!store.chatReady ? 'Chat unavailable' : store.streaming ? 'Type to queue… (Enter to queue)' : placeholder"
        :disabled="!store.chatReady"
        rows="1"
        class="input-textarea"
      ></textarea>

      <!-- Speaker / listen-mode popover -->
      <div v-if="voiceTtsEnabled" class="speaker-wrapper">
        <button
          class="btn-icon btn-speaker"
          :class="{ 'speaker-active': listenMode, 'speaker-busy': tts.speaking.value }"
          @click="speakerPopoverOpen = !speakerPopoverOpen"
          :title="listenMode ? 'Listen mode on' : 'Listen / volume'"
          aria-label="Listen and volume settings"
        >
          <Volume2 v-if="!tts.speaking.value" :size="16" />
          <VolumeX v-else :size="16" />
        </button>
        <div v-if="speakerPopoverOpen" class="speaker-popover">
          <button
            class="speaker-row speaker-row--toggle"
            :class="{ 'speaker-row--active': listenMode }"
            @click="toggleListen"
          >
            <span class="speaker-row-label">{{ listenMode ? 'Listening' : 'Read aloud' }}</span>
            <span class="speaker-switch" :class="{ on: listenMode }"></span>
          </button>
          <div class="speaker-row">
            <span class="speaker-row-label">Volume</span>
            <input
              type="range" min="0" max="1" step="0.05"
              :value="audio.volume.value"
              @input="setVoiceVolume(+($event.target as HTMLInputElement).value)"
              class="speaker-volume-range"
              aria-label="Volume"
            />
            <span class="speaker-volume-pct">{{ Math.round(audio.volume.value * 100) }}%</span>
          </div>
          <button
            v-if="tts.speaking.value"
            class="speaker-row speaker-row--stop"
            @click="tts.stop()"
          >
            <Square :size="16" fill="currentColor" />
            <span>Stop playback</span>
          </button>
        </div>
      </div>

      <!-- PTT mic (with live amplitude halo behind) -->
      <div v-if="voiceEnabled" class="mic-wrapper">
        <div class="mic-glow" :style="micGlowStyle" aria-hidden="true"></div>
        <button
          class="btn-icon btn-mic"
          :class="{ 'mic-recording': recorder.recording.value, 'mic-transcribing': transcribingVoice }"
          @click.prevent="toggleVoice"
          :disabled="transcribingVoice || !store.chatReady"
          :title="recorder.recording.value ? 'Click to stop (or wait for silence)' : 'Click to speak'"
          :aria-label="recorder.recording.value ? 'Stop recording' : 'Start recording'"
        >
        <Mic v-if="!transcribingVoice" :size="16" />
        <Loader2 v-else :size="16" class="mic-loader" />
        </button>
      </div>

      <!-- Abort (streaming) or Send -->
      <button
        v-if="store.streaming"
        class="btn-abort-inline"
        @click="emit('abort')"
        title="Stop generation"
      >
        <Square :size="16" fill="currentColor" />
      </button>
      <button
        v-else
        class="btn-send"
        @click="onSubmit"
        :disabled="!messageInput.trim() || !store.chatReady"
      ><ArrowUp :size="16" /></button>
    </div>
  </div>
</template>

<style scoped>
.chat-input-bar {
  width: 100%;
}

.attached-note {
  padding: 0.25rem 0.5rem;
}
.attached-note-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  background: var(--color-primary);
  color: #fff;
  border-radius: 10px;
  padding: 0.15rem 0.4rem;
  font-size: 0.75rem;
}
.attached-note-remove {
  background: none;
  border: none;
  color: rgba(255, 255, 255, 0.7);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
  padding: 0 0.1rem;
}
.attached-note-remove:hover { color: #fff; }

.input-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.5rem 0.5rem 0.75rem;
  background: var(--color-input-bar-bg);
  border-radius: 12px;
  box-shadow: 0 2px 8px var(--color-shadow);
}

.chat-input-bar--pill .input-row {
  border-radius: 24px;
  padding: 0.6rem 0.6rem 0.6rem 1rem;
}

.input-textarea {
  flex: 1;
  resize: none;
  padding: 0.35rem 0.5rem;
  border: none;
  background: transparent;
  color: var(--color-input-bar-text);
  outline: none;
  font-family: inherit;
  font-size: 0.9rem;
  max-height: 150px;
  overflow-y: auto;
}
.input-textarea::placeholder { color: var(--color-input-bar-placeholder); }
.input-textarea:disabled { opacity: 0.5; }

.btn-icon {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-input-bar-text);
  opacity: 0.6;
  padding: 0.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.btn-icon:hover { opacity: 1; }
.btn-icon:disabled { opacity: 0.3; cursor: default; }

.btn-mic.mic-recording {
  opacity: 1;
  /* White icon sits on top of the red halo so it stays legible at any
     pulse size. */
  color: #fff;
  position: relative;
  z-index: 1;
}
.btn-mic.mic-transcribing { opacity: 0.5; }
.mic-loader { animation: mic-spin 1s linear infinite; }
@keyframes mic-spin { to { transform: rotate(360deg); } }

/* Mic wrapper + live amplitude halo. The glow is a filled red disc
   absolutely positioned behind the button, scaled/faded by the
   computed `micGlowStyle` so the user gets unmistakable feedback
   that their voice is being picked up while the mic icon itself
   stays put and readable on top. */
.mic-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.mic-glow {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(239, 68, 68, 0.9) 0%,
    rgba(239, 68, 68, 0.6) 55%,
    rgba(239, 68, 68, 0) 100%
  );
  transform: translate(-50%, -50%) scale(1);
  transform-origin: center;
  pointer-events: none;
  /* Smooth between amplitude samples (~100ms) so the pulse feels continuous
     rather than stepping. */
  transition: transform 0.12s ease-out, opacity 0.12s ease-out;
  z-index: 0;
}

.note-picker-wrapper { position: relative; }
.note-picker-dropdown {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  width: 260px;
  background: var(--color-bg-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 4px 16px var(--color-shadow);
  z-index: 20;
  overflow: hidden;
}
.note-picker-search {
  width: 100%;
  padding: 0.45rem 0.65rem;
  border: none;
  border-bottom: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text);
  font-size: 0.85rem;
  outline: none;
  font-family: inherit;
  box-sizing: border-box;
}
.note-picker-results { max-height: 180px; overflow-y: auto; }
.note-picker-item {
  padding: 0.4rem 0.65rem;
  cursor: pointer;
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.note-picker-item:hover { background: var(--color-bg-secondary); }
.note-picker-empty { padding: 0.4rem 0.65rem; color: var(--color-text-muted); font-size: 0.8rem; }

.btn-send {
  width: 30px;
  min-width: 30px;
  height: 30px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gradient-cta);
  color: #fff;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 1rem;
  flex-shrink: 0;
  transition: box-shadow 0.15s;
}
.btn-send:hover { box-shadow: 0 0 16px rgba(91, 74, 138, 0.35); }
.btn-send:disabled { opacity: 0.35; cursor: default; box-shadow: none; }

.btn-abort-inline {
  width: 28px;
  min-width: 28px;
  height: 28px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-border);
  border-radius: 50%;
  cursor: pointer;
  flex-shrink: 0;
}
.btn-abort-inline:hover { border-color: #ef4444; color: #ef4444; }

/* Speaker / listen-mode popover */
.speaker-wrapper { position: relative; display: inline-flex; flex-shrink: 0; }
.btn-speaker.speaker-active { opacity: 1; color: var(--color-primary); }
.btn-speaker.speaker-busy { opacity: 1; color: #f59e0b; }
.speaker-popover {
  position: absolute;
  bottom: calc(100% + 8px);
  right: 0;
  min-width: 220px;
  background: var(--color-bg-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 4px 16px var(--color-shadow);
  padding: 0.35rem;
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.speaker-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: default;
  text-align: left;
  width: 100%;
}
.speaker-row--toggle { cursor: pointer; }
.speaker-row--toggle:hover { background: var(--color-bg-secondary); }
.speaker-row--active { color: var(--color-primary); }
.speaker-row--stop {
  cursor: pointer;
  color: var(--color-text-muted);
  justify-content: flex-start;
}
.speaker-row--stop:hover { color: #ef4444; background: var(--color-bg-secondary); }
.speaker-row-label { flex: 1; }
.speaker-volume-range { flex: 1; min-width: 0; }
.speaker-volume-pct {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  min-width: 2.5rem;
  text-align: right;
}
.speaker-switch {
  position: relative;
  width: 28px;
  height: 16px;
  border-radius: 9999px;
  background: var(--color-border);
  transition: background 0.15s;
  flex-shrink: 0;
}
.speaker-switch::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-bg-card);
  transition: transform 0.15s;
}
.speaker-switch.on { background: var(--color-primary); }
.speaker-switch.on::after { transform: translateX(12px); }
</style>
