package app

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"sub2api-cn-relay-manager/internal/routing"
	"sub2api-cn-relay-manager/internal/store/sqlite"
)

type ResolveRouteRequest struct {
	RequestID       string `json:"request_id,omitempty"`
	LogicalGroupID  string `json:"logical_group_id"`
	PublicModel     string `json:"public_model"`
	Scope           string `json:"scope"`
	SubjectID       string `json:"subject_id"`
	UserKey         string `json:"user_key,omitempty"`
	ConversationKey string `json:"conversation_key,omitempty"`
	Sync            bool   `json:"sync,omitempty"`
}

type ResolveRouteInfo struct {
	RequestID      string `json:"request_id"`
	Backend        string `json:"backend"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	Scope          string `json:"scope"`
	SubjectID      string `json:"subject_id"`
	StickyKey      string `json:"sticky_key"`
	StickyHit      bool   `json:"sticky_hit"`
	StickyAction   string `json:"sticky_action"`
	FallbackUsed   bool   `json:"fallback_used,omitempty"`
	RouteID        string `json:"route_id"`
	RouteName      string `json:"route_name,omitempty"`
	ShadowGroupID  string `json:"shadow_group_id"`
	ShadowHostID   string `json:"shadow_host_id"`
	ShadowModel    string `json:"shadow_model,omitempty"`
	Priority       int    `json:"priority"`
	Weight         int    `json:"weight"`
	BoundAt        string `json:"bound_at,omitempty"`
	ExpiresAt      string `json:"expires_at,omitempty"`
}

type resolvedRouteCandidate struct {
	route      sqlite.LogicalGroupRoute
	routeModel sqlite.LogicalGroupRouteModel
}

type skippedResolvedRoute struct {
	route        sqlite.LogicalGroupRoute
	failureCount int
	reason       string
}

type routeSelectionResult struct {
	candidate    resolvedRouteCandidate
	fallbackUsed bool
	failoverFrom *skippedResolvedRoute
}

func handleResolveRoute(w http.ResponseWriter, r *http.Request, fn func(context.Context, ResolveRouteRequest) (ResolveRouteInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "resolve-route action is not configured"})
		return
	}
	var req ResolveRouteRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	info, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"resolve": info})
}

func buildResolveRouteAction(sqliteDSN string, stickyRuntime stickyStoreRuntime, writerSource *lazyRouteLogWriter) func(context.Context, ResolveRouteRequest) (ResolveRouteInfo, error) {
	return func(ctx context.Context, req ResolveRouteRequest) (ResolveRouteInfo, error) {
		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return ResolveRouteInfo{}, err
		}
		defer store.Close()

		group, err := getLogicalGroupRow(ctx, store, req.LogicalGroupID)
		if err != nil {
			return ResolveRouteInfo{}, err
		}
		if !isActiveStatus(group.Status) {
			return ResolveRouteInfo{}, fmt.Errorf("logical group %q is not active", group.LogicalGroupID)
		}

		req, stickyKey, requestID, err := normalizeResolveRouteRequest(req)
		if err != nil {
			return ResolveRouteInfo{}, err
		}

		writer, err := writerSource.get(ctx)
		if err != nil {
			return ResolveRouteInfo{}, err
		}

		binding, stickyHit, err := lookupValidStickyBinding(ctx, store, stickyRuntime, stickyKey, req)
		if err != nil {
			return ResolveRouteInfo{}, err
		}

		if stickyHit {
			candidate, err := loadResolvedRouteCandidate(ctx, store, group.LogicalGroupID, req.PublicModel, binding.RouteID)
			if err != nil {
				if err := stickyRuntime.store.Delete(ctx, stickyKey); err != nil {
					return ResolveRouteInfo{}, err
				}
			} else {
				info := resolveRouteInfoFromBinding(stickyRuntime.backend, stickyKey, req.Scope, req.SubjectID, candidate, binding, requestID, true, "hit", false)
				if err := writer.AppendStickyAudit(ctx, routing.RouteStickyAuditEvent{
					StickyKey:      stickyKey,
					StickyKeyType:  req.Scope,
					LogicalGroupID: req.LogicalGroupID,
					PublicModel:    req.PublicModel,
					RouteID:        candidate.route.RouteID,
					Action:         "hit",
					ExpiresAt:      binding.ExpiresAt,
				}); err != nil {
					return ResolveRouteInfo{}, err
				}
				if err := writer.AppendDecision(ctx, routing.RouteDecisionEvent{
					RequestID:             requestID,
					LogicalGroupID:        req.LogicalGroupID,
					PublicModel:           req.PublicModel,
					UserKey:               resolveUserKey(req),
					ConversationKey:       resolveConversationKey(req),
					StickyKey:             stickyKey,
					StickyKeyType:         req.Scope,
					StickyHit:             true,
					SelectedRouteID:       candidate.route.RouteID,
					SelectedShadowGroupID: candidate.route.ShadowGroupID,
				}); err != nil {
					return ResolveRouteInfo{}, err
				}
				if req.Sync {
					if err := writer.Flush(ctx); err != nil {
						return ResolveRouteInfo{}, err
					}
				}
				return info, nil
			}
		}

		selection, err := selectResolvedRouteCandidate(ctx, store, stickyRuntime, group, req.PublicModel)
		if err != nil {
			return ResolveRouteInfo{}, err
		}
		candidate := selection.candidate

		ttl, err := resolveStickyTTL(group, req.Scope)
		if err != nil {
			return ResolveRouteInfo{}, err
		}
		binding = routing.StickyBinding{
			LogicalGroupID: req.LogicalGroupID,
			PublicModel:    req.PublicModel,
			RouteID:        candidate.route.RouteID,
			ShadowGroupID:  candidate.route.ShadowGroupID,
		}
		if err := stickyRuntime.store.Set(ctx, stickyKey, binding, ttl); err != nil {
			return ResolveRouteInfo{}, err
		}
		stored, ok, err := stickyRuntime.store.Get(ctx, stickyKey)
		if err != nil {
			return ResolveRouteInfo{}, err
		}
		if !ok {
			return ResolveRouteInfo{}, fmt.Errorf("sticky binding %q not found after set", stickyKey)
		}

		if err := writer.AppendStickyAudit(ctx, routing.RouteStickyAuditEvent{
			StickyKey:      stickyKey,
			StickyKeyType:  req.Scope,
			LogicalGroupID: req.LogicalGroupID,
			PublicModel:    req.PublicModel,
			RouteID:        candidate.route.RouteID,
			Action:         "bind",
			ExpiresAt:      stored.ExpiresAt,
		}); err != nil {
			return ResolveRouteInfo{}, err
		}
		if err := writer.AppendDecision(ctx, routing.RouteDecisionEvent{
			RequestID:             requestID,
			LogicalGroupID:        req.LogicalGroupID,
			PublicModel:           req.PublicModel,
			UserKey:               resolveUserKey(req),
			ConversationKey:       resolveConversationKey(req),
			StickyKey:             stickyKey,
			StickyKeyType:         req.Scope,
			StickyHit:             false,
			SelectedRouteID:       candidate.route.RouteID,
			SelectedShadowGroupID: candidate.route.ShadowGroupID,
			FallbackUsed:          selection.fallbackUsed,
		}); err != nil {
			return ResolveRouteInfo{}, err
		}
		if selection.failoverFrom != nil {
			if err := writer.AppendFailover(ctx, routing.RouteFailoverEvent{
				RequestID:      requestID,
				LogicalGroupID: req.LogicalGroupID,
				PublicModel:    req.PublicModel,
				FromRouteID:    selection.failoverFrom.route.RouteID,
				ToRouteID:      candidate.route.RouteID,
				Reason:         selection.failoverFrom.reason,
				FailureCount:   selection.failoverFrom.failureCount,
			}); err != nil {
				return ResolveRouteInfo{}, err
			}
		}
		if req.Sync {
			if err := writer.Flush(ctx); err != nil {
				return ResolveRouteInfo{}, err
			}
		}
		return resolveRouteInfoFromBinding(stickyRuntime.backend, stickyKey, req.Scope, req.SubjectID, candidate, stored, requestID, false, "bind", selection.fallbackUsed), nil
	}
}

func normalizeResolveRouteRequest(req ResolveRouteRequest) (ResolveRouteRequest, string, string, error) {
	req.RequestID = strings.TrimSpace(req.RequestID)
	req.LogicalGroupID = strings.TrimSpace(req.LogicalGroupID)
	req.PublicModel = strings.TrimSpace(req.PublicModel)
	req.Scope = strings.TrimSpace(req.Scope)
	req.SubjectID = strings.TrimSpace(req.SubjectID)
	req.UserKey = strings.TrimSpace(req.UserKey)
	req.ConversationKey = strings.TrimSpace(req.ConversationKey)

	if req.LogicalGroupID == "" {
		return ResolveRouteRequest{}, "", "", fmt.Errorf("logical_group_id is required")
	}
	if req.PublicModel == "" {
		return ResolveRouteRequest{}, "", "", fmt.Errorf("public_model is required")
	}
	if req.Scope == "" {
		return ResolveRouteRequest{}, "", "", fmt.Errorf("scope is required")
	}
	if req.SubjectID == "" {
		return ResolveRouteRequest{}, "", "", fmt.Errorf("subject_id is required")
	}

	stickyKey, err := routing.BuildStickyKey(req.Scope, req.LogicalGroupID, req.PublicModel, req.SubjectID)
	if err != nil {
		return ResolveRouteRequest{}, "", "", err
	}
	requestID := req.RequestID
	if requestID == "" {
		requestID = fmt.Sprintf("resolve_%d", time.Now().UnixNano())
	}
	return req, stickyKey, requestID, nil
}

func lookupValidStickyBinding(ctx context.Context, store *sqlite.DB, runtime stickyStoreRuntime, stickyKey string, req ResolveRouteRequest) (routing.StickyBinding, bool, error) {
	binding, ok, err := runtime.store.Get(ctx, stickyKey)
	if err != nil || !ok {
		return routing.StickyBinding{}, false, err
	}
	candidate, err := loadResolvedRouteCandidate(ctx, store, req.LogicalGroupID, req.PublicModel, binding.RouteID)
	if err != nil {
		if deleteErr := runtime.store.Delete(ctx, stickyKey); deleteErr != nil {
			return routing.StickyBinding{}, false, deleteErr
		}
		return routing.StickyBinding{}, false, nil
	}
	cooldown, ok, err := runtime.store.GetCooldown(ctx, candidate.route.RouteID)
	if err != nil {
		return routing.StickyBinding{}, false, err
	}
	if ok && !isExpiredRFC3339(cooldown.Until) {
		if deleteErr := runtime.store.Delete(ctx, stickyKey); deleteErr != nil {
			return routing.StickyBinding{}, false, deleteErr
		}
		return routing.StickyBinding{}, false, nil
	}
	return binding, true, nil
}

func selectResolvedRouteCandidate(ctx context.Context, store *sqlite.DB, runtime stickyStoreRuntime, group sqlite.LogicalGroup, publicModel string) (routeSelectionResult, error) {
	models, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, group.LogicalGroupID)
	if err != nil {
		return routeSelectionResult{}, err
	}
	if !logicalGroupHasActiveModel(models, publicModel) {
		return routeSelectionResult{}, fmt.Errorf("logical group %q does not expose active model %q", group.LogicalGroupID, publicModel)
	}

	routes, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, group.LogicalGroupID)
	if err != nil {
		return routeSelectionResult{}, err
	}
	var skipped []skippedResolvedRoute
	for _, route := range routes {
		if !isActiveStatus(route.Status) {
			continue
		}
		if !routeExitsCooldown(route.CooldownUntil) {
			skipped = append(skipped, skippedResolvedRoute{
				route:  route,
				reason: "route_cooldown_until_active",
			})
			continue
		}
		failureState, ok, err := runtime.store.GetRouteFailure(ctx, route.RouteID)
		if err != nil {
			return routeSelectionResult{}, err
		}
		if ok && failureState.FailureCount >= group.FailoverThreshold {
			skipped = append(skipped, skippedResolvedRoute{
				route:        route,
				reason:       failureSkipReason(failureState),
				failureCount: failureState.FailureCount,
			})
			continue
		}
		cooldown, ok, err := runtime.store.GetCooldown(ctx, route.RouteID)
		if err != nil {
			return routeSelectionResult{}, err
		}
		if ok && !routeExitsCooldown(cooldown.Until) {
			skipped = append(skipped, skippedResolvedRoute{
				route:  route,
				reason: cooldownSkipReason(cooldown),
			})
			continue
		}
		routeModels, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, route.RouteID)
		if err != nil {
			return routeSelectionResult{}, err
		}
		for _, routeModel := range routeModels {
			if strings.TrimSpace(routeModel.PublicModel) == publicModel && isActiveStatus(routeModel.Status) {
				result := routeSelectionResult{
					candidate: resolvedRouteCandidate{route: route, routeModel: routeModel},
				}
				if len(skipped) > 0 {
					result.fallbackUsed = true
					result.failoverFrom = &skipped[0]
				}
				return result, nil
			}
		}
	}
	return routeSelectionResult{}, fmt.Errorf("no active route found for logical group %q model %q", group.LogicalGroupID, publicModel)
}

func loadResolvedRouteCandidate(ctx context.Context, store *sqlite.DB, logicalGroupID string, publicModel string, routeID string) (resolvedRouteCandidate, error) {
	route, err := getLogicalGroupRouteRow(ctx, store, routeID)
	if err != nil {
		return resolvedRouteCandidate{}, err
	}
	if route.LogicalGroupID != logicalGroupID {
		return resolvedRouteCandidate{}, fmt.Errorf("logical group route %q not found under logical group %q", route.RouteID, logicalGroupID)
	}
	if !isActiveStatus(route.Status) {
		return resolvedRouteCandidate{}, fmt.Errorf("logical group route %q is not active", route.RouteID)
	}
	if !routeExitsCooldown(route.CooldownUntil) {
		return resolvedRouteCandidate{}, fmt.Errorf("logical group route %q is cooling down", route.RouteID)
	}
	routeModels, err := store.LogicalGroupRouteModels().ListByRouteID(ctx, route.RouteID)
	if err != nil {
		return resolvedRouteCandidate{}, err
	}
	for _, routeModel := range routeModels {
		if strings.TrimSpace(routeModel.PublicModel) == strings.TrimSpace(publicModel) && isActiveStatus(routeModel.Status) {
			return resolvedRouteCandidate{route: route, routeModel: routeModel}, nil
		}
	}
	return resolvedRouteCandidate{}, fmt.Errorf("logical group route %q does not expose active model %q", route.RouteID, publicModel)
}

func logicalGroupHasActiveModel(models []sqlite.LogicalGroupModel, publicModel string) bool {
	publicModel = strings.TrimSpace(publicModel)
	for _, model := range models {
		if strings.TrimSpace(model.PublicModel) == publicModel && isActiveStatus(model.Status) {
			return true
		}
	}
	return false
}

func resolveStickyTTL(group sqlite.LogicalGroup, scope string) (time.Duration, error) {
	switch strings.ToLower(strings.TrimSpace(scope)) {
	case routing.StickyScopeConversation, routing.StickyScopeSession:
		return secondsToDuration(group.ConversationTTLSeconds, 0)
	case routing.StickyScopeUser:
		return secondsToDuration(group.UserModelTTLSeconds, 0)
	default:
		return 0, fmt.Errorf("unsupported sticky scope %q", scope)
	}
}

func resolveRouteInfoFromBinding(backend string, stickyKey string, scope string, subjectID string, candidate resolvedRouteCandidate, binding routing.StickyBinding, requestID string, stickyHit bool, stickyAction string, fallbackUsed bool) ResolveRouteInfo {
	return ResolveRouteInfo{
		RequestID:      requestID,
		Backend:        backend,
		LogicalGroupID: binding.LogicalGroupID,
		PublicModel:    binding.PublicModel,
		Scope:          strings.TrimSpace(scope),
		SubjectID:      strings.TrimSpace(subjectID),
		StickyKey:      stickyKey,
		StickyHit:      stickyHit,
		StickyAction:   stickyAction,
		FallbackUsed:   fallbackUsed,
		RouteID:        candidate.route.RouteID,
		RouteName:      candidate.route.Name,
		ShadowGroupID:  candidate.route.ShadowGroupID,
		ShadowHostID:   candidate.route.ShadowHostID,
		ShadowModel:    candidate.routeModel.ShadowModel,
		Priority:       candidate.route.Priority,
		Weight:         candidate.route.Weight,
		BoundAt:        binding.BoundAt,
		ExpiresAt:      binding.ExpiresAt,
	}
}

func resolveUserKey(req ResolveRouteRequest) string {
	if req.UserKey != "" {
		return req.UserKey
	}
	if strings.EqualFold(req.Scope, routing.StickyScopeUser) {
		return req.SubjectID
	}
	return ""
}

func resolveConversationKey(req ResolveRouteRequest) string {
	if req.ConversationKey != "" {
		return req.ConversationKey
	}
	if strings.EqualFold(req.Scope, routing.StickyScopeConversation) || strings.EqualFold(req.Scope, routing.StickyScopeSession) {
		return req.SubjectID
	}
	return ""
}

func isActiveStatus(status string) bool {
	return strings.EqualFold(strings.TrimSpace(status), "active")
}

func isExpiredRFC3339(raw string) bool {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return true
	}
	until, err := time.Parse(time.RFC3339, raw)
	if err != nil {
		return true
	}
	return !until.After(time.Now().UTC())
}

func routeExitsCooldown(raw string) bool {
	return isExpiredRFC3339(raw)
}

func failureSkipReason(state routing.RouteFailureState) string {
	if reason := strings.TrimSpace(state.LastErrorClass); reason != "" {
		return "failure_threshold_exceeded:" + reason
	}
	return "failure_threshold_exceeded"
}

func cooldownSkipReason(state routing.RouteCooldownState) string {
	if reason := strings.TrimSpace(state.Reason); reason != "" {
		return "active_cooldown:" + reason
	}
	return "active_cooldown"
}
