package common

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/pkg/kutt"
)

const inviteShortLinkCachePrefix = "cache:invite:short_link:"

type inviteLinkCustomData struct {
	ShareURL string `json:"shareUrl"`
	Domain   string `json:"domain"`
}

type InviteLinkResolver struct {
	ctx             context.Context
	svcCtx          *svc.ServiceContext
	createShortLink func(ctx context.Context, targetURL, domain string) (string, error)
}

func NewInviteLinkResolver(ctx context.Context, svcCtx *svc.ServiceContext) *InviteLinkResolver {
	resolver := &InviteLinkResolver{
		ctx:    ctx,
		svcCtx: svcCtx,
	}
	resolver.createShortLink = func(ctx context.Context, targetURL, domain string) (string, error) {
		client := kutt.NewClient(svcCtx.Config.Kutt.ApiURL, svcCtx.Config.Kutt.ApiKey)
		link, err := client.CreateShortLink(ctx, &kutt.CreateLinkRequest{
			Target: targetURL,
			Reuse:  true,
			Domain: domain,
		})
		if err != nil {
			return "", err
		}

		shortLink := strings.TrimSpace(link.Link)
		if strings.HasPrefix(shortLink, "http://") {
			shortLink = strings.Replace(shortLink, "http://", "https://", 1)
		}
		return shortLink, nil
	}
	return resolver
}

func (r *InviteLinkResolver) ResolveInviteLink(referCode string) string {
	normalizedCode := strings.TrimSpace(referCode)
	if normalizedCode == "" {
		return ""
	}

	longLink := r.buildLongInviteLink(normalizedCode)
	if !r.canUseKutt() || longLink == "" {
		return longLink
	}

	if cached := r.getCachedShortLink(normalizedCode); cached != "" {
		return cached
	}

	shortLink, err := r.generateShortLinkWithTimeout(normalizedCode, 1500*time.Millisecond)
	if err != nil || strings.TrimSpace(shortLink) == "" {
		return longLink
	}

	r.cacheShortLink(normalizedCode, shortLink)
	return shortLink
}

func (r *InviteLinkResolver) ResolveInviteLinksBatch(referCodes []string, maxGenerate, maxConcurrency int, timeout time.Duration) map[string]string {
	result := make(map[string]string)
	uniqueCodes := uniqueReferCodes(referCodes)
	if len(uniqueCodes) == 0 {
		return result
	}

	for _, referCode := range uniqueCodes {
		result[referCode] = r.buildLongInviteLink(referCode)
	}

	if !r.canUseKutt() {
		return result
	}

	toGenerate := make([]string, 0, len(uniqueCodes))
	for _, referCode := range uniqueCodes {
		if cached := r.getCachedShortLink(referCode); cached != "" {
			result[referCode] = cached
			continue
		}
		toGenerate = append(toGenerate, referCode)
	}

	if maxGenerate > 0 && len(toGenerate) > maxGenerate {
		toGenerate = toGenerate[:maxGenerate]
	}

	if len(toGenerate) == 0 {
		return result
	}

	if maxConcurrency <= 0 {
		maxConcurrency = 1
	}
	if timeout <= 0 {
		timeout = 1500 * time.Millisecond
	}

	limiter := make(chan struct{}, maxConcurrency)
	var waitGroup sync.WaitGroup
	var mutex sync.Mutex

	for _, referCode := range toGenerate {
		waitGroup.Add(1)
		currentCode := referCode

		go func() {
			defer waitGroup.Done()
			limiter <- struct{}{}
			defer func() { <-limiter }()

			shortLink, err := r.generateShortLinkWithTimeout(currentCode, timeout)
			if err != nil || strings.TrimSpace(shortLink) == "" {
				return
			}

			mutex.Lock()
			result[currentCode] = shortLink
			mutex.Unlock()

			r.cacheShortLink(currentCode, shortLink)
		}()
	}

	waitGroup.Wait()
	return result
}

func (r *InviteLinkResolver) canUseKutt() bool {
	if r == nil || r.svcCtx == nil {
		return false
	}
	if !r.svcCtx.Config.Kutt.Enable {
		return false
	}
	if strings.TrimSpace(r.svcCtx.Config.Kutt.ApiURL) == "" || strings.TrimSpace(r.svcCtx.Config.Kutt.ApiKey) == "" {
		return false
	}
	return r.createShortLink != nil
}

