//
//  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.
//

// tts_dispatch.go — TTS dispatcher interface for the audio package.
//
// The audio package's ModelProviderFunc contract (see
// model_provider_synthesizer.go) is a function-typed seam that the
// production boot (cmd/server_main.go) plugs in. This file extracts
// the dispatch logic into a small `TTSDispatcher` interface so the
// dispatch can be unit-tested without the audio package depending on
// internal/service. The interface is the minimum surface the audio
// package needs:
//
//   - Synthesize: a single method that the model's audio driver
//     actually exposes (see internal/entity/models/types.go:32-33
//     BaseModel.AudioSpeech); everything else (provider lookup,
//     tenant resolution, fallback model selection) is the model's
//     own internal responsibility.
//
// The audio package does not import internal/service directly; it
// takes a TTSDispatcher (typically the *service.ModelProviderService
// instance installed at boot). The function returns a non-nil
// SynthesizeResponse on success and a non-nil error on every
// failure path; the audio package's caller (modelProviderSynthesizer)
// maps nil-error-with-empty-audio to ErrSynthesizeEmpty and nil-
// error-with-non-empty-audio to a clean pass.

package audio

import (
	"context"
	"fmt"

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

// TTSDispatcher is the minimum interface the audio package needs
// from the project's model provider service. It mirrors the
// *service.ModelProviderService.AudioSpeech method shape so the
// production wiring is a one-line cast. Tests can substitute a
// stub without spinning up a real model driver.
//
// The signature matches the real AudioSpeech exactly (including
// the common.ErrorCode return) so no adapter wrapper is needed
// at the call site. A non-CodeSuccess return is treated as an
// error; the audio package propagates the error to the SSE
// consumer.
type TTSDispatcher interface {
	AudioSpeech(
		ctx context.Context,
		providerName, instanceName, modelName, modelID *string,
		userID string,
		audioContent *string,
		apiConfig *modelModule.APIConfig,
		modelConfig *modelModule.TTSConfig,
	) (*modelModule.TTSResponse, common.ErrorCode, error)
}

// NewTTSDispatchFunc returns an audio.ModelProviderFunc that
// dispatches a SynthesizeRequest to the supplied TTSDispatcher.
//
// Field mapping (audio.SynthesizeRequest → model dispatch):
//
//   - ModelProviderRequest.ModelName (from req.Engine)  → modelName
//     The Engine field is repurposed as a model identifier hint
//     in the audio package's contract. Empty falls through to the
//     model's default TTS model.
//   - Text  → audioContent
//   - Voice → TTSConfig.Params["voice"]
//   - Lang  → TTSConfig.Params["lang"]
//
// Error contract: a non-nil error short-circuits the audio
// package's cache (no write) and surfaces to the caller as a
// failed Synthesize. A nil error with nil TTSResponse or empty
// audio is also an error (the audio package treats it as
// "model produced no audio"); we surface that as
// ErrSynthesizeEmpty so the failure is observable in logs.
func NewTTSDispatchFunc(d TTSDispatcher) ModelProviderFunc {
	if d == nil {
		return nil
	}
	return func(ctx context.Context, req ModelProviderRequest) (*SynthesizeResponse, error) {
		// ModelName / Engine may both be empty; both are legal —
		// the model dispatcher will fall back to the tenant's
		// default TTS model in that case.
		var modelName *string
		if req.ModelName != "" {
			mn := req.ModelName
			modelName = &mn
		}

		// We don't have a per-request APIConfig; leave nil so
		// the model's default credentials / base URL take effect.
		var apiConfig *modelModule.APIConfig

		// Build a TTSConfig from the request's voice + lang so
		// the model driver can select a voice variant when the
		// provider supports it (e.g. OpenAI's alloy/echo fable,
		// edge-tts' voice short-name).
		ttsConfig := &modelModule.TTSConfig{Params: map[string]any{}}
		if req.Voice != "" {
			ttsConfig.Params["voice"] = req.Voice
		}
		if req.Lang != "" {
			ttsConfig.Params["lang"] = req.Lang
		}
		if len(ttsConfig.Params) == 0 {
			ttsConfig.Params = nil
		}

		text := req.Text
		resp, code, err := d.AudioSpeech(
			ctx,
			nil, // providerName — let the dispatcher resolve by name
			nil, // instanceName — same
			modelName,
			nil, // modelID — look up by (provider, instance, model)
			req.TenantID,
			&text,
			apiConfig,
			ttsConfig,
		)
		if err != nil {
			return nil, fmt.Errorf("audio: TTS model-provider dispatch: %w", err)
		}
		if code != common.CodeSuccess {
			return nil, fmt.Errorf("audio: TTS model-provider dispatch: code=%d", code)
		}
		if resp == nil || len(resp.Audio) == 0 {
			return nil, ErrSynthesizeEmpty
		}
		return &SynthesizeResponse{
			Audio:     resp.Audio,
			MediaType: "audio/mpeg",
		}, nil
	}
}
