package dingtalk

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"sync"
	"time"

	"github.com/chenhg5/cc-connect/core"
)

// aiCard implements core.StreamingCard for DingTalk AI Card streaming.
type aiCard struct {
	cardInstanceId string
	outTrackId     string
	templateKey    string // 卡片模板变量名，默认 "content"
	platform       *Platform

	mu              sync.Mutex
	state           string // "processing" | "finished" | "failed"
	lastSentContent string
	lastSentAt      time.Time

	// 节流控制（single-flight + latest-wins 语义）
	throttleMs     int
	pendingContent string
	timer          *time.Timer
	inFlight       bool
	done           chan struct{} // closed when finalized or failed
}

// Ensure aiCard implements core.StreamingCard
var _ core.StreamingCard = (*aiCard)(nil)

// generateOutTrackID generates a unique outTrackId for AI Card.
func generateOutTrackID() string {
	b := make([]byte, 8)
	_, _ = rand.Read(b)
	return fmt.Sprintf("card_%d_%s", time.Now().UnixMilli(), hex.EncodeToString(b))
}

// generateGUID generates a UUID-like string for API requests.
func generateGUID() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	// Set version (4) and variant bits
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}

// createAICard creates a new AI Card instance and delivers it to the conversation.
func (p *Platform) createAICard(ctx context.Context, rc replyContext) (*aiCard, error) {
	token, err := p.getAccessToken()
	if err != nil {
		return nil, fmt.Errorf("get access token: %w", err)
	}

	outTrackId := generateOutTrackID()
	isGroup := rc.isGroup

	// Build openSpaceId based on conversation type
	// See: https://open.dingtalk.com/document/development/create-and-deliver-cards
	var openSpaceId string
	if isGroup {
		openSpaceId = fmt.Sprintf("dtv1.card//IM_GROUP.%s", rc.conversationId)
	} else {
		openSpaceId = fmt.Sprintf("dtv1.card//IM_ROBOT.%s", rc.senderStaffId)
	}

	// Build card data
	cardParamMap := map[string]string{
		"config":          `{"autoLayout":true,"enableForward":true}`,
		p.cardTemplateKey: "",
	}

	payload := map[string]any{
		"cardTemplateId": p.cardTemplateID,
		"outTrackId":     outTrackId,
		"cardData": map[string]any{
			"cardParamMap": cardParamMap,
		},
		"callbackType":          "STREAM",
		"imGroupOpenSpaceModel": map[string]any{"supportForward": true},
		"imRobotOpenSpaceModel": map[string]any{"supportForward": true},
		"openSpaceId":           openSpaceId,
		"userIdType":            1,
	}

	// Set delivery model based on conversation type
	if isGroup {
		payload["imGroupOpenDeliverModel"] = map[string]any{
			"robotCode": p.robotCode,
		}
	} else {
		payload["imRobotOpenDeliverModel"] = map[string]any{
			"spaceType": "IM_ROBOT",
			"robotCode": p.robotCode,
		}
	}

	bodyBytes, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal payload: %w", err)
	}

	reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(reqCtx, http.MethodPost,
		"https://api.dingtalk.com/v1.0/card/instances/createAndDeliver",
		bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-acs-dingtalk-access-token", token)

	slog.Debug("dingtalk: creating AI card", "outTrackId", outTrackId, "isGroup", isGroup)

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)

	slog.Debug("dingtalk: createAndDeliver response",
		"status", resp.StatusCode,
		"body", string(respBody))

	if resp.StatusCode != http.StatusOK {
		slog.Error("dingtalk: create AI card failed",
			"status", resp.StatusCode,
			"body", string(respBody))
		// Check if we should trigger degrade
		if resp.StatusCode == 403 || resp.StatusCode == 429 || resp.StatusCode >= 500 {
			p.activateCardDegrade(fmt.Sprintf("card.create:%d", resp.StatusCode))
		}
		return nil, fmt.Errorf("create AI card: status=%d, body=%s", resp.StatusCode, string(respBody))
	}

	// Parse response to get cardInstanceId
	var result struct {
		Result struct {
			CardInstanceId  string `json:"cardInstanceId"`
			OutTrackId      string `json:"outTrackId"`
			ProcessQueryKey string `json:"processQueryKey"`
		} `json:"result"`
		CardInstanceId string `json:"cardInstanceId"`
		OutTrackId     string `json:"outTrackId"`
	}
	if err := json.Unmarshal(respBody, &result); err != nil {
		slog.Warn("dingtalk: failed to parse createAndDeliver response", "error", err, "body", string(respBody))
	}

	cardInstanceId := result.Result.CardInstanceId
	if cardInstanceId == "" {
		cardInstanceId = result.CardInstanceId
	}
	if cardInstanceId == "" {
		cardInstanceId = outTrackId
	}

	resolvedOutTrackId := result.Result.OutTrackId
	if resolvedOutTrackId == "" {
		resolvedOutTrackId = result.OutTrackId
	}
	if resolvedOutTrackId == "" {
		resolvedOutTrackId = outTrackId
	}

	// Check deliverResults for actual delivery success
	var deliverCheck struct {
		Result struct {
			DeliverResults []struct {
				Success  bool   `json:"success"`
				ErrorMsg string `json:"errorMsg"`
			} `json:"deliverResults"`
		} `json:"result"`
	}
	if err := json.Unmarshal(respBody, &deliverCheck); err == nil {
		for _, dr := range deliverCheck.Result.DeliverResults {
			if !dr.Success {
				slog.Warn("dingtalk: AI card delivery failed",
					"errorMsg", dr.ErrorMsg,
					"outTrackId", outTrackId,
					"isGroup", isGroup)
				return nil, fmt.Errorf("AI card delivery failed: %s", dr.ErrorMsg)
			}
		}
	}

	slog.Info("dingtalk: AI card created",
		"cardInstanceId", cardInstanceId,
		"outTrackId", resolvedOutTrackId)

	card := &aiCard{
		cardInstanceId: cardInstanceId,
		outTrackId:     resolvedOutTrackId,
		templateKey:    p.cardTemplateKey,
		platform:       p,
		state:          "processing",
		throttleMs:     p.cardThrottleMs,
		done:           make(chan struct{}),
	}

	return card, nil
}

