// Package tts metni sese çevirir (Phase 7, "TTS").
//
// TASARIM (stt paketinin simetriği): motor seçimi KULLANICININ kararıdır; bu paket
// yalnızca ARAYÜZÜ tanımlar. Sağlanan iki uygulama:
//   - NewOpenAI: OpenAI-uyumlu /v1/audio/speech ucu (bulut veya yerel uyumlu sunucu)
//   - NewPiper:  Piper HTTP sunucusu (yerel, gizlilik korumalı)
//
// Neden arayüz? Oturum/WS/ayar katmanı motoru tanımaz. Başka bir TTS motoru
// (Coqui, XTTS, Windows SAPI...) eklemek YALNIZCA bu pakete yeni bir dosya demektir.
//
// Neden cümle bölme? Motorlar uzun metni ya reddeder ya da çok yavaş konuşur.
// Cümle cümle sentezlemek ilk sesi hızlı getirir ve bir cümlede takılınan
// yarım kalmış sesi atmayı sağlar.
package tts

import (
	"context"
	"errors"
	"strings"
	"time"
)

// Speech üretilen ses parçasıdır.
type Speech struct {
	Audio []byte // ham ses baytları
	Mime  string // içerik türü (audio/mpeg, audio/wav, audio/ogg ...)
}

// Synthesizer metni sese çevirir.
type Synthesizer interface {
	Speak(ctx context.Context, text string) (Speech, error)
}

// Hatalar: WS katmanı bunları kullanıcıya anlaşılır mesajla çevirir.
var (
	// ErrNoSynthesizer TTS motoru yapılandırılmamış (sunucu ses üretmez; metin akışı sürer).
	ErrNoSynthesizer = errors.New("tts: ses motoru yapılandırılmamış")
	// ErrEmptyText okunacak metin yok (yalnızca kod bloğu, bağlantı, boş yanıt).
	ErrEmptyText = errors.New("tts: okunacak metin yok")
	// ErrTooLong metin motorun kabul ettiği sınırın üstünde.
	ErrTooLong = errors.New("tts: metin çok uzun")
	// ErrUnavailable motor yanıt vermedi (kapalı, zaman aşımı, hatalı yanıt). GEÇİCİDİR.
	ErrUnavailable = errors.New("tts: ses motoru yanıt vermedi")
)

// Varsayılanlar (motor bağımsız güvenli sınırlar).
const (
	// DefaultMaxChars tek çağrıda gönderilecek en uzun metin. 500 karakter ≈ 40 saniye;
	// uzun metinler cümle bölme ile küçültülür.
	//
	// Neden 160? Gerçek zamanlı hedefi. T3 otoregresif döngüsü metin uzunluğuyla
	// doğrusal büyür: 500 karakterlik tek cümle ~25 sn sürer ve kullanıcı o süre
	// bekler (bazı motorlarda ise süre aşımına uğrar). 160 karakter ~3-8 sn'de
	// üretilir; akışlı seslendirme bunu ilk cümle için olduğu kadar sonrakiler için de
	// geçerli kılar: kullanıcı cümleleri sırayla, ara boşlukla duyar.
	DefaultMaxChars = 160
	// DefaultTimeout tek cümle sentezinin üst sınırı. Gerçek zamanlı hissi korumak
	// için kasıtlı kısa: geciken ses kullanıcıya rahatsızlık verir.
	// NOT: VRAM dolu olduğunda (LLM + TTS aynı GPU'da) RTF 2'ye çıkabiliyor;
	// o durumda 160 karakter ~20 sn sürer ve 60 sn sınırına takılır.
	DefaultTimeout = 60 * time.Second
)

