//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

package handler

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	"ragflow/internal/common"
	"ragflow/internal/entity"
	modelModule "ragflow/internal/entity/models"
)

const (
	// chatAudioSpeechMaxBodyBytes caps the JSON body of the TTS endpoint.
	chatAudioSpeechMaxBodyBytes int64 = 1 << 20 // 1 MiB
	// chatAudioUploadMaxBytes caps the multipart body of the transcription endpoint.
	chatAudioUploadMaxBytes int64 = 64 << 20 // 64 MiB
)

// chatAudioSpeechRequest is the request body for POST /api/v1/chat/audio/speech.
type chatAudioSpeechRequest struct {
	Text string `json:"text" binding:"required"`
}

// ttsSegmentSplitRegex splits TTS input into synthesis segments on CJK
// punctuation (，。/《》？；：！), ASCII ':' and ';', and newlines.
var ttsSegmentSplitRegex = regexp.MustCompile("[，。/《》？；：！\\n\\r:;]+")

// ChatAudioSpeech converts text to speech using the tenant's default TTS model
// and streams the concatenated MP3 segments as an audio/mpeg response.
func (h *ChatHandler) ChatAudioSpeech(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	ctx := c.Request.Context()
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, chatAudioSpeechMaxBodyBytes)

	var req chatAudioSpeechRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeArgumentError, nil, err.Error())
		return
	}

	if h.llm == nil {
		common.ErrorWithCode(c, common.CodeServerError, "TTS service not available")
		return
	}

	driver, modelName, apiConfig, _, err := h.llm.GetTenantDefaultModelByType(ctx, user.ID, entity.ModelTypeTTS)
	if err != nil {
		common.ErrorWithCode(c, common.CodeDataError, err.Error())
		return
	}

	segments := ttsSegmentSplitRegex.Split(req.Text, -1)
	headerWritten := false
	for i, seg := range segments {
		seg = strings.TrimSpace(seg)
		if seg == "" {
			continue
		}
		resp, err := driver.AudioSpeech(ctx, &modelName, &seg, apiConfig, &modelModule.TTSConfig{Format: "mp3"}, nil)
		if err != nil {
			common.Warn("chat TTS synthesis failed",
				zap.Int("segmentIndex", i),
				zap.Int("segmentLen", len(seg)),
				zap.Error(err))
			continue
		}
		if resp == nil || len(resp.Audio) == 0 {
			continue
		}
		if !headerWritten {
			// Commit the audio headers only once the first chunk is available,
			// so a fully failed synthesis can still return a JSON error status.
			c.Header("Content-Type", "audio/mpeg")
			c.Header("Cache-Control", "no-cache")
			c.Header("Connection", "keep-alive")
			c.Header("X-Accel-Buffering", "no")
			c.Writer.WriteHeader(http.StatusOK)
			headerWritten = true
		}
		if _, werr := c.Writer.Write(resp.Audio); werr != nil {
			return
		}
		c.Writer.Flush()
	}
	if !headerWritten {
		common.ErrorWithCode(c, common.CodeServerError, "TTS synthesis produced no audio")
	}
}

// chatAudioAllowedExts is the set of audio file extensions accepted by the
// transcription endpoint.
var chatAudioAllowedExts = map[string]struct{}{
	".wav":  {},
	".mp3":  {},
	".m4a":  {},
	".aac":  {},
	".flac": {},
	".ogg":  {},
	".webm": {},
	".opus": {},
	".wma":  {},
}

// chatAudioAllowedExtsList is the sorted, human-readable form of
// chatAudioAllowedExts, computed once for error messages.
var chatAudioAllowedExtsList = func() string {
	exts := make([]string, 0, len(chatAudioAllowedExts))
	for ext := range chatAudioAllowedExts {
		exts = append(exts, ext)
	}
	sort.Strings(exts)
	return strings.Join(exts, ", ")
}()