// Update replaces the card content with the given markdown.
// Implements throttling using single-flight + latest-wins semantics.
func (c *aiCard) Update(ctx context.Context, content string) error {
	c.mu.Lock()

	// If already finished or failed, skip
	if c.state == "finished" || c.state == "failed" {
		c.mu.Unlock()
		return nil
	}

	c.pendingContent = content

	// If there's an in-flight request, schedule a timer
	if c.inFlight {
		c.scheduleFlushLocked()
		c.mu.Unlock()
		return nil
	}

	// If enough time has passed since last send, flush immediately
	if c.timer == nil && time.Since(c.lastSentAt) >= time.Duration(c.throttleMs)*time.Millisecond {
		c.mu.Unlock()
		c.flush(ctx)
		return nil
	}

	// Otherwise, schedule a timer
	c.scheduleFlushLocked()
	c.mu.Unlock()
	return nil
}

// scheduleFlushLocked schedules a flush after throttleMs. Must be called with mu held.
func (c *aiCard) scheduleFlushLocked() {
	if c.timer != nil {
		return
	}
	delay := time.Duration(c.throttleMs)*time.Millisecond - time.Since(c.lastSentAt)
	if delay < 0 {
		delay = 0
	}
	c.timer = time.AfterFunc(delay, func() {
		c.mu.Lock()
		c.timer = nil
		c.mu.Unlock()
		c.flush(context.Background())
	})
}

// flush sends the pending content to the DingTalk streaming API.
func (c *aiCard) flush(ctx context.Context) {
	c.mu.Lock()
	if c.state == "finished" || c.state == "failed" {
		c.mu.Unlock()
		return
	}
	if c.pendingContent == "" {
		c.mu.Unlock()
		return
	}
	if c.inFlight {
		c.mu.Unlock()
		return
	}

	content := c.pendingContent
	c.pendingContent = ""
	c.inFlight = true
	c.mu.Unlock()

	err := c.doStream(ctx, content, false)

	c.mu.Lock()
	c.inFlight = false
	if err != nil {
		slog.Error("dingtalk: AI card stream update failed", "error", err)
		// Check pending content that arrived during in-flight
		if c.pendingContent != "" {
			c.scheduleFlushLocked()
		}
	} else {
		c.lastSentContent = content
		c.lastSentAt = time.Now()
		// Check if new content arrived during in-flight
		if c.pendingContent != "" {
			c.scheduleFlushLocked()
		}
	}
	c.mu.Unlock()
}