// Options motor ayarlarıdır.
type Options struct {
	// URL motorun uç noktası (OpenAI-uyumlu: ".../v1/audio/speech", Piper: ".../api/tts").
	// Boşsa motor yapılandırılmamış demektir.
	URL string
	// APIKey isteğe bağlı taşıyıcı belirteçtir (bulut motorlar).
	APIKey string
	// Model motor/ses modeli adı (OpenAI-uyumlu: tts-1, gpt-4o-mini-tts ...).
	Model string
	// Voice ses kimliği (OpenAI-uyumlu: alloy; Piper: tr_TR-female ...).
	Voice string
	// Format istenen ses biçimi (mp3, wav, ogg, opus). Boşsa motorun varsayılanı.
	Format string
	// Speed konuşma hızı (OpenAI-uyumlu: 0.5–2.0; Piper: length_scale).
	// 0 ise motorun varsayılanı kullanılır.
	Speed float64
	// Timeout tek çağrının üst sınırı; 0 ise DefaultTimeout.
	Timeout time.Duration
}

// Plain, markdown metnini OKUNABİLİR düz metne çevirir.
//
// Neden? Model yanıtları markdown döner ("**kalın**", "`kod`", "- liste", "```kod```").
// Bunlar harf harf okunursa kullanıcıya saçmalık dinletir. İşaretler kaldırılır,
// kod BLOKLARI atlanır (sözlü kod yararsızdır; geriye okunacak metin kalmazsa sonuç boştur).
func Plain(text string) string {
	if text == "" {
		return ""
	}
	s := stripCodeBlocks(text)
	s = replaceLinks(s)
	s = strings.ReplaceAll(s, "`", " ")

	// Satır satır temizle: yatay çizgi satırları silinir, işaretler kaldırılır ve
	// satıra cümle sonu eklenir (okuma için iki satır iki ayrı cümledir).
	var parts []string
	for _, ln := range strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n") {
		if t := cleanLine(ln); t != "" {
			parts = append(parts, t)
		}
	}
	s = strings.Join(parts, " ")

	// Kalan vurgu işaretleri. NOT: TEK "*" korunur — "2 * 3 = 6" ifadesi
	// italik sanılıp bozulmamalı; markdown'da tek yıldız nadirdir.
	s = strings.NewReplacer("**", "", "__", "", "~~", "", "…", "...").Replace(s)
	s = strings.Join(strings.Fields(s), " ")
	return strings.TrimSpace(s)
}

// cleanLine tek satırı okunur cümle parçasına çevirir; boş satır için "" döner.
func cleanLine(ln string) string {
	t := strings.TrimSpace(ln)
	if t == "" {
		return ""
	}
	// Yatay çizgi / bölüm çizgisi: sözlü bir karşılığı yoktur.
	if t == "---" || t == "***" || t == "___" || t == "===" {
		return ""
	}
	// Tablo kenarı.
	t = strings.TrimSpace(strings.Trim(t, "|"))
	if t == "" {
		return ""
	}
	// Başlık, liste, alıntı işaretlerini at.
	if strings.ContainsRune("#>*-•·", rune(t[0])) {
		t = strings.TrimSpace(strings.TrimLeft(t, "#>*-•·"))
	}
	// Numaralı liste: "1. " / "1) ".
	for len(t) > 1 && t[0] >= '0' && t[0] <= '9' && (t[1] == '.' || t[1] == ')') {
		t = strings.TrimSpace(t[2:])
	}
	if t == "" {
		return ""
	}
	if !endsWithPunct(t) {
		t += "."
	}
	return t
}

// Split, metni okunabilir cümlelere böler ve maxChars sınırına uydurur.
// Boş parça üretmez; okunacak metin yoksa nil döner.
func Split(text string, maxChars int) []string {
	s := Plain(text)
	if s == "" {
		return nil
	}
	if maxChars <= 0 {
		maxChars = DefaultMaxChars
	}
	var out []string
	for _, sent := range splitSentences(s) {
		out = append(out, hardSplit(sent, maxChars)...)
	}
	return out
}

// stripCodeBlocks, ```...``` aralıklarını ATAR (yerine hiçbir şey yazmaz).
//
// Neden nokta eklemiyoruz? Satır temizliği zaten cümle sonu koyuyor; burada ayrıca
// eklemek "Merhaba.. .. Görüşürüz" gibi çift noktalı bozuk metin üretiyordu (test, canlı bulgu).
func stripCodeBlocks(s string) string {
	var sb strings.Builder
	rest := s
	for {
		i := strings.Index(rest, "```")
		if i < 0 {
			sb.WriteString(rest)
			return sb.String()
		}
		sb.WriteString(rest[:i])
		rest = rest[i+3:]
		if j := strings.Index(rest, "```"); j >= 0 {
			rest = rest[j+3:]
		} else {
			rest = "" // kapanış yok: sonuna kadar at
		}
	}
}

