package core

import (
	"bufio"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"sync"
	"time"
)

// TextToSpeech synthesizes text into audio bytes.
type TextToSpeech interface {
	Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) (audio []byte, format string, err error)
}

// TTSSynthesisOpts carries optional synthesis parameters.
type TTSSynthesisOpts struct {
	Voice        string  // voice name, e.g. "Cherry", "Alloy"; empty = provider default
	LanguageType string  // e.g. "Chinese", "English"; empty = auto-detect
	Speed        float64 // speaking speed multiplier (0.5–2.0); 0 = default
}

// TTSCfg holds TTS configuration for the engine (mirrors SpeechCfg).
type TTSCfg struct {
	Enabled      bool
	Provider     string
	Voice        string  // default voice used when TTSSynthesisOpts.Voice is empty
	LanguageType string  // optional provider-specific language hint
	Speed        float64 // speaking speed multiplier; 0 = provider default
	TTS          TextToSpeech
	MaxTextLen   int // max rune count before skipping TTS; 0 = no limit

	mu      sync.RWMutex
	ttsMode string // "voice_only" (default) | "always"
}

// GetTTSMode returns the current TTS mode safely.
func (c *TTSCfg) GetTTSMode() string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	if c.ttsMode == "" {
		return "voice_only"
	}
	return c.ttsMode
}

// SetTTSMode updates the TTS mode safely.
func (c *TTSCfg) SetTTSMode(mode string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.ttsMode = mode
}

// AudioSender is implemented by platforms that support sending voice/audio messages.
//
// `format` is the lowercase short format hint extracted from the filename
// extension or MIME type (e.g. "mp3", "ogg", "opus", "m4a"). Platforms
// that need a specific codec (Feishu requires opus) are expected to
// transcode internally; format is only an input hint.
type AudioSender interface {
	SendAudio(ctx context.Context, replyCtx any, audio []byte, format string) error
}

// VideoSender is implemented by platforms that can render a native
// inline video bubble for an outbound clip rather than a generic file
// download. Engine code falls back to FileSender when a platform does
// not implement this interface.
//
// `format` is the lowercase short format hint (e.g. "mp4", "mov",
// "webm"). Implementations are free to transcode to the platform's
// preferred codec; on Feishu mp4/H.264 has the broadest playback
// support so non-mp4 inputs may render with reduced compatibility.
type VideoSender interface {
	SendVideo(ctx context.Context, replyCtx any, video []byte, format string, fileName string) error
}

// ──────────────────────────────────────────────────────────────
// QwenTTS — Alibaba DashScope TTS implementation
// ──────────────────────────────────────────────────────────────

// QwenTTS implements TextToSpeech using Alibaba DashScope multimodal generation API.
type QwenTTS struct {
	APIKey  string
	BaseURL string
	Model   string
	Client  *http.Client
}

// NewQwenTTS creates a new QwenTTS instance.
func NewQwenTTS(apiKey, baseURL, model string, client *http.Client) *QwenTTS {
	if baseURL == "" {
		baseURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
	}
	if model == "" {
		model = "qwen3-tts-flash"
	}
	if client == nil {
		client = &http.Client{Timeout: 60 * time.Second}
	}
	return &QwenTTS{
		APIKey:  apiKey,
		BaseURL: baseURL,
		Model:   model,
		Client:  client,
	}
}

