/**
 * public/voice.ts — Web Speech API wrapper for push-to-talk voice input (A2).
 *
 * Pure frontend module. Audio NEVER reaches this server — the browser's speech
 * engine handles it (Chrome sends audio to Google; see SEC-L2 / AC-A2.5).
 * The final transcript is delivered to the caller, who sends it as raw terminal
 * input via the existing WS input path (byte-shuttle unchanged).
 *
 * Usage:
 *   const voice = createVoiceInput((text) => sendToTerminal(text), { autoSend: true })
 *   if (!voice) { /* hide mic UI *\/ }
 *   micBtn.addEventListener('touchstart', () => voice.start())
 *   micBtn.addEventListener('touchend',   () => voice.stop())
 */

// ── Local SpeechRecognition interface (not in TypeScript 6.x DOM lib) ─────────

/** Minimal interface for the Web Speech recognition object. */
interface ISpeechRecognition {
  continuous: boolean
  interimResults: boolean
  lang: string
  onresult: ((event: ISpeechRecognitionEvent) => void) | null
  onend: (() => void) | null
  onerror: ((event: { error: string }) => void) | null
  start(): void
  stop(): void
  abort(): void
}

interface ISpeechRecognitionEvent {
  resultIndex: number
  results: ISpeechRecognitionResultList
}

interface ISpeechRecognitionResultList {
  length: number
  [index: number]: ISpeechRecognitionResult | undefined
}

interface ISpeechRecognitionResult {
  isFinal: boolean
  [index: number]: ISpeechRecognitionAlternative | undefined
}

interface ISpeechRecognitionAlternative {
  transcript: string
  confidence: number
}

type SpeechRecognitionCtor = new () => ISpeechRecognition

type SpeechRecognitionWindow = Window & {
  SpeechRecognition?: SpeechRecognitionCtor
  webkitSpeechRecognition?: SpeechRecognitionCtor
}

// ── Public API ────────────────────────────────────────────────────────────────

/** Options for createVoiceInput. */
export interface VoiceInputOptions {
  /** Called with intermediate (non-final) text while the user is still speaking. */
  onInterim?: (text: string) => void
  /**
   * When true, the final transcript is suffixed with \r (carriage return) so
   * Claude receives it immediately without the user pressing Enter (A2-FR3).
   * Default: false — insert text only; user presses Enter manually.
   */
  autoSend?: boolean
  /**
   * BCP-47 language tag, e.g. 'en-US', 'ja-JP'.
   * Default: navigator.language (the browser's UI language).
   */
  lang?: string
}

/**
 * A live voice recognition session.  Obtain via createVoiceInput().
 * Call dispose() when the owning component unmounts.
 */
export interface VoiceInput {
  /** Begin listening (no-op if already active or disposed). */
  start(): void
  /** Stop listening and emit the final result (no-op if not active). */
  stop(): void
  /** Abort and clean up; no callbacks fire after this call. */
  dispose(): void
  /** True while recognition is in progress. */
  isActive(): boolean
}

/** True if this browser supports the Web Speech API (standard or webkit prefix). */
export function isSpeechSupported(): boolean {
  if (typeof window === 'undefined') return false
  return 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window
}

/** Retrieve the SpeechRecognition constructor, handling the webkit prefix. */
function getSRConstructor(): SpeechRecognitionCtor | undefined {
  if (!isSpeechSupported()) return undefined
  const w = window as SpeechRecognitionWindow
  return w.SpeechRecognition ?? w.webkitSpeechRecognition
}

/**
 * Create a push-to-talk voice input session.
 *
 * Returns null when the browser does not support SpeechRecognition — the
 * caller should hide the mic UI in that case (AC-A2.3).
 *
 * @param onTranscript  Receives the final recognised text (+ '\r' if autoSend),
 *                       plus the top alternative's ASR confidence (0-1) as an
 *                       optional 2nd arg when the engine reports one (VC decision D).
 * @param opts          See VoiceInputOptions.
 */
export function createVoiceInput(
  onTranscript: (text: string, confidence?: number) => void,
  opts?: VoiceInputOptions,
): VoiceInput | null {
  const SRClass = getSRConstructor()
  if (!SRClass) return null

  const recognition = new SRClass()
  recognition.continuous = false
  recognition.interimResults = opts?.onInterim !== undefined
  recognition.lang = opts?.lang ?? navigator.language

  let active = false
  let disposed = false

  recognition.onresult = (event: ISpeechRecognitionEvent) => {
    if (disposed) return
    let interimText = ''
    let finalText = ''
    let finalConfidence: number | undefined
    for (let i = event.resultIndex; i < event.results.length; i++) {
      const result = event.results[i]
      const alt = result?.[0]
      const text = alt?.transcript ?? ''
      if (result?.isFinal) {
        finalText += text
        // VC (decision D): forward the top alternative's confidence so the
        // caller can gate risk-asymmetric decisions (e.g. approve). Still
        // transcription-only — no decision-making happens in this module.
        finalConfidence = alt?.confidence ?? finalConfidence
      } else {
        interimText += text
      }
    }
    if (interimText && opts?.onInterim) {
      opts.onInterim(interimText)
    }
    if (finalText) {
      onTranscript(opts?.autoSend ? finalText + '\r' : finalText, finalConfidence)
    }
  }

  recognition.onend = () => {
    active = false
  }

  recognition.onerror = () => {
    active = false
  }

  return {
    start(): void {
      if (disposed || active) return
      active = true
      recognition.start()
    },
    stop(): void {
      if (disposed || !active) return
      recognition.stop()
      active = false
    },
    dispose(): void {
      if (disposed) return
      disposed = true
      if (active) recognition.abort()
      active = false
    },
    isActive(): boolean {
      return active
    },
  }
}