// replaceLinks, "[metin](url)" biçimini "metin" yapar.
//
// DİKKAT: köşeli parantezden ÖNCEKİ metin korunmalı. "bkz. [doküman](x) şeyinde"
// yanlışlıkla "doküman şeyinde" olurdu — önceki yazımda "bkz. " kayboluyordu (test, canlı bulgu).
func replaceLinks(s string) string {
	var sb strings.Builder
	rest := s
	for {
		i := strings.Index(rest, "](")
		if i < 0 {
			break
		}
		open := strings.LastIndex(rest[:i], "[")
		if open < 0 {
			sb.WriteString(rest[:i+2]) // eslesen '[' yok: oldugu gibi yaz
			rest = rest[i+2:]
			continue
		}
		j := strings.Index(rest[i:], ")")
		if j < 0 {
			break // kap anis yok: kalan ham yazilir
		}
		sb.WriteString(rest[:open])      // '[' ONCESI metin (parantezin kendisi atilir)
		sb.WriteString(rest[open+1 : i]) // baglanti METNI
		rest = rest[i+j+1:]              // '](' ve adres atilir
	}
	// Kalan metin bosluklariyla YALNIZ BIR KEZ yazilir (dongude yazmak
	// 'dosyasi dosyasi' gibi tekrar uretiyordu - test, canli bulgu).
	sb.WriteString(rest)
	return sb.String()
}

// splitSentences, cümle sonu noktalama işaretlerinde böler (Türkçe dâhil).
//
// NOT: "…" Unicode'dur; byte dizisi üzerinde `r == '…'` karşılaştırması derlenmez
// (8230 > 255). Bu yüzden metin önce "..."e çevrilir (Plain yapar) ve burada
// ASCII noktalama taranır.
//
// "3.14" / "com.tr" gibi noktaları yanlışlıkla cümle sonu saymamak için noktadan
// sonra BOŞLUK gelmiyorsa bölmez.
func splitSentences(s string) []string {
	var out []string
	start := 0
	for i := 0; i < len(s); i++ {
		r := s[i]
		if r != '.' && r != '!' && r != '?' && r != ';' && r != ':' {
			continue
		}
		if i+1 >= len(s) {
			break // metnin sonu: aşağıdaki son bloğu ekler
		}
		if !isSpace(s[i+1]) {
			continue // kelime içi noktalama
		}
		j := i + 1
		for j < len(s) && (s[j] == '!' || s[j] == '?' || s[j] == '.' || s[j] == '"') {
			j++
		}
		if seg := strings.TrimSpace(s[start:j]); seg != "" {
			out = append(out, seg)
		}
		start = j
		i = j - 1
	}
	if seg := strings.TrimSpace(s[start:]); seg != "" {
		out = append(out, seg)
	}
	return out
}

// hardSplit, aşırı uzun cümlüyü kelime sınırında böler (motor sınırı).
// Sınırı aşan TEK kelime kesilmez: kelime bölmek yanlış telaffuz edilir.
func hardSplit(sentence string, maxChars int) []string {
	if len([]rune(sentence)) <= maxChars {
		return []string{sentence}
	}
	var out []string
	var cur strings.Builder
	for _, w := range strings.Fields(sentence) {
		if cur.Len() > 0 && cur.Len()+1+len(w) > maxChars {
			out = append(out, strings.TrimSpace(cur.String()))
			cur.Reset()
		}
		if cur.Len() > 0 {
			cur.WriteByte(' ')
		}
		cur.WriteString(w)
	}
	if rest := strings.TrimSpace(cur.String()); rest != "" {
		out = append(out, rest)
	}
	return out
}

func isSpace(b byte) bool { return b == ' ' || b == '\t' || b == '\n' }

func endsWithPunct(s string) bool {
	if s == "" {
		return false
	}
	return strings.ContainsRune(".!?;", rune(s[len(s)-1]))
}
