import { createFileRoute, Link } from "@tanstack/react-router";
import { Phone, PhoneOff } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Photo } from "@/components/site/look";
import { Frame } from "@/components/site/shell";
import { Button, TextArea } from "@/components/ui";
import { askConcierge, getPreferredLocale, getShell, speakVoice, transcribeVoice } from "@/lib/clinic/api";
import type { Locale } from "@/lib/clinic/locale";
import { useI18n } from "@/lib/i18n/context";

type Msg = { role: "user" | "assistant"; content: string; mode?: string };
type Phase = "listening" | "thinking" | "speaking";
type Spoken = { ok: true; audio: string } | { ok: false; error?: string };

function speechLang(locale: Locale): string {
  if (locale === "fa") return "fa-IR";
  if (locale === "zh-CN") return "zh-CN";
  if (locale === "pt-BR") return "pt-BR";
  if (locale === "hi") return "hi-IN";
  if (locale === "ar") return "ar";
  if (locale === "es") return "es-ES";
  if (locale === "fr") return "fr-FR";
  return "en-US";
}

async function blobToBase64(blob: Blob): Promise<string> {
  const buf = new Uint8Array(await blob.arrayBuffer());
  let binary = "";
  for (let i = 0; i < buf.length; i += 0x8000) {
    binary += String.fromCharCode(...buf.subarray(i, i + 0x8000));
  }
  return btoa(binary);
}

function silentWavUrl(): string {
  const sampleRate = 8000;
  const samples = 160;
  const dataSize = samples * 2;
  const buffer = new ArrayBuffer(44 + dataSize);
  const view = new DataView(buffer);
  const write = (offset: number, value: string) => {
    for (let i = 0; i < value.length; i += 1) view.setUint8(offset + i, value.charCodeAt(i));
  };
  write(0, "RIFF");
  view.setUint32(4, 36 + dataSize, true);
  write(8, "WAVE");
  write(12, "fmt ");
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);
  write(36, "data");
  view.setUint32(40, dataSize, true);
  let binary = "";
  const bytes = new Uint8Array(buffer);
  for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
  return `data:audio/wav;base64,${btoa(binary)}`;
}

export const Route = createFileRoute("/concierge")({
  loader: async () => getShell({ data: await getPreferredLocale() }),
  component: ConciergePage,
});

