package wire

import (
	"fmt"
	"hash/fnv"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

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

const routeWorkspaceHeader = "X-AgentTeam-Route-Workspace-Id"

type requestHeaders struct {
	AuthToken        string
	RouteWorkspaceID string
}

func setRequestHeaders(req *http.Request, headers requestHeaders) {
	if req == nil {
		return
	}
	setAgentTeamAuthHeader(req, headers.AuthToken)
	setAgentTeamRouteWorkspaceHeader(req, headers.RouteWorkspaceID)
}

func setAgentTeamAuthHeader(req *http.Request, authToken string) {
	if req == nil {
		return
	}
	req.Header.Set("Authorization", "Bearer "+authToken)
}

// ---------------------------------------------------------------------------
// URL helpers
// ---------------------------------------------------------------------------

func buildWSURL(serverURL string, apiKey string, workspaceID string) string {
	u := strings.Replace(serverURL, "http://", "ws://", 1)
	u = strings.Replace(u, "https://", "wss://", 1)
	u = strings.TrimRight(u, "/")
	path := "/api/v1/workspaces/" + url.PathEscape(strings.TrimSpace(workspaceID)) + "/daemon/connect"
	return fmt.Sprintf("%s%s?%s", u, path, url.Values{"key": {apiKey}}.Encode())
}

func daemonAgentAuthTokenPath(workspaceID string, agentID string) string {
	escapedAgentID := url.PathEscape(strings.TrimSpace(agentID))
	return "/api/v1/workspaces/" + url.PathEscape(strings.TrimSpace(workspaceID)) + "/daemon/agents/" + escapedAgentID + "/auth-token"
}

func daemonSessionTokenPath(workspaceID string) string {
	return "/api/v1/workspaces/" + url.PathEscape(strings.TrimSpace(workspaceID)) + "/daemon/session-token"
}

func daemonUpgradeInfoPath(workspaceID string) string {
	return "/api/v1/workspaces/" + url.PathEscape(strings.TrimSpace(workspaceID)) + "/daemon/upgrade-info"
}

func agentRuntimePath(workspaceID string, agentID string, suffix string) string {
	escapedAgentID := url.PathEscape(strings.TrimSpace(agentID))
	if !strings.HasPrefix(suffix, "/") {
		suffix = "/" + suffix
	}
	return "/api/v1/workspaces/" + url.PathEscape(strings.TrimSpace(workspaceID)) + "/agents/" + escapedAgentID + "/runtime" + suffix
}

func routeWorkspaceHeaders(workspaceID string) http.Header {
	headers := http.Header{}
	setAgentTeamRouteWorkspaceHeaderValue(headers, workspaceID)
	return headers
}

func setAgentTeamRouteWorkspaceHeader(req *http.Request, workspaceID string) {
	if req == nil {
		return
	}
	setAgentTeamRouteWorkspaceHeaderValue(req.Header, workspaceID)
}

func setAgentTeamRouteWorkspaceHeaderValue(headers http.Header, workspaceID string) {
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return
	}
	headers.Set(routeWorkspaceHeader, workspaceID)
}

// ---------------------------------------------------------------------------
// Target formatting
// ---------------------------------------------------------------------------

// knowledgeBaseScopeTarget collapses reply-thread targets to their parent
// channel/DM so KB authorization remains channel-scoped.
func knowledgeBaseScopeTarget(target string) string {
	trimmed := strings.TrimSpace(target)
	if trimmed == "" {
		return ""
	}
	parsed, err := domain.ParseMessageTargetReference(trimmed)
	if err != nil {
		// Knowledge-base callers may also carry canonical scope projections,
		// which deliberately are not author-facing message targets.
		return trimmed
	}
	switch parsed.Kind {
	case domain.MessageTargetKindReplyThread:
		if parsed.Thread.Type == domain.ThreadTypeDM {
			return "dm:@" + parsed.Thread.PeerMemberID
		}
		return "#" + parsed.Thread.ChannelSlug
	case domain.MessageTargetKindTeam:
		if strings.TrimSpace(parsed.Team.ShortID) != "" {
			return "#" + parsed.Team.TeamID
		}
	}
	return parsed.String()
}

// taskScopeProjectionTarget accepts either the author-facing #tsk_xxx form or
// a canonical task scope id carried by a trusted delivery projection.
func taskScopeProjectionTarget(value string) string {
	trimmed := strings.TrimSpace(value)
	if strings.HasPrefix(trimmed, "tsk_") {
		trimmed = "#" + trimmed
	}
	parsed, err := domain.ParseMessageTargetReference(trimmed)
	if err != nil || parsed.Kind != domain.MessageTargetKindTask {
		return ""
	}
	return parsed.String()
}

