// gummy provider：阿里云百炼 DashScope 流式语音识别（gummy-realtime-v1）。
// WS 协议：run-task → 推二进制音频 → finish-task；下行 result-generated / task-finished / task-failed。
// ⚠️ 待真实 DASHSCOPE_API_KEY 到位后联调验证（6B 验收项），协议细节以
// https://help.aliyun.com/zh/model-studio/ 实时语音识别文档为准。
package asr

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

const dashscopeWS = "wss://dashscope.aliyuncs.com/api-ws/v1/inference"

type GummyProvider struct {
	APIKey string
	Model  string // 默认 gummy-realtime-v1

	// spare 预连接（12B/#24）：后台常备一条已完成 run-task 握手的 DashScope
	// 会话，start 直接取用，把 dial+task-started（实测暖路径 ~130ms、抖动可达
	// 2.5~3.7s）从 first_partial 关键路径上移除。协议固定 16kHz，spare 可互换。
	mu          sync.Mutex
	spare       *spareEntry
	dialing     bool
	maintainOne sync.Once
}

type spareEntry struct {
	sess *gummySession
	born time.Time
}

// spareMaxAge spare 的可用年龄上限：DashScope 空闲 60s 断连（实测 60s 存活、
// 120s 报 Idle timeout），留余量在 45s 内取用、40s 定期换新。
const spareMaxAge = 45 * time.Second

func NewGummy(apiKey string) *GummyProvider {
	return &GummyProvider{APIKey: apiKey, Model: "gummy-realtime-v1"}
}

func (g *GummyProvider) Name() string { return "gummy" }

func (g *GummyProvider) StartSession(ctx context.Context, cfg SessionConfig) (Session, error) {
	g.maintainOne.Do(func() { go g.maintainSpare() })
	if cfg.SampleRate == 16000 {
		if s := g.takeSpare(); s != nil {
			go g.replenish()
			return s, nil
		}
		defer func() { go g.replenish() }()
	}
	return g.dialSession(ctx, cfg)
}

// takeSpare 取走当前 spare（若仍新鲜且存活）；过期/已死的就地关闭丢弃。
func (g *GummyProvider) takeSpare() *gummySession {
	g.mu.Lock()
	sp := g.spare
	g.spare = nil
	g.mu.Unlock()
	if sp == nil {
		return nil
	}
	if time.Since(sp.born) < spareMaxAge && sp.sess.alive() {
		slog.Info("gummy: session from spare", "task", sp.sess.taskID,
			"age_ms", time.Since(sp.born).Milliseconds())
		return sp.sess
	}
	_ = sp.sess.Close()
	return nil
}

// replenish 异步补位一条 spare；已有 spare 或正在补位时空转返回。
func (g *GummyProvider) replenish() {
	g.mu.Lock()
	if g.dialing || g.spare != nil {
		g.mu.Unlock()
		return
	}
	g.dialing = true
	g.mu.Unlock()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	sess, err := g.dialSession(ctx, SessionConfig{SampleRate: 16000, SessionID: "spare"})
	cancel()

	g.mu.Lock()
	g.dialing = false
	if err == nil {
		if g.spare == nil {
			g.spare = &spareEntry{sess: sess.(*gummySession), born: time.Now()}
			g.mu.Unlock()
			return
		}
		g.mu.Unlock()
		_ = sess.Close() // 竞态下已有他人补位：丢弃多余会话
		return
	}
	g.mu.Unlock()
	slog.Warn("gummy: spare replenish failed", "err", err)
}

// maintainSpare 每 40s 换新 spare，避免超过 DashScope 60s 空闲上限后
// 取到已死会话、退化为冷启 dial。进程常驻，随 provider 生命周期运行。
func (g *GummyProvider) maintainSpare() {
	t := time.NewTicker(40 * time.Second)
	defer t.Stop()
	for range t.C {
		g.mu.Lock()
		sp := g.spare
		stale := sp != nil && time.Since(sp.born) >= spareMaxAge-5*time.Second
		if stale {
			g.spare = nil
		}
		g.mu.Unlock()
		if stale {
			_ = sp.sess.Close()
		}
		g.replenish()
	}
}

