package audio

import (
	"context"
	"encoding/binary"
	"fmt"
	"io"
	"net/http"
	"os/exec"
	"time"

	"github.com/hraban/opus"
)

// MP3URLToOpusFrames 从 audioURL 下载音频，通过 ffmpeg pipe 解码为 PCM，
// 再用 libopus 编码为 60ms 帧列表，全程流式处理不落磁盘。
//
// ⚠️ 安全约束：audioURL 只能作为 http.Get 的参数，
// 绝对不能出现在 exec.Command 的参数列表中（防止命令注入）。
func MP3URLToOpusFrames(ctx context.Context, audioURL string) ([][]byte, error) {
	// 1. 下载音频（流式，不全量载入内存）
	httpCtx, httpCancel := context.WithTimeout(ctx, 60*time.Second)
	defer httpCancel()

	req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, audioURL, nil)
	if err != nil {
		return nil, fmt.Errorf("audio: build request: %w", err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("audio: download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("audio: download status %d", resp.StatusCode)
	}

	// 2. ffmpeg：stdin 读原始音频，stdout 输出 s16le PCM（16kHz 单声道）
	// 所有参数硬编码，audioURL 不进入命令行（防命令注入）
	ffmpegCtx, ffmpegCancel := context.WithTimeout(ctx, 120*time.Second)
	defer ffmpegCancel()

	cmd := exec.CommandContext(ffmpegCtx,
		"ffmpeg",
		"-nostdin",
		"-loglevel", "error",  // 只输出错误，不污染 stdout pipe
		"-i", "pipe:0",        // 从 stdin 读输入
		"-ar", "16000",        // 目标采样率
		"-ac", "1",            // 单声道
		"-f", "s16le",         // 输出格式：有符号 16bit 小端 PCM
		"pipe:1",              // 输出到 stdout
	)
	cmd.Stdin = resp.Body // HTTP body 直接接 ffmpeg stdin，不经过磁盘

	pcmReader, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("audio: stdout pipe: %w", err)
	}
	stderrPipe, _ := cmd.StderrPipe()

	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("audio: start ffmpeg: %w", err)
	}

	// 3. 逐帧读取 PCM 并实时 Opus 编码
	enc, err := opus.NewEncoder(SampleRate, Channels, opus.AppAudio)
	if err != nil {
		return nil, fmt.Errorf("audio: create encoder: %w", err)
	}

	pcmBuf := make([]int16, FrameSize)         // 960 int16 samples
	opusBuf := make([]byte, 4000)              // Opus 输出缓冲（4KB 足够单帧）
	var frames [][]byte

	for {
		err := binary.Read(pcmReader, binary.LittleEndian, pcmBuf)
		if err == io.EOF || err == io.ErrUnexpectedEOF {
			// 最后一帧不足时已补零（binary.Read 会读已有字节），直接编码
			if err == io.ErrUnexpectedEOF {
				n, encErr := enc.Encode(pcmBuf, opusBuf)
				if encErr == nil && n > 0 {
					frame := make([]byte, n)
					copy(frame, opusBuf[:n])
					frames = append(frames, frame)
				}
			}
			break
		}
		if err != nil {
			// ffmpeg 已结束（context cancel 等），读取结束
			break
		}

		n, err := enc.Encode(pcmBuf, opusBuf)
		if err != nil {
			return nil, fmt.Errorf("audio: opus encode: %w", err)
		}
		frame := make([]byte, n)
		copy(frame, opusBuf[:n])
		frames = append(frames, frame)
	}

	// 排空 stderr 避免 ffmpeg 阻塞
	io.Copy(io.Discard, stderrPipe)

	if err := cmd.Wait(); err != nil {
		// context 超时导致的退出不视为错误（已有 frames 可以播放）
		if ffmpegCtx.Err() == nil {
			return nil, fmt.Errorf("audio: ffmpeg exit: %w", err)
		}
	}

	if len(frames) == 0 {
		return nil, fmt.Errorf("audio: no frames produced from %s", truncateURL(audioURL))
	}

	return frames, nil
}

// truncateURL 截断 URL 用于日志，避免输出带签名的完整 URL。
func truncateURL(u string) string {
	if len(u) > 80 {
		return u[:80] + "..."
	}
	return u
}