// doStream sends content to the DingTalk streaming API.
func (c *aiCard) doStream(ctx context.Context, content string, isFinalize bool) error {
	token, err := c.platform.getAccessToken()
	if err != nil {
		return fmt.Errorf("get access token: %w", err)
	}

	payload := map[string]any{
		"outTrackId": c.outTrackId,
		"key":        c.templateKey,
		"content":    content,
		"isFull":     true,
		"isFinalize": isFinalize,
		"isError":    false,
		"guid":       generateGUID(),
	}

	bodyBytes, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal payload: %w", err)
	}

	reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(reqCtx, http.MethodPut,
		"https://api.dingtalk.com/v1.0/card/streaming",
		bytes.NewReader(bodyBytes))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-acs-dingtalk-access-token", token)

	slog.Debug("dingtalk: streaming AI card",
		"outTrackId", c.outTrackId,
		"contentLen", len(content),
		"isFinalize", isFinalize)

	resp, err := c.platform.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)

	slog.Debug("dingtalk: streaming response",
		"status", resp.StatusCode,
		"body", string(respBody),
		"isFinalize", isFinalize)

	if resp.StatusCode != http.StatusOK {
		slog.Error("dingtalk: stream AI card failed",
			"status", resp.StatusCode,
			"body", string(respBody))
		// Check if we should trigger degrade
		if resp.StatusCode == 403 || resp.StatusCode == 429 || resp.StatusCode >= 500 {
			c.platform.activateCardDegrade(fmt.Sprintf("card.stream:%d", resp.StatusCode))
			c.mu.Lock()
			c.state = "failed"
			close(c.done)
			c.mu.Unlock()
		}
		return fmt.Errorf("stream AI card: status=%d, body=%s", resp.StatusCode, string(respBody))
	}

	slog.Debug("dingtalk: AI card streamed successfully", "isFinalize", isFinalize)
	return nil
}

// Finalize sends the final content and marks the card as complete.
func (c *aiCard) Finalize(ctx context.Context, content string) error {
	c.mu.Lock()

	// Stop any pending timer
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}

	// If already finished or failed, skip
	if c.state == "finished" || c.state == "failed" {
		c.mu.Unlock()
		return nil
	}

	// Wait for in-flight to complete
	for c.inFlight {
		c.mu.Unlock()
		select {
		case <-c.done:
			return nil
		case <-time.After(100 * time.Millisecond):
		}
		c.mu.Lock()
	}

	c.inFlight = true
	c.mu.Unlock()

	err := c.doStream(ctx, content, true)

	c.mu.Lock()
	c.inFlight = false
	if err != nil {
		c.state = "failed"
	} else {
		c.state = "finished"
		c.lastSentContent = content
		c.lastSentAt = time.Now()
	}
	select {
	case <-c.done:
	default:
		close(c.done)
	}
	c.mu.Unlock()

	return err
}

// Failed returns true if the card has entered a failed state.
func (c *aiCard) Failed() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.state == "failed"
}

// isCardDegraded returns true if card API is temporarily degraded.
func (p *Platform) isCardDegraded() bool {
	p.degradeMu.Lock()
	defer p.degradeMu.Unlock()
	return time.Now().Before(p.degradeUntil)
}

// activateCardDegrade activates card API degradation for 30 minutes.
func (p *Platform) activateCardDegrade(reason string) {
	p.degradeMu.Lock()
	defer p.degradeMu.Unlock()
	p.degradeUntil = time.Now().Add(30 * time.Minute)
	slog.Warn("dingtalk: AI card API degraded",
		"reason", reason,
		"until", p.degradeUntil.Format(time.RFC3339))
}