func (r *InviteLinkResolver) resolveShareURLAndDomain() (string, string) {
	if r == nil || r.svcCtx == nil {
		return "", ""
	}

	shareURL := strings.TrimSpace(r.svcCtx.Config.Kutt.TargetURL)
	domain := strings.TrimSpace(r.svcCtx.Config.Kutt.Domain)

	customData := strings.TrimSpace(r.svcCtx.Config.Site.CustomData)
	if customData == "" {
		return shareURL, domain
	}

	var parsedData inviteLinkCustomData
	if err := json.Unmarshal([]byte(customData), &parsedData); err != nil {
		return shareURL, domain
	}

	if strings.TrimSpace(parsedData.ShareURL) != "" {
		shareURL = strings.TrimSpace(parsedData.ShareURL)
	}
	if strings.TrimSpace(parsedData.Domain) != "" {
		domain = strings.TrimSpace(parsedData.Domain)
	}

	return shareURL, domain
}

func (r *InviteLinkResolver) buildLongInviteLink(referCode string) string {
	normalizedCode := strings.TrimSpace(referCode)
	if normalizedCode == "" {
		return ""
	}

	shareURL, _ := r.resolveShareURLAndDomain()
	if shareURL == "" {
		return ""
	}

	parsedURL, err := url.Parse(shareURL)
	if err != nil {
		return fallbackLongInviteLink(shareURL, normalizedCode)
	}

	queryValues := parsedURL.Query()
	queryValues.Set("ic", normalizedCode)
	parsedURL.RawQuery = queryValues.Encode()

	return parsedURL.String()
}

func (r *InviteLinkResolver) generateShortLinkWithTimeout(referCode string, timeout time.Duration) (string, error) {
	longLink := r.buildLongInviteLink(referCode)
	if longLink == "" {
		return "", nil
	}
	_, domain := r.resolveShareURLAndDomain()

	const maxRetries = 3
	var lastErr error
	for attempt := 0; attempt < maxRetries; attempt++ {
		requestCtx := r.ctx
		var cancel context.CancelFunc
		if timeout > 0 {
			requestCtx, cancel = context.WithTimeout(r.ctx, timeout)
		}

		shortLink, err := r.createShortLink(requestCtx, longLink, domain)
		if cancel != nil {
			cancel()
		}
		if err == nil && strings.TrimSpace(shortLink) != "" {
			return strings.TrimSpace(shortLink), nil
		}
		lastErr = err
	}

	return "", lastErr
}

func (r *InviteLinkResolver) getCachedShortLink(referCode string) string {
	if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
		return ""
	}

	cacheKey := inviteShortLinkCachePrefix + referCode
	shortLink, err := r.svcCtx.Redis.Get(r.ctx, cacheKey).Result()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(shortLink)
}

func (r *InviteLinkResolver) cacheShortLink(referCode, shortLink string) {
	if r == nil || r.svcCtx == nil || r.svcCtx.Redis == nil {
		return
	}
	if strings.TrimSpace(referCode) == "" || strings.TrimSpace(shortLink) == "" {
		return
	}

	cacheKey := inviteShortLinkCachePrefix + referCode
	_ = r.svcCtx.Redis.Set(r.ctx, cacheKey, shortLink, 7*24*time.Hour).Err()
}

func uniqueReferCodes(referCodes []string) []string {
	uniqueCodes := make([]string, 0, len(referCodes))
	seen := make(map[string]struct{}, len(referCodes))

	for _, referCode := range referCodes {
		normalizedCode := strings.TrimSpace(referCode)
		if normalizedCode == "" {
			continue
		}
		if _, exists := seen[normalizedCode]; exists {
			continue
		}
		seen[normalizedCode] = struct{}{}
		uniqueCodes = append(uniqueCodes, normalizedCode)
	}

	return uniqueCodes
}

func fallbackLongInviteLink(baseURL, referCode string) string {
	normalizedBase := strings.TrimSpace(baseURL)
	normalizedCode := strings.TrimSpace(referCode)
	if normalizedBase == "" || normalizedCode == "" {
		return ""
	}

	separator := "?"
	if strings.Contains(normalizedBase, "?") {
		separator = "&"
	}
	trimmedBase := strings.TrimRight(normalizedBase, "?&")
	return fmt.Sprintf("%s%sic=%s", trimmedBase, separator, url.QueryEscape(normalizedCode))
}
