"use client";

import Image from "next/image";
import { useCallback, useEffect, useRef, useState } from "react";
import { Mic, PhoneOff, RotateCcw, Volume2, X } from "lucide-react";

import { MobileShell } from "@/app/_components/core";
import { getApiConfig } from "@/core/net/config/api_config";
import {
  VoiceCallTransport,
  type CallBillingStatusPayload,
} from "@/core/net/voice-call-transport";
import { VoiceCallAudioCapture } from "@/integrations/voice-call-audio";
import { getSessionAccessToken } from "@/lib/auth/auth_session";
import { useActiveCharacter, useActiveCharacterRoutes } from "@/providers/character-provider";
import { useAppNavigator } from "@/router/use-app-navigator";

import { VoiceCallPlayer } from "./voice-call-player";
import styles from "./voice-call-screen.module.css";

type CallUiState = "idle" | "connecting" | "listening" | "thinking" | "speaking" | "ended" | "error";

const STATE_COPY: Record<CallUiState, string> = {
  idle: "Ready to call",
  connecting: "Connecting…",
  listening: "Listening…",
  thinking: "Thinking…",
  speaking: "Speaking…",
  ended: "Call ended",
  error: "Call unavailable",
};

async function getSessionToken(): Promise<string> {
  const token = await getSessionAccessToken();
  if (token) return token;
  throw new Error("Please sign in before starting a call");
}