// dialSession 建立一条全新的 DashScope 会话（原 StartSession 主体）。
func (g *GummyProvider) dialSession(ctx context.Context, cfg SessionConfig) (Session, error) {
	header := map[string][]string{
		"Authorization": {"bearer " + g.APIKey},
		"X-DashScope-DataInspection": {"enable"},
	}
	tDial := time.Now()
	conn, _, err := websocket.DefaultDialer.DialContext(ctx, dashscopeWS, header)
	if err != nil {
		return nil, fmt.Errorf("dashscope dial: %w", err)
	}
	dialMs := time.Since(tDial).Milliseconds()

	taskID := uuid.NewString()
	runTask := map[string]any{
		"header": map[string]any{
			"action":    "run-task",
			"task_id":   taskID,
			"streaming": "duplex",
		},
		"payload": map[string]any{
			"task_group": "audio",
			"task":       "asr",
			"function":   "recognition",
			"model":      g.Model,
			"parameters": map[string]any{
				"sample_rate":                cfg.SampleRate,
				"format":                     "pcm",
				"transcription_enabled":      true,
				"translation_enabled":        false,
			},
			"input": map[string]any{},
		},
	}
	if err := conn.WriteJSON(runTask); err != nil {
		conn.Close()
		return nil, fmt.Errorf("dashscope run-task: %w", err)
	}

	s := &gummySession{
		conn:    conn,
		taskID:  taskID,
		results: make(chan Result, 64),
		done:    make(chan struct{}),
		started: make(chan struct{}),
	}
	go s.readLoop()

	// 协议要求：必须等 task-started 后才能推音频，否则服务端静默丢弃
	tWait := time.Now()
	select {
	case <-s.started:
		// 会话建立耗时是 first_partial 延迟的组成部分（12B）：dial 冷启（DNS+TLS）
		// 可达 ~600ms、暖路径 ~60ms，task-started 通常 ~60ms。持续观测供延迟排查。
		slog.Info("gummy: session ready", "task", taskID,
			"dial_ms", dialMs, "task_started_wait_ms", time.Since(tWait).Milliseconds())
		return s, nil
	case <-s.done:
		return nil, fmt.Errorf("dashscope: closed before task-started")
	case <-ctx.Done():
		_ = conn.Close()
		return nil, ctx.Err()
	case <-time.After(10 * time.Second):
		_ = conn.Close()
		return nil, fmt.Errorf("dashscope: task-started timeout")
	}
}

type gummySession struct {
	conn      *websocket.Conn
	taskID    string
	results   chan Result
	done      chan struct{}
	started   chan struct{} // task-started 已收到
	writeMu   sync.Mutex
	closeOnce sync.Once
	startOnce sync.Once
	lastText  string // 当前句已下发文本，用于切分 partial/final
}

// alive 会话底层连接是否仍然存活（readLoop 退出即 done 关闭）。
func (s *gummySession) alive() bool {
	select {
	case <-s.done:
		return false
	default:
		return true
	}
}

func (s *gummySession) SendAudio(pcm []byte) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	select {
	case <-s.done:
		return nil
	default:
	}
	return s.conn.WriteMessage(websocket.BinaryMessage, pcm)
}

// dashscope 下行事件结构（按需字段）。
type dsEvent struct {
	Header struct {
		Event        string `json:"event"` // task-started | result-generated | task-finished | task-failed
		ErrorMessage string `json:"error_message"`
	} `json:"header"`
	Payload struct {
		Output struct {
			// gummy: transcription；paraformer: sentence —— 两种都尝试解析
			Transcription *dsSentence `json:"transcription"`
			Sentence      *dsSentence `json:"sentence"`
		} `json:"output"`
	} `json:"payload"`
}

type dsSentence struct {
	Text          string   `json:"text"`
	SentenceEnd   flexBool `json:"sentence_end"`
	IsSentenceEnd flexBool `json:"is_sentence_end"`
	BeginTime     int64    `json:"begin_time"`
	EndTime       int64    `json:"end_time"`
}

// flexBool 兼容 dashscope 同一字段混用 bool 与字符串（"true"/"false"）的情况。
type flexBool bool

func (b *flexBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case "true", `"true"`:
		*b = true
	default:
		*b = false
	}
	return nil
}

func (s *gummySession) readLoop() {
	defer func() {
		s.closeOnce.Do(func() { close(s.done) })
		close(s.results)
		s.conn.Close()
	}()
	for {
		_, data, err := s.conn.ReadMessage()
		if err != nil {
			select {
			case <-s.done: // 主动关闭
			default:
				s.results <- Result{Err: err}
			}
			return
		}
		if os.Getenv("GUMMY_DEBUG") != "" {
			fmt.Fprintf(os.Stderr, "[gummy] %s\n", data)
		}
		var ev dsEvent
		if json.Unmarshal(data, &ev) != nil {
			continue
		}
		switch ev.Header.Event {
		case "task-started":
			s.startOnce.Do(func() { close(s.started) })
		case "result-generated":
			sen := ev.Payload.Output.Transcription
			if sen == nil {
				sen = ev.Payload.Output.Sentence
			}
			if sen == nil || sen.Text == "" {
				continue
			}
			isFinal := bool(sen.SentenceEnd) || bool(sen.IsSentenceEnd)
			s.results <- Result{Text: sen.Text, IsFinal: isFinal, EndTimeMs: sen.EndTime}
			if isFinal {
				s.lastText = ""
			} else {
				s.lastText = sen.Text
			}
		case "task-finished":
			return
		case "task-failed":
			s.results <- Result{Err: fmt.Errorf("dashscope: %s", ev.Header.ErrorMessage)}
			return
		}
	}
}

func (s *gummySession) Results() <-chan Result { return s.results }

// Close 发 finish-task 让上游 flush 尾部，等 task-finished 后 readLoop 退出。
func (s *gummySession) Close() error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	select {
	case <-s.done:
		return nil
	default:
	}
	finish := map[string]any{
		"header": map[string]any{
			"action":    "finish-task",
			"task_id":   s.taskID,
			"streaming": "duplex",
		},
		"payload": map[string]any{"input": map[string]any{}},
	}
	return s.conn.WriteJSON(finish)
}