function ConciergePage() {
  const initial = Route.useLoaderData();
  const { locale, t } = useI18n();
  const [shell, setShell] = useState(initial);
  const [text, setText] = useState("");
  const [conversationId, setConversationId] = useState<string>();
  const [messages, setMessages] = useState<Msg[]>([]);
  const [busy, setBusy] = useState(false);
  const [calling, setCalling] = useState(false);
  const audioCtx = useRef<AudioContext | null>(null);
  const analyserRef = useRef<AnalyserNode | null>(null);
  const speaker = useRef<HTMLAudioElement | null>(null);
  const greetingVoice = useRef<Promise<Spoken> | null>(null);
  const thread = useRef<HTMLDivElement>(null);
  useEffect(() => {
    thread.current?.scrollTo({ top: thread.current.scrollHeight });
  }, [messages, busy]);
  useEffect(() => {
    if (shell.locale === locale) return;
    let live = true;
    getShell({ data: locale }).then((next) => { if (live) setShell(next); });
    return () => { live = false; };
  }, [locale, shell.locale]);

  async function send(message: string) {
    const value = message.trim();
    if (!value || busy) return;
    setMessages((list) => [...list, { role: "user", content: value }]);
    setText("");
    setBusy(true);
    const result = await askConcierge({ data: { locale, message: value, conversationId } }).catch(() => ({
      ok: false as const,
      error: "invalid",
    }));
    setBusy(false);
    if (!result.ok) {
      setMessages((list) => [...list, { role: "assistant", content: t(`errors.${result.error}`), mode: "fallback" }]);
      return;
    }
    setConversationId(result.conversationId);
    setMessages((list) => [...list, { role: "assistant", content: result.text, mode: result.mode }]);
  }

  const modeLabel = (mode?: string) => mode === "model" ? t("concierge.modeModel") : mode === "fallback" ? t("concierge.modeFallback") : t("concierge.modeRecords");

  function openCall() {
    const Ctx = window.AudioContext ?? (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
    if (Ctx) {
      const ctx = audioCtx.current ?? new Ctx();
      audioCtx.current = ctx;
      void ctx.resume();
      const silent = ctx.createBuffer(1, Math.max(1, Math.floor(ctx.sampleRate / 20)), ctx.sampleRate);
      const ping = ctx.createBufferSource();
      ping.buffer = silent;
      ping.connect(ctx.destination);
      ping.start();
    }
    const line = speaker.current ?? new Audio();
    speaker.current = line;
    line.setAttribute("playsinline", "true");
    if (Ctx && audioCtx.current && !analyserRef.current) {
      try {
        const media = audioCtx.current.createMediaElementSource(line);
        const analyser = audioCtx.current.createAnalyser();
        analyser.fftSize = 128;
        analyser.smoothingTimeConstant = 0.72;
        media.connect(analyser);
        analyser.connect(audioCtx.current.destination);
        analyserRef.current = analyser;
      } catch {
        analyserRef.current = null;
      }
    }
    line.src = silentWavUrl();
    void line.play().then(() => {
      line.pause();
    }).catch(() => undefined);
    if ("speechSynthesis" in window) {
      const primer = new SpeechSynthesisUtterance(" ");
      primer.volume = 0;
      window.speechSynthesis.speak(primer);
    }
    greetingVoice.current = speakVoice({ data: { text: t("concierge.greeting"), locale } }).catch(() => ({ ok: false as const, error: "invalid" }));
    setCalling(true);
  }
  return (
    <Frame shell={shell}>
      <div className="mx-auto max-w-2xl px-4 pt-5">
        <section className="rounded-[1.75rem] bg-paper px-5 py-6 text-center md:px-8 md:py-8">
          <Photo src="/clinic/aurelia.jpg" alt={t("concierge.portraitAlt")} className="mx-auto size-28 rounded-full object-cover object-top ring-4 ring-ivory md:size-36" />
          <p className="mt-4 text-[11px] tracking-[0.28em] text-gold-deep uppercase">{t("concierge.kicker")}</p>
          <h1 className="mt-2 font-display text-4xl text-forest md:text-5xl">{t("concierge.title")}</h1>
          <p className="mx-auto mt-3 max-w-md text-sm text-muted">{t("concierge.disclaimer")}</p>
          <button type="button" className="mt-5 flex h-14 w-full items-center justify-center gap-3 rounded-full bg-forest text-ivory" onClick={openCall}>
            <Phone className="size-5" />
            {t("concierge.call")}
          </button>
          <p className="mt-3 text-sm text-muted">{t("concierge.voiceNote")}</p>
        </section>
        <div className="mt-3 grid grid-cols-2 gap-2">
          {[t("concierge.suggestion1"), t("concierge.suggestion2"), t("concierge.suggestion3"), t("concierge.suggestion4")].map((prompt) => (
            <button key={prompt} type="button" className="min-h-14 rounded-2xl bg-sage/70 px-3 py-2 text-start text-xs leading-snug text-forest md:text-sm" onClick={() => void send(prompt)}>{prompt}</button>
          ))}
        </div>
        <section className="mt-3 rounded-[1.75rem] border border-line bg-ivory p-4">
          <div ref={thread} className={`grid gap-3 overflow-y-auto ${messages.length ? "max-h-[24rem]" : ""}`}>
            {!messages.length ? <p className="text-center text-sm text-muted">{t("concierge.empty")}</p> : null}
            {messages.map((message, index) => (
              <article key={`${message.role}-${index}`} className={message.role === "user" ? "ms-8 rounded-2xl bg-sage p-3" : "me-8 rounded-2xl border border-line p-3"}>
                <p className="whitespace-pre-wrap text-sm">{message.content}</p>
                {message.role === "assistant" ? <p className="mt-2 text-xs text-muted">{modeLabel(message.mode)}</p> : null}
              </article>
            ))}
            {busy ? <p className="text-sm text-muted">{t("concierge.thinking")}</p> : null}
          </div>
        </section>
        <form className="sticky bottom-24 z-30 mt-3 rounded-[1.75rem] border border-line bg-ivory p-3 shadow-soft md:bottom-28" onSubmit={(event) => { event.preventDefault(); void send(text); }}>
          <TextArea
            value={text}
            placeholder={t("concierge.placeholder")}
            onChange={(event) => setText(event.target.value)}
            onKeyDown={(event) => {
              if (event.key === "Enter" && !event.shiftKey) {
                event.preventDefault();
                void send(text);
              }
            }}
          />
          <div className="mt-3 flex gap-3">
            <Button type="submit" disabled={busy}>{t("concierge.send")}</Button>
            <Link to="/book" className="inline-flex h-11 items-center text-sm">{t("concierge.openBook")}</Link>
          </div>
        </form>
        <p className="mt-4 text-center text-xs text-muted">{t("common.emergency")}</p>
      </div>
      {calling ? (
        <VoiceCall
          locale={locale}
          t={t}
          audioCtx={audioCtx.current}
          analyser={analyserRef.current}
          speaker={speaker.current}
          firstVoice={greetingVoice.current}
          onLine={(role, content) => setMessages((list) => [...list, { role, content }])}
          onClose={() => setCalling(false)}
        />
      ) : null}
    </Frame>
  );
}

function VoiceMeter({ speaking, analyser }: { speaking: boolean; analyser: AnalyserNode | null }) {
  const count = 36;
  const bars = useRef<Array<HTMLSpanElement | null>>([]);
  useEffect(() => {
    const nodes = bars.current;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce || !speaking) {
      nodes.forEach((el, index) => {
        if (el) el.style.transform = `scaleY(${speaking ? 0.45 + (index % 4) * 0.08 : 0.16})`;
      });
      if (reduce || !speaking) return;
    }
    let frame = 0;
    const bins = analyser ? new Uint8Array(analyser.frequencyBinCount) : null;
    const tick = () => {
      const now = performance.now() / 260;
      let energy = 0;
      if (bins && analyser) {
        analyser.getByteFrequencyData(bins);
        for (let i = 0; i < bins.length; i += 1) energy += bins[i];
        energy /= bins.length * 255;
      }
      nodes.forEach((el, index) => {
        if (!el) return;
        const wave = 0.2 + Math.abs(Math.sin(now + index * 0.48)) * 0.62;
        const live = bins ? 0.12 + (bins[Math.min(bins.length - 1, Math.floor((index / count) * bins.length * 0.7))] / 255) * 1.05 : wave;
        const level = energy > 0.04 ? live : wave;
        el.style.transform = `scaleY(${Math.max(0.12, Math.min(1, level))})`;
      });
      frame = window.requestAnimationFrame(tick);
    };
    frame = window.requestAnimationFrame(tick);
    return () => window.cancelAnimationFrame(frame);
  }, [speaking, analyser]);
  return (
    <div className="mt-6 flex h-14 w-full max-w-xs items-center justify-center gap-[3px]" aria-hidden="true">
      {Array.from({ length: count }, (_, index) => (
        <span
          key={index}
          ref={(node) => { bars.current[index] = node; }}
          className="h-full w-[3px] origin-center rounded-full bg-gold"
          style={{ transform: "scaleY(0.16)" }}
        />
      ))}
    </div>
  );
}