// Synthesize sends text to Qwen TTS API and returns WAV audio bytes.
func (q *QwenTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = "Cherry"
	}
	reqBody := map[string]any{
		"model": q.Model,
	}
	input := map[string]any{
		"text":  text,
		"voice": voice,
	}
	if opts.LanguageType != "" {
		input["language_type"] = opts.LanguageType
	}
	reqBody["input"] = input
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, q.BaseURL, bytes.NewReader(jsonData))
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+q.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := q.Client.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: read response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, "", fmt.Errorf("qwen tts API %d: %s", resp.StatusCode, body)
	}

	var result struct {
		Code    string `json:"code"`
		Message string `json:"message"`
		Output  struct {
			Audio struct {
				URL string `json:"url"`
			} `json:"audio"`
		} `json:"output"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, "", fmt.Errorf("qwen tts: parse response: %w", err)
	}
	if result.Code != "" {
		return nil, "", fmt.Errorf("qwen tts API error %s: %s", result.Code, result.Message)
	}
	if result.Output.Audio.URL == "" {
		return nil, "", fmt.Errorf("qwen tts: empty audio URL in response")
	}

	// Download WAV from temporary URL
	audioReq, err := http.NewRequestWithContext(ctx, http.MethodGet, result.Output.Audio.URL, nil)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: create download request: %w", err)
	}
	audioResp, err := q.Client.Do(audioReq)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: download audio: %w", err)
	}
	defer audioResp.Body.Close()

	wavData, err := io.ReadAll(audioResp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("qwen tts: read audio: %w", err)
	}
	return wavData, "wav", nil
}

// ──────────────────────────────────────────────────────────────
// OpenAITTS — OpenAI-compatible TTS implementation (P1)
// ──────────────────────────────────────────────────────────────

// OpenAITTS implements TextToSpeech using the OpenAI /v1/audio/speech API.
type OpenAITTS struct {
	APIKey  string
	BaseURL string
	Model   string
	Client  *http.Client
}

// NewOpenAITTS creates a new OpenAITTS instance.
func NewOpenAITTS(apiKey, baseURL, model string, client *http.Client) *OpenAITTS {
	if baseURL == "" {
		baseURL = "https://api.openai.com/v1"
	}
	if model == "" {
		model = "tts-1"
	}
	if client == nil {
		client = &http.Client{Timeout: 60 * time.Second}
	}
	return &OpenAITTS{
		APIKey:  apiKey,
		BaseURL: baseURL,
		Model:   model,
		Client:  client,
	}
}

// Synthesize sends text to OpenAI TTS API and returns MP3 audio bytes.
func (o *OpenAITTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = "alloy"
	}
	reqBody := map[string]any{
		"model": o.Model,
		"input": text,
		"voice": voice,
	}
	if opts.Speed > 0 {
		reqBody["speed"] = opts.Speed
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, "", fmt.Errorf("openai tts: marshal request: %w", err)
	}

	url := strings.TrimRight(o.BaseURL, "/") + "/audio/speech"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonData))
	if err != nil {
		return nil, "", fmt.Errorf("openai tts: create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+o.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := o.Client.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("openai tts: request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, "", fmt.Errorf("openai tts API %d: %s", resp.StatusCode, body)
	}

	mp3Data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("openai tts: read audio: %w", err)
	}
	return mp3Data, "mp3", nil
}

// ──────────────────────────────────────────────────────────────
// MiniMaxTTS — MiniMax T2A v2 TTS implementation
// ──────────────────────────────────────────────────────────────

// MiniMaxTTS implements TextToSpeech using the MiniMax T2A v2 API.
type MiniMaxTTS struct {
	APIKey  string
	BaseURL string
	Model   string
	Client  *http.Client
}

// NewMiniMaxTTS creates a new MiniMaxTTS instance.
func NewMiniMaxTTS(apiKey, baseURL, model string, client *http.Client) *MiniMaxTTS {
	if baseURL == "" {
		baseURL = "https://api.minimaxi.com"
	}
	if model == "" {
		model = "speech-2.8-hd"
	}
	if client == nil {
		client = &http.Client{Timeout: 60 * time.Second}
	}
	return &MiniMaxTTS{
		APIKey:  apiKey,
		BaseURL: baseURL,
		Model:   model,
		Client:  client,
	}
}

// Synthesize sends text to MiniMax T2A v2 API and returns MP3 audio bytes.
func (m *MiniMaxTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = "English_Graceful_Lady"
	}
	speed := opts.Speed
	if speed <= 0 {
		speed = 1.0
	}

	reqBody := map[string]any{
		"model":  m.Model,
		"text":   text,
		"stream": true,
		"voice_setting": map[string]any{
			"voice_id": voice,
			"speed":    speed,
		},
		"audio_setting": map[string]any{
			"format":      "mp3",
			"sample_rate": 32000,
		},
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, "", fmt.Errorf("minimax tts: marshal request: %w", err)
	}

	url := strings.TrimRight(m.BaseURL, "/") + "/v1/t2a_v2"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonData))
	if err != nil {
		return nil, "", fmt.Errorf("minimax tts: create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+m.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := m.Client.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("minimax tts: request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, "", fmt.Errorf("minimax tts API %d: %s", resp.StatusCode, body)
	}

	// Parse SSE stream: each line is "data: {...}" with hex-encoded audio chunks.
	var audioBuf bytes.Buffer
	scanner := bufio.NewScanner(resp.Body)
	scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
	for scanner.Scan() {
		select {
		case <-ctx.Done():
			return nil, "", ctx.Err()
		default:
		}
		line := scanner.Text()
		if !strings.HasPrefix(line, "data:") {
			continue
		}
		data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
		if data == "" {
			continue
		}
		var chunk struct {
			Data struct {
				Audio  string `json:"audio"`
				Status int    `json:"status"`
			} `json:"data"`
			BaseResp struct {
				StatusCode int    `json:"status_code"`
				StatusMsg  string `json:"status_msg"`
			} `json:"base_resp"`
		}
		if err := json.Unmarshal([]byte(data), &chunk); err != nil {
			continue
		}
		if chunk.BaseResp.StatusCode != 0 {
			return nil, "", fmt.Errorf("minimax tts API error %d: %s", chunk.BaseResp.StatusCode, chunk.BaseResp.StatusMsg)
		}
		// MiniMax T2A v2 stream protocol: status=1 carries incremental audio
		// chunks; the final status=2 chunk re-sends the full audio as a
		// trailer for non-stream clients. Appending the trailer doubles the
		// audio length and makes the spoken text play twice, so skip it.
		if chunk.Data.Status == 2 {
			break
		}
		if chunk.Data.Audio != "" {
			audioBytes, err := hex.DecodeString(chunk.Data.Audio)
			if err != nil {
				return nil, "", fmt.Errorf("minimax tts: decode audio hex: %w", err)
			}
			audioBuf.Write(audioBytes)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, "", fmt.Errorf("minimax tts: read SSE stream: %w", err)
	}
	if audioBuf.Len() == 0 {
		return nil, "", fmt.Errorf("minimax tts: no audio data received")
	}
	return audioBuf.Bytes(), "mp3", nil
}

// ──────────────────────────────────────────────────────────────
// MimoTTS — Xiaomi MiMo-V2.5-TTS implementation
// ──────────────────────────────────────────────────────────────

// MimoTTS implements TextToSpeech using the Xiaomi MiMo-V2.5-TTS API,
// which is shaped like OpenAI chat completions: the synthesis text rides
// on an assistant message and audio bytes come back base64-encoded inside
// choices[0].message.audio.data.
//
// Docs: https://platform.xiaomimimo.com/#/docs/usage-guide/speech-synthesis
type MimoTTS struct {
	APIKey  string
	BaseURL string
	Model   string
	Client  *http.Client
}

// NewMimoTTS creates a new MimoTTS instance.
func NewMimoTTS(apiKey, baseURL, model string, client *http.Client) *MimoTTS {
	if baseURL == "" {
		baseURL = "https://api.xiaomimimo.com/v1"
	}
	if model == "" {
		model = "mimo-v2.5-tts"
	}
	if client == nil {
		client = &http.Client{Timeout: 60 * time.Second}
	}
	return &MimoTTS{
		APIKey:  apiKey,
		BaseURL: baseURL,
		Model:   model,
		Client:  client,
	}
}

// Synthesize calls MiMo /chat/completions and returns the decoded WAV bytes.
func (m *MimoTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = "mimo_default"
	}

	// Per MiMo docs: synthesis text MUST live on an assistant message; the
	// user message is optional for built-in-voice mode but required for
	// voicedesign. Sending an empty user content stays valid across all
	// three model variants.
	reqBody := map[string]any{
		"model": m.Model,
		"messages": []map[string]any{
			{"role": "user", "content": ""},
			{"role": "assistant", "content": text},
		},
		"audio": map[string]any{
			"format": "wav",
			"voice":  voice,
		},
	}
	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, "", fmt.Errorf("mimo tts: marshal request: %w", err)
	}

	url := strings.TrimRight(m.BaseURL, "/") + "/chat/completions"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonData))
	if err != nil {
		return nil, "", fmt.Errorf("mimo tts: create request: %w", err)
	}
	// MiMo authenticates via "api-key" header, not "Authorization: Bearer".
	req.Header.Set("api-key", m.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := m.Client.Do(req)
	if err != nil {
		return nil, "", fmt.Errorf("mimo tts: request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("mimo tts: read response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, "", fmt.Errorf("mimo tts API %d: %s", resp.StatusCode, body)
	}

	var result struct {
		Choices []struct {
			Message struct {
				Audio struct {
					Data string `json:"data"`
				} `json:"audio"`
			} `json:"message"`
		} `json:"choices"`
		Error *struct {
			Message string `json:"message"`
			Type    string `json:"type"`
			Code    any    `json:"code"`
		} `json:"error"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, "", fmt.Errorf("mimo tts: parse response: %w", err)
	}
	if result.Error != nil && result.Error.Message != "" {
		return nil, "", fmt.Errorf("mimo tts API error: %s", result.Error.Message)
	}
	if len(result.Choices) == 0 || result.Choices[0].Message.Audio.Data == "" {
		return nil, "", fmt.Errorf("mimo tts: empty audio data in response")
	}

	audio, err := base64.StdEncoding.DecodeString(result.Choices[0].Message.Audio.Data)
	if err != nil {
		return nil, "", fmt.Errorf("mimo tts: decode audio base64: %w", err)
	}
	return audio, "wav", nil
}