// ---------------------------------------------------------------------------
// Session & text helpers
// ---------------------------------------------------------------------------

func newSessionID(prefix string) string {
	return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}

func previewText(text string, maxRunes int) string {
	runes := []rune(text)
	if len(runes) <= maxRunes {
		return text
	}
	return string(runes[:maxRunes]) + "..."
}

// ---------------------------------------------------------------------------
// WebSocket helpers
// ---------------------------------------------------------------------------

func isNormalClose(err error) bool {
	return websocket.IsCloseError(err,
		websocket.CloseNormalClosure,
		websocket.CloseGoingAway,
	) || strings.Contains(err.Error(), "use of closed network connection")
}

// ---------------------------------------------------------------------------
// Hashing
// ---------------------------------------------------------------------------

func fnvHash64(agentID string, seq int) uint64 {
	return fnvHashKey(agentID, strconv.Itoa(seq))
}

func fnvHashKey(agentID string, key string) uint64 {
	h := fnv.New64a()
	h.Write([]byte(agentID))
	h.Write([]byte{0})
	h.Write([]byte(key))
	return h.Sum64()
}

func deliveryDedupHash(agentID string, seq int, messageID string) uint64 {
	return deliveryDedupHashWithInterruptPolicy(agentID, seq, messageID, "")
}

func deliveryDedupHashWithInterruptPolicy(agentID string, seq int, messageID string, interruptPolicy string) uint64 {
	key := ""
	if trimmed := strings.TrimSpace(messageID); trimmed != "" {
		key = "msg:" + trimmed
	} else {
		key = "seq:" + strconv.Itoa(seq)
	}
	if strings.TrimSpace(interruptPolicy) == agentInterruptPolicyCurrentRun {
		key += ":interrupt:" + agentInterruptPolicyCurrentRun
	}
	return fnvHashKey(agentID, key)
}

// ---------------------------------------------------------------------------
// Bounded dedup ring buffer
// ---------------------------------------------------------------------------

// dedupRing is a bounded ring buffer for message deduplication.
// When the buffer reaches capacity, the oldest entry is evicted
// automatically, providing deterministic O(1) insert and lookup.
type dedupRing struct {
	set  map[uint64]struct{} // O(1) lookup
	ring []uint64            // circular buffer of insertion order
	keys []string            // parallel buffer: key (e.g. agentID) per slot
	pos  int                 // next write position
	cap  int                 // maximum capacity
}

// newDedupRing creates a dedupRing with the given capacity.
func newDedupRing(capacity int) *dedupRing {
	return &dedupRing{
		set:  make(map[uint64]struct{}, capacity),
		ring: make([]uint64, capacity),
		keys: make([]string, capacity),
		cap:  capacity,
	}
}

// Contains reports whether hash h is in the dedup set.
func (d *dedupRing) Contains(h uint64) bool {
	_, ok := d.set[h]
	return ok
}

// Add inserts hash h into the dedup set. If the ring is at capacity,
// the oldest entry is evicted first. Returns true if h was already present.
func (d *dedupRing) Add(h uint64) (duplicate bool) {
	return d.AddWithKey(h, "")
}

// AddWithKey is like Add but also associates a key (e.g. agentID) with the
// entry so that RemoveByKey can later purge all entries for that key.
func (d *dedupRing) AddWithKey(h uint64, key string) (duplicate bool) {
	if _, ok := d.set[h]; ok {
		return true
	}
	// Evict the oldest entry at the current ring position if it is occupied.
	if old := d.ring[d.pos]; old != 0 {
		delete(d.set, old)
	}
	d.ring[d.pos] = h
	d.keys[d.pos] = key
	d.set[h] = struct{}{}
	d.pos = (d.pos + 1) % d.cap
	return false
}

// RemoveByKey removes all entries whose key matches the given value.
// This is used to clear dedup state for an agent that has restarted,
// so that re-delivered messages are not mistaken for duplicates.
func (d *dedupRing) RemoveByKey(key string) {
	for i := 0; i < d.cap; i++ {
		if d.keys[i] == key && d.ring[i] != 0 {
			delete(d.set, d.ring[i])
			d.ring[i] = 0
			d.keys[i] = ""
		}
	}
}

// Len returns the current number of entries in the dedup set.
func (d *dedupRing) Len() int {
	return len(d.set)
}