function VoiceCall({
  locale,
  t,
  audioCtx,
  analyser,
  speaker,
  firstVoice,
  onLine,
  onClose,
}: {
  locale: Locale;
  t: (path: string) => string;
  audioCtx: AudioContext | null;
  analyser: AnalyserNode | null;
  speaker: HTMLAudioElement | null;
  firstVoice: Promise<Spoken> | null;
  onLine: (role: "user" | "assistant", content: string) => void;
  onClose: () => void;
}) {
  const token = useRef(0);
  const conv = useRef<string | undefined>(undefined);
  const sourceRef = useRef<AudioBufferSourceNode | null>(null);
  const recorder = useRef<MediaRecorder | null>(null);
  const [phase, setPhase] = useState<Phase>("speaking");
  const [line, setLine] = useState("");
  const [seconds, setSeconds] = useState(0);
  const [error, setError] = useState("");
  const failed = useRef("");

  function stopAudio() {
    const source = sourceRef.current;
    sourceRef.current = null;
    if (source) {
      source.onended = null;
      try { source.stop(); } catch { /* already stopped */ }
    }
    if (speaker) {
      speaker.onended = null;
      speaker.pause();
    }
    if ("speechSynthesis" in window) window.speechSynthesis.cancel();
    if (recorder.current && recorder.current.state !== "inactive") recorder.current.stop();
  }

  function hang() {
    token.current += 1;
    stopAudio();
    onClose();
  }

  useEffect(() => {
    const id = ++token.current;
    const live = () => token.current === id;
    const clock = window.setInterval(() => {
      if (live()) setSeconds((value) => value + 1);
    }, 1000);
    void loop(live);
    return () => {
      if (token.current === id) token.current += 1;
      window.clearInterval(clock);
      stopAudio();
    };
    // The call owns its loop for the life of the overlay.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  async function playClip(el: HTMLAudioElement, audio: string, live: () => boolean) {
    el.pause();
    el.onended = null;
    el.src = `data:audio/mpeg;base64,${audio}`;
    await Promise.race([
      el.play(),
      new Promise((_, reject) => window.setTimeout(() => reject(new Error("play")), 3000)),
    ]);
    if (!(el.duration > 0.4)) {
      await new Promise((resolve) => {
        el.onloadedmetadata = () => resolve(undefined);
        window.setTimeout(resolve, 700);
      });
    }
    if (!live() || !(el.duration > 0.4)) {
      el.pause();
      return false;
    }
    await new Promise<void>((resolve) => {
      let settled = false;
      const finish = () => {
        if (settled) return;
        settled = true;
        resolve();
      };
      el.onended = finish;
      window.setTimeout(finish, el.duration * 1000 + 350);
    });
    return live();
  }

  async function playBuffer(ctx: AudioContext, audio: string, live: () => boolean) {
    await Promise.race([ctx.resume(), new Promise((resolve) => window.setTimeout(resolve, 400))]);
    if (ctx.state !== "running" || !live()) return false;
    const binary = atob(audio);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
    const buffer = await ctx.decodeAudioData(bytes.buffer.slice(0));
    if (!live()) return false;
    const source = ctx.createBufferSource();
    source.buffer = buffer;
    source.connect(analyser ?? ctx.destination);
    sourceRef.current = source;
    const mark = ctx.currentTime;
    const played = await new Promise<boolean>((resolve) => {
      let settled = false;
      const finish = (ok: boolean) => {
        if (settled) return;
        settled = true;
        if (sourceRef.current === source) sourceRef.current = null;
        resolve(ok);
      };
      source.onended = () => finish(true);
      source.start();
      window.setTimeout(() => finish(ctx.currentTime > mark + Math.min(0.15, buffer.duration / 2)), Math.ceil(buffer.duration * 1000) + 300);
    });
    return played;
  }

  async function playText(text: string, preset: Promise<Spoken> | null, live: () => boolean) {
    const spoken = text.replace(/\s+/g, " ").trim().slice(0, 420);
    if (!spoken || !live()) return;
    setPhase("speaking");
    const voice = await (preset ?? speakVoice({ data: { text: spoken, locale } }).catch(() => ({ ok: false as const, error: "invalid" })));
    if (!live()) return;
    if (voice.ok && voice.audio) {
      if (speaker) {
        const played = await playClip(speaker, voice.audio, live).catch(() => false);
        if (played) return;
        speaker.pause();
      }
      if (audioCtx) {
        try {
          const played = await playBuffer(audioCtx, voice.audio, live);
          if (played) return;
        } catch {
          /* studio voice could not be decoded */
        }
      }
    }
    await deviceSpeak(spoken, live);
  }

  function deviceSpeak(text: string, live: () => boolean) {
    return new Promise<void>((resolve) => {
      let settled = false;
      const finish = () => {
        if (settled) return;
        settled = true;
        resolve();
      };
      const estimate = Math.min(12000, Math.max(2200, text.length * 80));
      window.setTimeout(finish, estimate);
      if (!("speechSynthesis" in window) || !live()) return finish();
      const utter = new SpeechSynthesisUtterance(text);
      utter.lang = speechLang(locale);
      utter.onend = finish;
      utter.onerror = finish;
      window.speechSynthesis.cancel();
      window.speechSynthesis.speak(utter);
    });
  }

  function listenOnce(live: () => boolean): Promise<Blob | null> {
    return new Promise((resolve) => {
      if (!live()) return resolve(null);
      navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } }).then((stream) => {
        if (!live()) {
          stream.getTracks().forEach((track) => track.stop());
          return resolve(null);
        }
        const context = new AudioContext();
        const source = context.createMediaStreamSource(stream);
        const analyser = context.createAnalyser();
        analyser.fftSize = 512;
        source.connect(analyser);
        const data = new Uint8Array(analyser.fftSize);
        const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : "";
        const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
        const chunks: Blob[] = [];
        recorder.current = rec;
        rec.ondataavailable = (event) => { if (event.data.size) chunks.push(event.data); };
        rec.onstop = () => {
          stream.getTracks().forEach((track) => track.stop());
          void context.close();
          resolve(chunks.length ? new Blob(chunks, { type: rec.mimeType || "audio/webm" }) : null);
        };
        rec.start();
        const started = performance.now();
        let spoke = false;
        let silence = 0;
        const timer = window.setInterval(() => {
          if (!live()) {
            window.clearInterval(timer);
            if (rec.state === "recording") rec.stop();
            return;
          }
          analyser.getByteTimeDomainData(data);
          let sum = 0;
          for (let i = 0; i < data.length; i += 1) {
            const sample = (data[i] - 128) / 128;
            sum += sample * sample;
          }
          const rms = Math.sqrt(sum / data.length);
          const elapsed = performance.now() - started;
          if (rms > 0.035) {
            spoke = true;
            silence = 0;
          } else if (spoke) silence += 120;
          const finished = (spoke && silence > 1100) || elapsed > 18000;
          const nobody = !spoke && elapsed > 14000;
          if (finished || nobody) {
            window.clearInterval(timer);
            if (rec.state === "recording") rec.stop();
          }
        }, 120);
      }).catch(() => {
        failed.current = t("concierge.micDenied");
        setError(t("concierge.micDenied"));
        resolve(null);
      });
    });
  }

  async function loop(live: () => boolean) {
    const hello = t("concierge.greeting");
    setLine(hello);
    onLine("assistant", hello);
    await playText(hello, firstVoice, live);
    while (live()) {
      setPhase("listening");
      const blob = await listenOnce(live);
      if (!live()) break;
      if (failed.current) break;
      if (!blob || blob.size < 800) continue;
      setPhase("thinking");
      const audio = await blobToBase64(blob);
      const heard = await transcribeVoice({ data: { audio, mime: blob.type, locale } }).catch(() => ({ ok: false as const, error: "invalid" }));
      if (!live()) break;
      if (!heard.ok) {
        setError(t(`errors.${heard.error}`));
        if (heard.error === "rate") break;
        continue;
      }
      if (!heard.text) continue;
      setLine(heard.text);
      onLine("user", heard.text);
      const result = await askConcierge({ data: { locale, message: heard.text, conversationId: conv.current } }).catch(() => ({
        ok: false as const,
        error: "invalid",
      }));
      if (!live()) break;
      if (!result.ok) {
        setError(t(`errors.${result.error}`));
        break;
      }
      conv.current = result.conversationId;
      setLine(result.text);
      onLine("assistant", result.text);
      await playText(result.text, null, live);
    }
    if (live() && !failed.current) hang();
  }

  const status = error || (phase === "listening" ? t("concierge.callListening") : phase === "thinking" ? t("concierge.callThinking") : t("concierge.callSpeaking"));
  const clock = `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
  return (
    <div className="fixed inset-0 z-[80] flex flex-col bg-forest-deep text-ivory" role="dialog" aria-label={t("concierge.call")}>
      <Photo src="/clinic/aurelia.jpg" alt="" className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-25" />
      <div className="absolute inset-0 bg-gradient-to-b from-forest-deep/70 via-forest-deep/80 to-forest-deep" />
      <div className="relative flex min-h-full flex-col">
        <p className="pt-10 text-center text-[11px] tracking-[0.28em] text-gold uppercase">{t("concierge.kicker")}</p>
        <h2 className="mt-2 text-center font-display text-5xl">Aurelia</h2>
        <p className="mt-1 text-center text-sm tabular-nums text-ivory/70">{clock}</p>
        <div className="flex flex-1 flex-col items-center justify-center px-6">
          <div className="relative grid size-64 place-items-center sm:size-72">
            <div className={`aurelia-ring absolute inset-0 rounded-full border border-dashed border-gold/70 ${phase === "speaking" ? "opacity-100" : "opacity-35"}`} />
            <Photo src="/clinic/aurelia.jpg" alt="" className="size-52 rounded-full object-cover object-top shadow-soft sm:size-60" />
          </div>
          <VoiceMeter speaking={phase === "speaking"} analyser={analyser} />
          <p className="mt-6 text-[11px] tracking-[0.22em] uppercase">{status}</p>
          <p className="mt-4 max-w-md text-center font-display text-2xl text-ivory/90">{line}</p>
        </div>
        <button type="button" className="relative mx-auto mb-12 grid size-16 place-items-center rounded-full bg-danger" aria-label={t("concierge.hangUp")} onClick={hang}>
          <PhoneOff />
        </button>
      </div>
    </div>
  );
}
