/*************************************************
 * 🎤 在线录音弹层 —— window.NRVoiceRecModal（2026-10-19 拍板：录音两条路都给了）
 * 💻 电脑直接录：getUserMedia+MediaRecorder，提词器大字显示稿子对着念，录好解码转 WAV 直传
 *    （wan3 reference_audio 只收 wav/mp3 ≤15s——前端 AudioContext 解码+PCM 编码，通吃 webm/mp4 源）
 * 📱 手机扫码录：QR 打开移动版录音页（voice_rec_token 换一次性免登录 URL），手机录完直传，
 *    本弹层 5s 轮询「我的声音」，新声音到了自动选中收工
 * 入口：AI 视频弹层 ⑥ 配音音色 / 我的信息-我的声音。onSaved(voices)=保存成功后回传最新声音列表
 *************************************************/
window.NRVoiceRecModal = (function () {
	const { useState, useEffect, useRef } = React;
	const { Modal: RMo, Button: RBt, Spin: RSp, message: RMg, Input: RIn, QRCode: RQr, Tag: RTg } = antd;

	const MAX_SEC = 15;   // wan3 reference_audio 上限 15s

	//MediaRecorder 产出（webm/opus 或 mp4/aac）→ AudioContext 解码 → 16bit 单声道 WAV（wav/mp3 之外唯一稳的通吃解）
	async function blobToWav(blob) {
		const AC = window.AudioContext || window.webkitAudioContext;
		const ac = new AC();
		try {
			const buf = await ac.decodeAudioData(await blob.arrayBuffer());
			const ch = buf.getChannelData(0);
			const sr = buf.sampleRate;
			const pcm = new Int16Array(ch.length);
			for (let i = 0; i < ch.length; i++) {
				const s = Math.max(-1, Math.min(1, ch[i]));
				pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
			}
			const head = new ArrayBuffer(44);
			const v = new DataView(head);
			const wstr = (off, s) => { for (let i = 0; i < s.length; i++) { v.setUint8(off + i, s.charCodeAt(i)); } };
			wstr(0, "RIFF"); v.setUint32(4, 36 + pcm.length * 2, true); wstr(8, "WAVE");
			wstr(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true);
			v.setUint32(24, sr, true); v.setUint32(28, sr * 2, true); v.setUint16(32, 2, true); v.setUint16(34, 16, true);
			wstr(36, "data"); v.setUint32(40, pcm.length * 2, true);
			return new Blob([head, pcm.buffer], { type: "audio/wav" });
		} finally {
			try { ac.close(); } catch (e) { }
		}
	}

	function VoiceRecModal({ open, onClose, onSaved, script }) {
		const [tab, setTab] = useState("pc");            // pc 电脑直录 / mobile 手机扫码
		const [recState, setRecState] = useState("idle"); // idle/recording/done
		const [sec, setSec] = useState(0);               // 已录秒数
		const [playUrl, setPlayUrl] = useState("");      // 试听
		const [wavBlob, setWavBlob] = useState(null);    // 转好的 WAV
		const [name, setName] = useState("");
		const [saving, setSaving] = useState(false);
		const [micErr, setMicErr] = useState("");
		const [qrToken, setQrToken] = useState("");
		const [voicesN, setVoicesN] = useState(-1);      // 打开时的声音数（轮询检测新增）
		const mediaRef = useRef(null);                   // {rec,chunks,stream}
		const timerRef = useRef(null);
		const waveRef = useRef(null);                    // 录音波形 canvas
		const animRef = useRef(null);                    // 波形 rAF
		const actxRef = useRef(null);                    // 波形专用 AudioContext（与解码用的分开）

		//提词器稿子：父级传入的文案；空就给一句示例
		const promptText = (script || "").trim();

		useEffect(() => {
			if (!open) return;
			setTab("pc"); setRecState("idle"); setSec(0); setPlayUrl(""); setWavBlob(null);
			setName("我的声音 " + dayjs().format("MM-DD HH:mm")); setMicErr(""); setQrToken("");
			NRC.api("author_get").then(d => setVoicesN(((d.info || {}).voices || []).length)).catch(() => setVoicesN(-1));
			NRC.api("voice_rec_token").then(d => setQrToken(d.token || "")).catch(() => setQrToken(""));
			return () => stopAll();
		}, [open]);

		//手机扫码路：5s 轮询声音列表，数变多=手机录完了 → 自动收工选中最新
		useEffect(() => {
			if (!open || tab !== "mobile") return undefined;
			const tm = setInterval(() => {
				NRC.api("author_get").then(d => {
					const vs = ((d.info || {}).voices || []);
					if (voicesN >= 0 && vs.length > voicesN) {
						clearInterval(tm);
						RMg.success("手机录的声音到了！已自动选中");
						onSaved && onSaved(vs);
					}
				}).catch(() => { });
			}, 5000);
			return () => clearInterval(tm);
		}, [open, tab, voicesN]);

		const stopAll = () => {
			if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
			if (animRef.current) { cancelAnimationFrame(animRef.current); animRef.current = null; }
			if (actxRef.current) { try { actxRef.current.close(); } catch (e) { } actxRef.current = null; }
			const m = mediaRef.current;
			if (m) {
				try { if (m.rec && m.rec.state !== "inactive") { m.rec.stop(); } } catch (e) { }
				try { (m.stream.getTracks() || []).forEach(t => t.stop()); } catch (e) { }
				mediaRef.current = null;
			}
		};

		const startRec = async () => {
			setMicErr("");
			if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
				setMicErr("这个浏览器不支持录音（需 https + 新版 Chrome/Edge）——用手机扫码录也一样");
				return;
			}
			try {
				const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
				let mime = "";
				["audio/webm;codecs=opus", "audio/webm", "audio/mp4"].forEach(t => {
					if (!mime && window.MediaRecorder && MediaRecorder.isTypeSupported(t)) { mime = t; }
				});
				const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
				const chunks = [];
				rec.ondataavailable = e => { if (e.data && e.data.size) { chunks.push(e.data); } };
				rec.onstop = async () => {
					try { (stream.getTracks() || []).forEach(t => t.stop()); } catch (e) { }
					const raw = new Blob(chunks, { type: mime || "audio/webm" });
					try {
						const wav = await blobToWav(raw);
						setWavBlob(wav);
						setPlayUrl(URL.createObjectURL(wav));
						setRecState("done");
					} catch (e2) {
						setMicErr("录音解析失败，换个浏览器或用手机扫码录");
						setRecState("idle");
					}
					mediaRef.current = null;
				};
				mediaRef.current = { rec: rec, stream: stream };
				rec.start(200);
				setSec(0); setRecState("recording");
				//录音波形：AnalyserNode 时域数据逐帧画线（让用户看得见麦克风在吃声音）
				try {
					const AC2 = window.AudioContext || window.webkitAudioContext;
					const ac2 = new AC2();
					const an = ac2.createAnalyser();
					an.fftSize = 256;
					ac2.createMediaStreamSource(stream).connect(an);
					actxRef.current = ac2;
					const buf = new Uint8Array(an.fftSize);
					const draw = () => {
						const cv = waveRef.current;
						if (!cv) { return; }
						an.getByteTimeDomainData(buf);
						const ctx = cv.getContext("2d");
						const w = cv.width, h = cv.height;
						ctx.clearRect(0, 0, w, h);
						ctx.strokeStyle = "#d4380d"; ctx.lineWidth = 2; ctx.beginPath();
						const step = w / buf.length;
						for (let i = 0; i < buf.length; i++) {
							const y = h / 2 + (buf[i] - 128) / 128 * (h / 2 - 2);
							if (i === 0) { ctx.moveTo(0, y); } else { ctx.lineTo(i * step, y); }
						}
						ctx.stroke();
						animRef.current = requestAnimationFrame(draw);
					};
					draw();
				} catch (e) { }
				timerRef.current = setInterval(() => {
					setSec(s => {
						if (s + 1 >= MAX_SEC) { stopRec(); }
						return Math.min(MAX_SEC, s + 1);
					});
				}, 1000);
			} catch (e) {
				setMicErr("没拿到麦克风权限——浏览器地址栏左边点🔒允许麦克风，或用手机扫码录");
			}
		};

		const stopRec = () => {
			if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
			const m = mediaRef.current;
			if (m && m.rec && m.rec.state !== "inactive") {
				try { m.rec.stop(); } catch (e) { }
			}
		};

		const reRec = () => {
			stopAll(); setRecState("idle"); setSec(0); setPlayUrl(""); setWavBlob(null);
		};

		const save = () => {
			if (!wavBlob) { return; }
			if (sec < 1) { RMg.info("至少录 1 秒"); return; }
			setSaving(true);
			const fn = (name.trim() || "我的声音").replace(/[\\/:*?"<>|]/g, "") + ".wav";
			NRC.upload("dh_audio_upload", new File([wavBlob], fn, { type: "audio/wav" }), {})
				.then(d => {
					RMg.success("已存进「我的声音」");
					onSaved && onSaved(d.voices || null);
				})
				.catch(e => RMg.error(e.message))
				.finally(() => setSaving(false));
		};

		const qrUrl = qrToken ? (location.origin + "/?c=gen&m=voice_rec_page&t=" + encodeURIComponent(qrToken)) : "";

		const tabBtn = (k, t) => (
			<div onClick={() => { stopAll(); setRecState("idle"); setPlayUrl(""); setWavBlob(null); setTab(k); }}
				style={{ flex: 1, textAlign: "center", padding: "9px 0", borderRadius: 9, cursor: "pointer", fontSize: 14, fontWeight: tab === k ? 700 : 400, color: tab === k ? "var(--nr-brand)" : "#666", background: tab === k ? "#FFF5F4" : "#f7f5f4", border: tab === k ? "1px solid var(--nr-brand-bright)" : "1px solid transparent" }}>{t}</div>
		);

		return (
			<RMo open={open} onCancel={() => { stopAll(); onClose(); }} footer={null} width={520} centered title={null}>
				<div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>🎤 录一段我的声音</div>
				<div style={{ fontSize: 13, color: "#999", marginBottom: 14 }}>录 1-15 秒干净人声，做 AI 视频时用你的音色念文案（存进「我的信息-我的声音」，最多 3 条）</div>
				<div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
					{tabBtn("pc", "💻 电脑直接录")}
					{tabBtn("mobile", "📱 手机扫码录")}
				</div>

				{tab === "pc" ? (
					<div>
						{/* 提词器：对着念 */}
						<div style={{ background: "#faf8f7", borderRadius: 10, padding: "14px 16px", marginBottom: 14, maxHeight: 150, overflowY: "auto" }}>
							{promptText
								? <div style={{ fontSize: 16, lineHeight: 2, color: "#333" }}>{promptText}</div>
								: <div style={{ fontSize: 14, color: "#bbb", lineHeight: 1.9 }}>稿子会显示在这里对着念（从 AI 视频弹层进来就自动带文案）；<br />随便念也行，比如：「大家好，我是做劲仪好物的，今天给大家推荐一款我们店里卖得特别好的东西。」</div>}
						</div>
						<div style={{ textAlign: "center", padding: "6px 0 10px" }}>
							{recState === "recording" ? (
								<div>
									<div style={{ fontSize: 26, fontWeight: 700, color: "#d4380d", marginBottom: 10 }}>● {sec}s / {MAX_SEC}s</div>
									{/* 录音波形：看得见麦克风在吃声音，没波形=没录上 */}
									<canvas ref={waveRef} width={440} height={64} style={{ width: "100%", height: 64, background: "#fff", borderRadius: 8, marginBottom: 12, border: "1px solid #f0e6e4", display: "block" }} />
									<RBt danger size="large" onClick={stopRec} style={{ minWidth: 160 }}>⏹ 录好了，停</RBt>
									<div style={{ fontSize: 12, color: "#bbb", marginTop: 8 }}>到 {MAX_SEC} 秒自动停（视频配音只能用 15 秒内）；没波形说明麦克风没声音</div>
								</div>
							) : recState === "done" ? (
								<div>
									<audio controls src={playUrl} style={{ width: "100%", height: 36, marginBottom: 10 }} />
									<div style={{ display: "flex", gap: 8, justifyContent: "center" }}>
										<RBt onClick={reRec}>↺ 重录</RBt>
										<RBt type="primary" loading={saving} onClick={save} style={{ background: "var(--nr-brand)", borderColor: "var(--nr-brand)", minWidth: 140 }}>✓ 就用这段</RBt>
									</div>
								</div>
							) : (
								<div>
									<RBt type="primary" size="large" onClick={startRec} style={{ background: "var(--nr-brand)", borderColor: "var(--nr-brand)", minWidth: 180 }}>🎤 开始录音</RBt>
									<div style={{ fontSize: 12, color: "#bbb", marginTop: 8 }}>点了对着上面稿子念就行；第一次会让浏览器授权麦克风</div>
								</div>
							)}
							{micErr ? <div style={{ fontSize: 12, color: "#d4380d", marginTop: 8 }}>{micErr}</div> : null}
						</div>
						{recState === "done" ? (
							<div>
								<div style={{ fontSize: 13, color: "#666", marginBottom: 4 }}>给这段声音起个名</div>
								<RIn value={name} onChange={e => setName(e.target.value)} maxLength={20} />
							</div>
						) : null}
					</div>
				) : (
					<div style={{ textAlign: "center", padding: "6px 0 10px" }}>
						{qrUrl
							? <RQr value={qrUrl} size={190} />
							: <div style={{ padding: 30 }}><RSp /></div>}
						<div style={{ fontSize: 13, color: "#666", marginTop: 12, lineHeight: 2 }}>
							手机微信/浏览器扫码 → 在手机上对着念 → 点上传<br />
							录完这里会自动收到（约 5 秒），不用手动刷新
						</div>
						<div style={{ fontSize: 12, color: "#ccc", marginTop: 6 }}>二维码 30 分钟内有效，过期关了重开</div>
					</div>
				)}
			</RMo>
		);
	}

	return VoiceRecModal;
})();
