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

package channels

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"sync"
	"time"

	"ragflow/internal/channels/core"
	"ragflow/internal/dao"
	"ragflow/internal/entity"
	"ragflow/internal/service"
)

const (
	reconcileInterval      = 10 * time.Second
	initialStartRetryDelay = 10 * time.Second
	maxStartRetryDelay     = 5 * time.Minute
)

type runningChannel struct {
	channel core.Channel
	fp      string
}

type failedChannel struct {
	fp          string
	attempts    int
	nextRetryAt time.Time
}

type Runtime struct {
	mu      sync.Mutex
	running map[string]runningChannel
	failed  map[string]failedChannel
}

// NewRuntime creates an empty chat-channel runtime reconciler.
func NewRuntime() *Runtime {
	return &Runtime{
		running: map[string]runningChannel{},
		failed:  map[string]failedChannel{},
	}
}

// Start launches the chat-channel runtime reconciler in the background.
func Start(ctx context.Context) *Runtime {
	rt := NewRuntime()
	service.SetChatChannelRuntimeProvider(whatsappRuntimeSnapshotMap)
	go rt.Run(ctx)
	return rt
}

// Run reconciles configured chat channels until the context is cancelled.
func (r *Runtime) Run(ctx context.Context) {
	ticker := time.NewTicker(reconcileInterval)
	defer ticker.Stop()
	defer r.stopAll(context.Background())

	for {
		if err := r.Reconcile(ctx); err != nil {
			log.Printf("chat channel reconcile failed: %v", err)
		}
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
		}
	}
}

// Reconcile starts, stops, and restarts channel instances to match the database state.
func (r *Runtime) Reconcile(ctx context.Context) error {
	desired, err := desiredChannels(ctx)
	if err != nil {
		return err
	}

	var toStop []core.Channel
	r.mu.Lock()
	for accountID, entry := range r.running {
		wanted, ok := desired[accountID]
		if !ok || wanted.fp != entry.fp {
			delete(r.running, accountID)
			toStop = append(toStop, entry.channel)
		}
	}
	for accountID, failure := range r.failed {
		wanted, ok := desired[accountID]
		if !ok || wanted.fp != failure.fp {
			delete(r.failed, accountID)
		}
	}
	r.mu.Unlock()

	for _, ch := range toStop {
		stopChannel(context.Background(), ch)
	}

	activeWhatsApp := false
	for _, wanted := range desired {
		if wanted.channel == "whatsapp" {
			activeWhatsApp = true
			break
		}
	}
	if err := syncWhatsAppGateway(ctx, activeWhatsApp); err != nil && activeWhatsApp {
		log.Printf("failed to sync WhatsApp gateway: %v", err)
	}

	now := time.Now()
	for accountID, wanted := range desired {
		r.mu.Lock()
		_, isRunning := r.running[accountID]
		failure, failed := r.failed[accountID]
		retryPending := failed && failure.fp == wanted.fp && now.Before(failure.nextRetryAt)
		r.mu.Unlock()
		if isRunning || retryPending {
			continue
		}
		if err := r.startChannel(ctx, accountID, wanted); err != nil {
			log.Printf("failed to start chat channel %s (%s): %v", accountID, wanted.channel, err)
			r.recordStartFailure(accountID, wanted.fp, now)
			continue
		}
		r.clearStartFailure(accountID)
	}
	return nil
}

// recordStartFailure saves the next retry window for a failed channel start.
func (r *Runtime) recordStartFailure(accountID string, fp string, now time.Time) {
	r.mu.Lock()
	defer r.mu.Unlock()

	attempts := 1
	if failure, ok := r.failed[accountID]; ok && failure.fp == fp {
		attempts = failure.attempts + 1
	}
	r.failed[accountID] = failedChannel{
		fp:          fp,
		attempts:    attempts,
		nextRetryAt: now.Add(startRetryDelay(attempts)),
	}
}

// clearStartFailure removes a stale failed-start record after a successful start.
func (r *Runtime) clearStartFailure(accountID string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	delete(r.failed, accountID)
}

// startRetryDelay returns the bounded exponential backoff for a start attempt.
func startRetryDelay(attempts int) time.Duration {
	if attempts < 1 {
		attempts = 1
	}
	delay := initialStartRetryDelay
	for i := 1; i < attempts; i++ {
		delay *= 2
		if delay >= maxStartRetryDelay {
			return maxStartRetryDelay
		}
	}
	return delay
}

type desiredChannel struct {
	channel    string
	credential map[string]any
	fp         string
}

// desiredChannels loads enabled chat-channel rows and reduces them to runtime configuration.
func desiredChannels(ctx context.Context) (map[string]desiredChannel, error) {
	rows, err := dao.NewChatChannel().ListActive(ctx, dao.DB)
	if err != nil {
		return nil, err
	}
	out := make(map[string]desiredChannel, len(rows))
	for _, row := range rows {
		credential := credentialFromConfig(row.Config)
		out[row.ID] = desiredChannel{
			channel:    row.Channel,
			credential: credential,
			fp:         fingerprint(row.Channel, credential),
		}
	}
	return out, nil
}