// ──────────────────────────────────────────────────────────────
// EspeakTTS — Local eSpeak text-to-speech implementation
// ──────────────────────────────────────────────────────────────

// EspeakTTS implements TextToSpeech using the local espeak command.
type EspeakTTS struct {
	Path  string // path to espeak executable (empty = "espeak")
	Voice string // default voice (e.g. "zh", "en", "zh+f3")
}

// NewEspeakTTS creates a new EspeakTTS instance.
func NewEspeakTTS(path, voice string) *EspeakTTS {
	if path == "" {
		path = "espeak"
	}
	if voice == "" {
		voice = "zh" // default to Chinese
	}
	return &EspeakTTS{
		Path:  path,
		Voice: voice,
	}
}

// Synthesize uses espeak to convert text to WAV audio bytes.
func (e *EspeakTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = e.Voice
	}

	// Build espeak command
	args := []string{
		"-v", voice,
		"-w", "/dev/stdout", // write WAV to stdout (Unix-only; not supported on Windows)
	}

	// Add speed option if specified
	if opts.Speed > 0 {
		// espeak speed is in words per minute, default 160
		// Convert speed multiplier (0.5-2.0) to wpm
		wpm := int(160 * opts.Speed)
		args = append(args, "-s", fmt.Sprintf("%d", wpm))
	}

	// Add text as argument
	args = append(args, text)

	// Execute espeak command
	// Use Output() instead of CombinedOutput() to avoid mixing stderr warnings with audio data
	cmd := exec.CommandContext(ctx, e.Path, args...)
	output, err := cmd.Output()
	if err != nil {
		return nil, "", fmt.Errorf("espeak: voice=%s text=%q: %w", voice, text, err)
	}

	return output, "wav", nil
}