export function VoiceCallScreen() {
  const character = useActiveCharacter();
  const routes = useActiveCharacterRoutes();
  const navigator = useAppNavigator();
  const [uiState, setUiState] = useState<CallUiState>("idle");
  const [userTranscript, setUserTranscript] = useState("");
  const [aiTranscript, setAiTranscript] = useState("");
  const [freeTurns, setFreeTurns] = useState(0);
  const [balance, setBalance] = useState(0);
  const [cost, setCost] = useState(2);
  const [error, setError] = useState<string | null>(null);
  const [showTopUp, setShowTopUp] = useState(false);
  const transportRef = useRef<VoiceCallTransport | null>(null);
  const captureRef = useRef<VoiceCallAudioCapture | null>(null);
  const playerRef = useRef<VoiceCallPlayer | null>(null);
  const callIdRef = useRef("");
  const activeTurnRef = useRef("");
  const reconnectTurnRef = useRef("");
  const resumeVersionRef = useRef(0);
  const endingRef = useRef(false);
  const pendingTopUpRef = useRef(false);

  const finishCall = useCallback((navigate = true) => {
    if (endingRef.current) return;
    endingRef.current = true;
    captureRef.current?.dispose();
    playerRef.current?.dispose();
    if (callIdRef.current) transportRef.current?.endCall(callIdRef.current);
    transportRef.current?.disconnect();
    reconnectTurnRef.current = "";
    resumeVersionRef.current = 0;
    setUiState("ended");
    if (navigate) navigator.push(routes.chat);
  }, [navigator, routes.chat]);

  const beginListening = useCallback((turnId: string) => {
    const capture = captureRef.current;
    const transport = transportRef.current;
    const callId = callIdRef.current;
    if (!capture || !transport || !callId || !turnId) return;
    activeTurnRef.current = turnId;
    reconnectTurnRef.current = turnId;
    setUiState(playerRef.current?.isPlaying ? "speaking" : "listening");
    const preRoll: ArrayBuffer[] = [];
    let streamOpened = false;
    capture.startTurn({
      onChunk: (chunk) => {
        if (streamOpened) {
          transport.sendAudioChunk(chunk);
          return;
        }
        preRoll.push(chunk);
        if (preRoll.length > 5) preRoll.shift();
      },
      onSpeechStart: () => {
        setError(null);
        if (playerRef.current?.isPlaying) {
          playerRef.current.stop();
          transport.interrupt(callId);
        }
        streamOpened = transport.startAudio(
          callId,
          turnId,
          capture.mimeType,
          capture.sampleRate,
        );
        if (streamOpened) {
          for (const chunk of preRoll) transport.sendAudioChunk(chunk);
          preRoll.length = 0;
          setUserTranscript("");
          setAiTranscript("");
          setUiState("listening");
        }
      },
      onTurnEnd: (vadLatencyMs) => {
        if (streamOpened) {
          transport.endAudio(callId, turnId, vadLatencyMs);
          setUiState("thinking");
        }
      },
      onError: (captureError) => {
        setError(captureError.message);
        setUiState("error");
      },
    });
  }, []);

  const applyBilling = useCallback((status: CallBillingStatusPayload) => {
    setBalance(status.creditBalance);
    setFreeTurns(status.freeTurnsRemaining);
    setCost(status.requiredCredits || 2);
    if (status.canContinue && status.nextTurnId) {
      beginListening(status.nextTurnId);
      return;
    }
    captureRef.current?.stopTurn();
    pendingTopUpRef.current = true;
    if (!playerRef.current?.isPlaying) setShowTopUp(true);
  }, [beginListening]);

  const startCall = useCallback(async () => {
    if (uiState !== "idle" && uiState !== "error") return;
    endingRef.current = false;
    pendingTopUpRef.current = false;
    setError(null);
    setUiState("connecting");
    try {
      const player = new VoiceCallPlayer();
      playerRef.current = player;
      const playerReady = player.prepare();
      const capture = new VoiceCallAudioCapture();
      await Promise.all([capture.prepare(), playerReady]);
      captureRef.current = capture;
      const token = await getSessionToken();
      const transport = new VoiceCallTransport(getApiConfig().wsUrl, token);
      transportRef.current = transport;
      player.onPlaying = () => setUiState("speaking");
      player.onIdle = () => {
        if (pendingTopUpRef.current) setShowTopUp(true);
        else if (activeTurnRef.current) setUiState("listening");
      };
      player.onError = (playbackError) => setError(playbackError.message);
      player.onGap = (milliseconds) => {
        transport.sendClientMetric("audioChunkGap", milliseconds);
      };
      transport.onOpen = () => {
        if (!reconnectTurnRef.current) reconnectTurnRef.current = crypto.randomUUID();
        transport.startCall(
          character.id,
          reconnectTurnRef.current,
          resumeVersionRef.current || undefined,
        );
      };
      transport.onReconnecting = () => {
        capture.stopTurn();
        player.stop();
        callIdRef.current = "";
        setUiState("connecting");
      };
      transport.onStarted = (started) => {
        callIdRef.current = started.callId;
        activeTurnRef.current = started.turnId;
        reconnectTurnRef.current = started.turnId;
        resumeVersionRef.current = started.resumeVersion;
        setFreeTurns(started.freeTurnsRemaining);
        setBalance(started.creditBalance);
        setCost(started.costPerPaidTurn || 2);
        beginListening(started.turnId);
      };
      transport.onState = setUiState;
      transport.onTranscript = (text) => setUserTranscript(text);
      transport.onAiSentence = (text) => {
        setAiTranscript((current) => `${current}${current ? " " : ""}${text}`);
      };
      transport.onAudioChunk = (meta, bytes) => player.enqueue(meta, bytes);
      transport.onBillingStatus = applyBilling;
      transport.onEnded = () => setUiState("ended");
      transport.onError = (code, message) => {
        setError(message);
        if (
          activeTurnRef.current
          && (code === "CALL_TURN_FAILED" || code === "CALL_STT_FAILED")
        ) {
          setUiState("listening");
        } else {
          setUiState("error");
        }
      };
      transport.connect();
    } catch (startError) {
      captureRef.current?.dispose();
      playerRef.current?.dispose();
      setError(startError instanceof Error ? startError.message : String(startError));
      setUiState("error");
    }
  }, [applyBilling, beginListening, character.id, uiState]);

  useEffect(() => () => finishCall(false), [finishCall]);

  const lowBalance = freeTurns === 0 && balance < cost * 2 && balance >= cost;

  return (
    <MobileShell background="#090810">
      <main className={styles.screen}>
        <Image src={character.assets.chatBackground} alt="" fill priority className={styles.background} />
        <div className={styles.scrim} />
        <header className={styles.header}>
          <button type="button" className={styles.iconButton} onClick={() => finishCall()} aria-label="Close call">
            <X size={22} aria-hidden="true" />
          </button>
          <div className={styles.balances}>
            <span>{freeTurns} free</span>
            <span>{balance} credits</span>
          </div>
        </header>

        <section className={styles.content} aria-live="polite">
          <div className={`${styles.avatarRing} ${styles[uiState]}`}>
            <Image src={character.assets.avatar} alt={character.displayName} width={148} height={148} className={styles.avatar} priority />
          </div>
          <h1>{character.displayName}</h1>
          <div className={styles.state}>
            {uiState === "listening" ? <Mic size={18} aria-hidden="true" /> : null}
            {uiState === "speaking" ? <Volume2 size={18} aria-hidden="true" /> : null}
            <span>{STATE_COPY[uiState]}</span>
          </div>
          {lowBalance ? <p className={styles.lowBalance}>Your balance covers fewer than two paid turns.</p> : null}
          <div className={styles.transcript}>
            {userTranscript ? <p><span>You</span>{userTranscript}</p> : null}
            {aiTranscript ? <p><span>{character.shortName}</span>{aiTranscript}</p> : null}
          </div>
          {error ? <p className={styles.error}>{error}</p> : null}
        </section>

        <footer className={styles.footer}>
          {uiState === "idle" || uiState === "error" ? (
            <button type="button" className={styles.startButton} onClick={() => void startCall()}>
              {uiState === "error" ? <RotateCcw size={20} aria-hidden="true" /> : <Mic size={20} aria-hidden="true" />}
              {uiState === "error" ? "Try again" : "Start call"}
            </button>
          ) : (
            <button type="button" className={styles.endButton} onClick={() => finishCall()} aria-label="End call">
              <PhoneOff size={27} aria-hidden="true" />
            </button>
          )}
          <p>Speak naturally — you can interrupt at any time.</p>
        </footer>

        {showTopUp ? (
          <div className={styles.topUpBackdrop} role="presentation">
            <section className={styles.topUpDialog} role="dialog" aria-modal="true" aria-labelledby="call-topup-title">
              <h2 id="call-topup-title">Not enough credits for another turn</h2>
              <p>Your current reply has finished. Top up to keep the conversation going.</p>
              <button type="button" onClick={() => navigator.openSubscription({ type: "topup", returnTo: "chat" })}>Top up credits</button>
              <button type="button" className={styles.secondaryButton} onClick={() => finishCall()}>End call</button>
            </section>
          </div>
        ) : null}
      </main>
    </MobileShell>
  );
}
