package apiclient

import (
	"container/list"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

const (
	defaultResolvedTargetTTL      = 24 * time.Hour
	defaultResolvedTargetCapacity = 4096
)

// ResolvePurpose selects the server-side policy used to resolve a target.
// Purpose is part of the cache identity because writable and review-context
// resolution have different authorization and target-kind constraints.
type ResolvePurpose string

const (
	ResolvePurposeWritable      ResolvePurpose = ""
	ResolvePurposeReviewContext ResolvePurpose = "review_context"
)

// ResolvedTarget is the stable scope identity returned by /resolve-channel.
// Cached results also carry an opaque handle for conditional invalidation.
type ResolvedTarget struct {
	ScopeID   string
	TargetKey string

	resolver *TargetResolver
	cacheKey resolvedTargetCacheKey
	entryID  uint64
}

type resolvedTargetCacheKey struct {
	ServerURL   string
	WorkspaceID string
	AgentID     string
	Purpose     ResolvePurpose
	Target      string
}

type resolvedTargetCacheEntry struct {
	key       resolvedTargetCacheKey
	value     ResolvedTarget
	expiresAt time.Time
}

// TargetResolver owns the process-local positive LRU and concurrent miss
// coalescing for /resolve-channel. A resolver is safe for concurrent use.
type TargetResolver struct {
	mu       sync.Mutex
	entries  map[resolvedTargetCacheKey]*list.Element
	recency  *list.List
	flights  singleflight.Group
	ttl      time.Duration
	capacity int
	now      func() time.Time

	nextEntryID     uint64
	agentGeneration map[string]uint64
}

// NewTargetResolver constructs the production target resolver.
func NewTargetResolver() *TargetResolver {
	return &TargetResolver{
		entries:         make(map[resolvedTargetCacheKey]*list.Element),
		recency:         list.New(),
		ttl:             defaultResolvedTargetTTL,
		capacity:        defaultResolvedTargetCapacity,
		now:             time.Now,
		agentGeneration: make(map[string]uint64),
	}
}

func (resolver *TargetResolver) get(key resolvedTargetCacheKey) (ResolvedTarget, uint64, bool) {
	if resolver == nil {
		return ResolvedTarget{}, 0, false
	}
	resolver.mu.Lock()
	defer resolver.mu.Unlock()
	generation := resolver.agentGeneration[key.AgentID]
	element, ok := resolver.entries[key]
	if !ok {
		return ResolvedTarget{}, generation, false
	}
	entry := element.Value.(resolvedTargetCacheEntry)
	if resolver.ttl <= 0 || !resolver.now().Before(entry.expiresAt) {
		resolver.removeElementLocked(element)
		return ResolvedTarget{}, generation, false
	}
	resolver.recency.MoveToFront(element)
	return entry.value, generation, true
}

func (resolver *TargetResolver) putIfGeneration(
	key resolvedTargetCacheKey,
	value ResolvedTarget,
	generation uint64,
) ResolvedTarget {
	if resolver == nil || resolver.ttl <= 0 || resolver.capacity <= 0 {
		return value
	}
	resolver.mu.Lock()
	defer resolver.mu.Unlock()
	if resolver.agentGeneration[key.AgentID] != generation {
		return value
	}

	now := resolver.now()
	resolver.removeExpiredLocked(now)
	resolver.nextEntryID++
	value.resolver = resolver
	value.cacheKey = key
	value.entryID = resolver.nextEntryID
	entry := resolvedTargetCacheEntry{
		key:       key,
		value:     value,
		expiresAt: now.Add(resolver.ttl),
	}
	if existing, ok := resolver.entries[key]; ok {
		existing.Value = entry
		resolver.recency.MoveToFront(existing)
		return value
	}
	element := resolver.recency.PushFront(entry)
	resolver.entries[key] = element
	for len(resolver.entries) > resolver.capacity {
		resolver.removeElementLocked(resolver.recency.Back())
	}
	return value
}

func (resolver *TargetResolver) removeExpiredLocked(now time.Time) {
	for element := resolver.recency.Back(); element != nil; {
		previous := element.Prev()
		entry := element.Value.(resolvedTargetCacheEntry)
		if !now.Before(entry.expiresAt) {
			resolver.removeElementLocked(element)
		}
		element = previous
	}
}

func (resolver *TargetResolver) removeIfCurrent(key resolvedTargetCacheKey, entryID uint64) bool {
	if resolver == nil || entryID == 0 {
		return false
	}
	resolver.mu.Lock()
	defer resolver.mu.Unlock()
	element, ok := resolver.entries[key]
	if !ok {
		return false
	}
	entry := element.Value.(resolvedTargetCacheEntry)
	if entry.value.entryID != entryID {
		return false
	}
	resolver.removeElementLocked(element)
	return true
}

func (resolver *TargetResolver) removeElementLocked(element *list.Element) {
	if element == nil {
		return
	}
	entry := element.Value.(resolvedTargetCacheEntry)
	delete(resolver.entries, entry.key)
	resolver.recency.Remove(element)
}

// RemoveAgent removes every cached target identity for one server agent and
// fences loads started before the removal from repopulating the cache.
func (resolver *TargetResolver) RemoveAgent(agentID string) {
	if resolver == nil {
		return
	}
	agentID = strings.TrimSpace(agentID)
	resolver.mu.Lock()
	defer resolver.mu.Unlock()
	resolver.agentGeneration[agentID]++
	for element := resolver.recency.Back(); element != nil; {
		previous := element.Prev()
		entry := element.Value.(resolvedTargetCacheEntry)
		if entry.key.AgentID == agentID {
			resolver.removeElementLocked(element)
		}
		element = previous
	}
}

func (resolver *TargetResolver) resolve(
	ctx context.Context,
	key resolvedTargetCacheKey,
	load func() (ResolvedTarget, error),
) (ResolvedTarget, error) {
	for {
		cached, generation, ok := resolver.get(key)
		if ok {
			return cached, nil
		}
		resultChannel := resolver.flights.DoChan(key.flightKey(generation), func() (any, error) {
			if cached, currentGeneration, ok := resolver.get(key); ok {
				return cached, nil
			} else if currentGeneration != generation {
				// This flight was superseded before its load began. Do not let a
				// caller that arrived after RemoveAgent join the old generation.
				return ResolvedTarget{}, errTargetResolverGenerationChanged
			}
			resolved, err := load()
			if err != nil {
				return ResolvedTarget{}, err
			}
			resolved = resolver.putIfGeneration(key, resolved, generation)
			return resolved, nil
		})
		select {
		case <-ctx.Done():
			return ResolvedTarget{}, ctx.Err()
		case result := <-resultChannel:
			if errors.Is(result.Err, errTargetResolverGenerationChanged) {
				continue
			}
			if result.Err != nil {
				return ResolvedTarget{}, result.Err
			}
			resolved, ok := result.Val.(ResolvedTarget)
			if !ok {
				return ResolvedTarget{}, fmt.Errorf("resolve target: unexpected singleflight result %T", result.Val)
			}
			return resolved, nil
		}
	}
}

var errTargetResolverGenerationChanged = errors.New("target resolver generation changed")

func (key resolvedTargetCacheKey) flightKey(generation uint64) string {
	var builder strings.Builder
	for _, part := range []string{
		key.ServerURL,
		key.WorkspaceID,
		key.AgentID,
		string(key.Purpose),
		key.Target,
		strconv.FormatUint(generation, 10),
	} {
		builder.WriteString(strconv.Itoa(len(part)))
		builder.WriteByte(':')
		builder.WriteString(part)
	}
	return builder.String()
}

func (a *Client) resolvedTargetCacheKey(target string, purpose ResolvePurpose) (resolvedTargetCacheKey, string) {
	requestTarget := strings.TrimSpace(target)
	normalizedTarget := requestTarget
	if parsed, err := domain.ParseMessageTargetReference(requestTarget); err == nil {
		normalizedTarget = parsed.String()
	}
	return resolvedTargetCacheKey{
		ServerURL:   strings.TrimRight(strings.TrimSpace(a.ServerURL), "/"),
		WorkspaceID: strings.TrimSpace(a.RouteWorkspaceID),
		AgentID:     strings.TrimSpace(a.AgentID),
		Purpose:     ResolvePurpose(strings.TrimSpace(string(purpose))),
		Target:      normalizedTarget,
	}, requestTarget
}

// ResolveTarget resolves one author-facing target through /resolve-channel.
// Successful non-empty results use the injected TargetResolver when present;
// clients without a resolver preserve an uncached compatibility path.
func (a *Client) ResolveTarget(ctx context.Context, target string, purpose ResolvePurpose) (ResolvedTarget, error) {
	if a == nil {
		return ResolvedTarget{}, fmt.Errorf("resolve target: API client is required")
	}
	key, requestTarget := a.resolvedTargetCacheKey(target, purpose)
	load := func() (ResolvedTarget, error) {
		return a.resolveTargetUpstream(ctx, requestTarget, key.Purpose)
	}
	if a.TargetResolver == nil {
		return load()
	}
	return a.TargetResolver.resolve(ctx, key, load)
}

func (a *Client) resolveTargetUpstream(ctx context.Context, target string, purpose ResolvePurpose) (ResolvedTarget, error) {
	body := map[string]string{"target": target}
	if purpose != ResolvePurposeWritable {
		body["purpose"] = string(purpose)
	}
	data, err := json.Marshal(body)
	if err != nil {
		return ResolvedTarget{}, fmt.Errorf("resolve target: agentteam api post: %w", err)
	}
	var response struct {
		ScopeID   string `json:"scopeId"`
		TargetKey string `json:"targetKey"`
		Error     string `json:"error"`
	}
	path := a.AgentRuntimePath("/resolve-channel")
	if err := a.execute(ctx, "POST "+path, attemptReplaySafe, func() error {
		return a.doPostJSONWithHeaders(ctx, path, data, &response, nil, 0)
	}); err != nil {
		return ResolvedTarget{}, fmt.Errorf("resolve target: %w", err)
	}
	if responseError := strings.TrimSpace(response.Error); responseError != "" {
		return ResolvedTarget{}, fmt.Errorf("resolve target: %w", errors.New(responseError))
	}
	scopeID := strings.TrimSpace(response.ScopeID)
	if scopeID == "" {
		return ResolvedTarget{}, fmt.Errorf("resolve target: empty scope id for %s", target)
	}
	targetKey := strings.TrimSpace(response.TargetKey)
	if targetKey == "" {
		targetKey = scopeID
	}
	return ResolvedTarget{ScopeID: scopeID, TargetKey: targetKey}, nil
}

// InvalidateOnError evicts this exact cached resolution after a downstream
// authorization or missing-scope response. It never replays the failed request.
func (resolved ResolvedTarget) InvalidateOnError(err error) bool {
	var apiErr *APIError
	if !errors.As(err, &apiErr) {
		return false
	}
	switch apiErr.StatusCode {
	case http.StatusForbidden, http.StatusNotFound, http.StatusGone:
		if resolved.resolver == nil {
			return false
		}
		return resolved.resolver.removeIfCurrent(resolved.cacheKey, resolved.entryID)
	default:
		return false
	}
}