// credentialFromConfig extracts the platform credential block from chat_channel.config.
func credentialFromConfig(config entity.JSONMap) map[string]any {
	if config == nil {
		return map[string]any{}
	}
	if raw, ok := config["credential"].(map[string]any); ok {
		return raw
	}
	if raw, ok := config["credential"].(entity.JSONMap); ok {
		return raw
	}
	return map[string]any{}
}

// fingerprint returns a stable hash for the configuration that requires a channel restart.
func fingerprint(channel string, credential map[string]any) string {
	payload, _ := json.Marshal(map[string]any{
		"channel":    channel,
		"credential": credential,
	})
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:])
}

// startChannel builds a platform channel, attaches the RAG bridge, and starts it.
func (r *Runtime) startChannel(ctx context.Context, accountID string, wanted desiredChannel) error {
	ch, err := buildChannel(accountID, wanted)
	if err != nil {
		return err
	}
	if ch == nil {
		return nil
	}

	channelService := service.NewChatChannelService()
	ch.SetMessageHandler(func(ctx context.Context, msg core.IncomingMessage) error {
		answer, err := channelService.HandleIncomingMessage(ctx, service.ChatChannelIncomingMessage{
			Channel:   msg.Channel,
			AccountID: msg.AccountID,
			ChatID:    msg.ChatID,
			ChatType:  msg.ChatType,
			MessageID: msg.MessageID,
			SenderID:  msg.SenderID,
			Text:      msg.Text,
		})
		if err != nil || answer == "" {
			return err
		}
		return ch.Send(ctx, core.OutgoingMessage{
			ChatID:           msg.ChatID,
			Text:             answer,
			ReplyToMessageID: msg.MessageID,
		})
	})
	if err = ch.Start(ctx); err != nil {
		return err
	}
	r.mu.Lock()
	r.running[accountID] = runningChannel{channel: ch, fp: wanted.fp}
	r.mu.Unlock()
	log.Printf("started chat channel %s:%s", ch.ChannelID(), accountID)
	return nil
}

// buildChannel constructs the platform-specific channel implementation for one chat_channel row.
func buildChannel(accountID string, wanted desiredChannel) (core.Channel, error) {
	switch wanted.channel {
	case "feishu":
		return newFeishuChannelFromConfig(accountID, wanted.credential)
	case "discord":
		return newDiscordChannelFromConfig(accountID, wanted.credential)
	case "qqbot":
		return newQQBotChannelFromConfig(accountID, wanted.credential)
	case "whatsapp":
		return newWhatsAppChannelFromConfig(accountID, wanted.credential)
	case "line":
		return newLineChannelFromConfig(accountID, wanted.credential)
	case "telegram":
		return newTelegramChannelFromConfig(accountID, wanted.credential)
	case "wecom":
		return newWeComChannelFromConfig(accountID, wanted.credential)
	case "dingtalk":
		return newDingTalkChannelFromConfig(accountID, wanted.credential)
	default:
		return nil, fmt.Errorf("unknown channel: %s", wanted.channel)
	}
}

// whatsappRuntimeSnapshotMap returns the API payload for a live WhatsApp runtime snapshot.
func whatsappRuntimeSnapshotMap(accountID string) (map[string]any, bool) {
	snapshot, ok := getWhatsAppRuntimeSnapshot(accountID)
	if !ok {
		return nil, false
	}
	return map[string]any{
		"account_id":       snapshot.AccountID,
		"session_key":      snapshot.SessionKey,
		"status":           snapshot.Status,
		"connected_at":     snapshot.ConnectedAt,
		"qr_updated_at":    snapshot.QRUpdatedAt,
		"qr_data_url":      snapshot.QRDataURL,
		"last_error":       snapshot.LastError,
		"session_id":       snapshot.SessionID,
		"last_snapshot_at": snapshot.LastSnapshotAt,
		"gateway_base_url": snapshot.GatewayBaseURL,
		"event_cursor":     snapshot.EventCursor,
	}, true
}

// stopAll stops every running channel and shuts down shared gateway processes.
func (r *Runtime) stopAll(ctx context.Context) {
	r.mu.Lock()
	running := r.running
	r.running = map[string]runningChannel{}
	r.mu.Unlock()
	for _, entry := range running {
		stopChannel(ctx, entry.channel)
	}
	_ = syncWhatsAppGateway(ctx, false)
}

// stopChannel stops one platform channel and logs any shutdown error.
func stopChannel(ctx context.Context, ch core.Channel) {
	if err := ch.Stop(ctx); err != nil {
		log.Printf("failed to stop chat channel %s:%s: %v", ch.ChannelID(), ch.AccountID(), err)
	}
}
