// openai.go OpenAI-uyumlu /v1/audio/speech ucunu konuşur.
//
// Bu ucu birçok motor sunar: OpenAI/Google/Azure bulutları, LM Studio, LocalAI,
// Coqui ve XTTS tabanlı sunucular. Bu yüzden FİKOM'da "bulut" ile "yerel" ayrımı
// değil, PROTOCOL ayrımı önemlidir: OpenAI-uyumlu olan motorlar aynı kodla çalışır.
//
// İstek: POST JSON {model, input, voice, response_format, speed}
// Yanıt: ham ses baytları (audio/mpeg varsayılan).
package tts

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
)

// OpenAI, OpenAI-uyumlu /v1/audio/speech uygulamasıdır.
type OpenAI struct {
	url    string
	apikey string
	model  string
	voice  string
	format string
	speed  float64
	client *http.Client
}

// NewOpenAI tanım oluşturur. URL boşsa nil döner (TTS yapılandırılmamış demektir).
func NewOpenAI(opt Options) *OpenAI {
	url := strings.TrimSpace(opt.URL)
	if url == "" {
		return nil
	}
	timeout := opt.Timeout
	if timeout <= 0 {
		timeout = DefaultTimeout
	}
	format := opt.Format
	if format == "" {
		format = "mp3"
	}
	voice := opt.Voice
	if voice == "" {
		voice = "alloy"
	}
	model := opt.Model
	if model == "" {
		model = "tts-1"
	}
	return &OpenAI{
		url:    url,
		apikey: strings.TrimSpace(opt.APIKey),
		model:  model,
		voice:  voice,
		format: format,
		speed:  opt.Speed,
		client: &http.Client{Timeout: timeout},
	}
}

// Speak metni sese çevirir.
func (o *OpenAI) Speak(ctx context.Context, text string) (Speech, error) {
	if o == nil {
		return Speech{}, ErrNoSynthesizer
	}
	text = Plain(text)
	if text == "" {
		return Speech{}, ErrEmptyText
	}

	req := map[string]any{"model": o.model, "input": text, "voice": o.voice, "response_format": o.format}
	if o.speed > 0 {
		req["speed"] = o.speed
	}
	body, err := json.Marshal(req)
	if err != nil {
		return Speech{}, err
	}

	hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, o.url, bytes.NewReader(body))
	if err != nil {
		return Speech{}, err
	}
	hreq.Header.Set("Content-Type", "application/json")
	if o.apikey != "" {
		hreq.Header.Set("Authorization", "Bearer "+o.apikey)
	}

	resp, err := o.client.Do(hreq)
	if err != nil {
		return Speech{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		raw, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return Speech{}, fmt.Errorf("%w: HTTP %d: %s", ErrUnavailable, resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	// Yanıt gövdesinin TAMAMı ses dosyasıdır (JSON değil) — bu yüzden kırpma yok.
	audio, err := io.ReadAll(resp.Body)
	if err != nil {
		return Speech{}, fmt.Errorf("%w: gövde okunamadı: %v", ErrUnavailable, err)
	}
	if len(audio) == 0 {
		return Speech{}, fmt.Errorf("%w: boş ses", ErrUnavailable)
	}
	return Speech{Audio: audio, Mime: mimeFor(o.format)}, nil
}

// mimeFor ses biçimini içerik türüne çevirir.
func mimeFor(format string) string {
	switch strings.ToLower(strings.TrimSpace(format)) {
	case "wav":
		return "audio/wav"
	case "opus":
		return "audio/ogg"
	case "ogg":
		return "audio/ogg"
	case "pcm":
		return "audio/pcm"
	default:
		return "audio/mpeg"
	}
}