// ──────────────────────────────────────────────────────────────
// PicoTTS — Google Pico TTS (better quality than espeak, offline)
// ──────────────────────────────────────────────────────────────

// PicoTTS implements TextToSpeech using pico2wave (Google Pico TTS).
type PicoTTS struct {
	Path  string // path to pico2wave executable (empty = "pico2wave")
	Voice string // default voice language (e.g. "zh-CN", "en-US")
}

// NewPicoTTS creates a new PicoTTS instance.
func NewPicoTTS(path, voice string) *PicoTTS {
	if path == "" {
		path = "pico2wave"
	}
	if voice == "" {
		voice = "zh-CN" // default to Chinese
	}
	return &PicoTTS{
		Path:  path,
		Voice: voice,
	}
}

// Synthesize uses pico2wave to convert text to WAV audio bytes.
// pico2wave produces much better quality than espeak.
func (p *PicoTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = p.Voice
	}

	// Create secure temp file for pico2wave output
	tmpFile, err := os.CreateTemp("", "pico_tts_*.wav")
	if err != nil {
		return nil, "", fmt.Errorf("pico2wave: create temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	tmpFile.Close()
	defer os.Remove(tmpPath)

	// Build pico2wave command
	// --lang: language code (zh-CN for Chinese, en-US for English)
	// --wave: output WAV file path
	args := []string{
		"--lang=" + voice,
		"--wave=" + tmpPath,
		text,
	}

	// Execute pico2wave command
	cmd := exec.CommandContext(ctx, p.Path, args...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, "", fmt.Errorf("pico2wave: voice=%s text=%q: %w, output: %s", voice, text, err, string(output))
	}

	// Read the generated WAV file
	audioData, err := os.ReadFile(tmpPath)
	if err != nil {
		return nil, "", fmt.Errorf("pico2wave: read output file: %w", err)
	}

	if len(audioData) == 0 {
		return nil, "", fmt.Errorf("pico2wave: produced empty audio file")
	}

	return audioData, "wav", nil
}

// ──────────────────────────────────────────────────────────────
// EdgeTTS — Microsoft Edge TTS (free, high quality, requires network)
// ──────────────────────────────────────────────────────────────

// EdgeTTS implements TextToSpeech using Microsoft Edge's free TTS API.
// This uses the edge-tts CLI command under the hood.
type EdgeTTS struct {
	Path  string // path to edge-tts executable (empty = "edge-tts")
	Voice string // default voice (e.g. "zh-CN-XiaoxiaoNeural")
}

// NewEdgeTTS creates a new EdgeTTS instance.
func NewEdgeTTS(voice string) *EdgeTTS {
	if voice == "" {
		voice = "zh-CN-XiaoxiaoNeural" // default Chinese voice
	}
	return &EdgeTTS{
		Voice: voice,
	}
}

// Synthesize uses edge-tts CLI to convert text to MP3 audio bytes.
// EdgeTTS provides high-quality neural voices but requires network connection.
func (e *EdgeTTS) Synthesize(ctx context.Context, text string, opts TTSSynthesisOpts) ([]byte, string, error) {
	voice := opts.Voice
	if voice == "" {
		voice = e.Voice
	}

	// Create secure temp file for edge-tts output
	tmpFile, err := os.CreateTemp("", "edge_tts_*.mp3")
	if err != nil {
		return nil, "", fmt.Errorf("edge-tts: create temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	tmpFile.Close()
	defer os.Remove(tmpPath)

	// Use edge-tts CLI directly to avoid code injection risks
	// Pass text via --text argument, not via embedded code
	args := []string{
		"--voice", voice,
		"--text", text,
		"--write-media", tmpPath,
	}

	path := e.Path
	if path == "" {
		path = "edge-tts"
	}
	cmd := exec.CommandContext(ctx, path, args...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, "", fmt.Errorf("edge-tts: voice=%s text=%q: %w, output: %s", voice, text, err, string(output))
	}

	// Read the generated MP3 file
	audioData, err := os.ReadFile(tmpPath)
	if err != nil {
		return nil, "", fmt.Errorf("edge-tts: read output file: %w", err)
	}

	if len(audioData) == 0 {
		return nil, "", fmt.Errorf("edge-tts: produced empty audio file")
	}

	return audioData, "mp3", nil
}