// ChatAudioTranscription transcribes an uploaded audio file using the tenant's
// default ASR model. It supports both a single JSON response and SSE streaming.
func (h *ChatHandler) ChatAudioTranscription(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	if h.llm == nil {
		common.ErrorWithCode(c, common.CodeServerError, "ASR service not available")
		return
	}

	ctx := c.Request.Context()
	// Cap the body before any multipart parsing so an oversized upload is
	// rejected instead of being drained into memory or spooled to disk.
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, chatAudioUploadMaxBytes)
	if cl := c.Request.ContentLength; cl > chatAudioUploadMaxBytes {
		common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "request body too large.")
		return
	}
	if err := c.Request.ParseMultipartForm(chatAudioUploadMaxBytes); err != nil {
		common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "invalid multipart form: "+err.Error())
		return
	}
	defer func() {
		if c.Request.MultipartForm != nil {
			_ = c.Request.MultipartForm.RemoveAll()
		}
	}()

	fileHeader, err := c.FormFile("file")
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "Missing 'file' in multipart form-data")
		return
	}

	suffix := strings.ToLower(filepath.Ext(fileHeader.Filename))
	if _, ok := chatAudioAllowedExts[suffix]; suffix == "" || !ok {
		common.ResponseWithCodeData(c, common.CodeArgumentError, nil,
			fmt.Sprintf("Unsupported audio format: %s. Allowed: %s", suffix, chatAudioAllowedExtsList))
		return
	}

	// Save the uploaded file to a temporary location so the model driver can read it.
	tmpFile, err := os.CreateTemp("", "*"+suffix)
	if err != nil {
		common.ErrorWithCode(c, common.CodeServerError, "Failed to create temp audio file: "+err.Error())
		return
	}
	tmpPath := tmpFile.Name()
	_ = tmpFile.Close()
	defer func() { _ = os.Remove(tmpPath) }()

	if err := c.SaveUploadedFile(fileHeader, tmpPath); err != nil {
		common.ErrorWithCode(c, common.CodeServerError, "Failed to save audio file: "+err.Error())
		return
	}

	driver, modelName, apiConfig, _, err := h.llm.GetTenantDefaultModelByType(ctx, user.ID, entity.ModelTypeSpeech2Text)
	if err != nil {
		common.ErrorWithCode(c, common.CodeDataError, err.Error())
		return
	}

	streamMode := strings.ToLower(c.PostForm("stream")) == "true"
	if streamMode {
		disableWriteDeadlineForSSE(c)
		c.Header("Content-Type", "text/event-stream")
		c.Header("Cache-Control", "no-cache")
		c.Header("Connection", "keep-alive")
		c.Header("X-Accel-Buffering", "no")
		c.Writer.WriteHeader(http.StatusOK)

		doneSent := false
		sender := func(content, _ *string) error {
			if content == nil {
				return nil
			}
			event := map[string]interface{}{"text": *content}
			if *content == "[DONE]" {
				event["event"] = "done"
				doneSent = true
			} else {
				event["event"] = "partial"
			}
			data, _ := json.Marshal(event)
			if _, err := c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data)); err != nil {
				return err
			}
			c.Writer.Flush()
			return nil
		}

		if err = driver.TranscribeAudioWithSender(ctx, &modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil, sender); err != nil {
			errEvent := map[string]interface{}{"event": "error", "text": err.Error()}
			data, _ := json.Marshal(errEvent)
			_, _ = c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data))
			c.Writer.Flush()
			return
		}
		if !doneSent {
			// Drivers are expected to terminate the stream with "[DONE]"; send
			// it here when they do not, so clients always see a completion event.
			doneEvent := map[string]interface{}{"event": "done", "text": "[DONE]"}
			data, _ := json.Marshal(doneEvent)
			_, _ = c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data))
			c.Writer.Flush()
		}
		return
	}

	resp, err := driver.TranscribeAudio(ctx, &modelName, &tmpPath, apiConfig, &modelModule.ASRConfig{}, nil)
	if err != nil {
		common.ErrorWithCode(c, common.CodeServerError, err.Error())
		return
	}
	if resp == nil {
		common.ErrorWithCode(c, common.CodeServerError, "empty transcription response")
		return
	}

	common.SuccessWithData(c, map[string]string{"text": resp.Text}, "success")
}
