package app

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

	"sub2api-cn-relay-manager/internal/config"
	"sub2api-cn-relay-manager/internal/routing"
)

const defaultStickyTTLSeconds = 600

type stickyStoreRuntime struct {
	backend string
	store   routing.StickyStore
}

type SetStickyBindingRequest struct {
	Scope          string `json:"scope"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	SubjectID      string `json:"subject_id"`
	RouteID        string `json:"route_id"`
	ShadowGroupID  string `json:"shadow_group_id"`
	TTLSeconds     int    `json:"ttl_seconds,omitempty"`
}

type GetStickyBindingRequest struct {
	Scope          string
	LogicalGroupID string
	PublicModel    string
	SubjectID      string
}

type StickyBindingInfo struct {
	Backend        string `json:"backend"`
	Key            string `json:"key"`
	Scope          string `json:"scope"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	SubjectID      string `json:"subject_id"`
	RouteID        string `json:"route_id"`
	ShadowGroupID  string `json:"shadow_group_id"`
	BoundAt        string `json:"bound_at,omitempty"`
	ExpiresAt      string `json:"expires_at,omitempty"`
}

type SetRouteFailureRequest struct {
	RouteID        string `json:"route_id"`
	FailureCount   int    `json:"failure_count"`
	LastErrorClass string `json:"last_error_class,omitempty"`
	TTLSeconds     int    `json:"ttl_seconds,omitempty"`
}

type RouteFailureInfo struct {
	Backend        string `json:"backend"`
	Key            string `json:"key"`
	RouteID        string `json:"route_id"`
	FailureCount   int    `json:"failure_count"`
	LastErrorClass string `json:"last_error_class,omitempty"`
	LastFailureAt  string `json:"last_failure_at,omitempty"`
	ExpiresAt      string `json:"expires_at,omitempty"`
}

type GetRouteFailureRequest struct {
	RouteID string
}

type SetRouteCooldownRequest struct {
	RouteID    string `json:"route_id"`
	Reason     string `json:"reason,omitempty"`
	TTLSeconds int    `json:"ttl_seconds,omitempty"`
}

type GetRouteCooldownRequest struct {
	RouteID string
}

type RouteCooldownInfo struct {
	Backend   string `json:"backend"`
	Key       string `json:"key"`
	RouteID   string `json:"route_id"`
	Reason    string `json:"reason,omitempty"`
	Until     string `json:"until,omitempty"`
	ExpiresAt string `json:"expires_at,omitempty"`
}

func newStickyStoreRuntime(ctx context.Context, cfg config.RouteRuntimeConfig) (stickyStoreRuntime, error) {
	backend, err := routing.NormalizeRuntimeBackend(cfg.Backend)
	if err != nil {
		return stickyStoreRuntime{}, err
	}

	switch backend {
	case routing.RuntimeBackendMemory:
		return stickyStoreRuntime{
			backend: routing.RuntimeBackendMemory,
			store:   routing.NewInMemoryStickyStore(),
		}, nil
	case routing.RuntimeBackendRedis:
		store, err := routing.NewRedisStickyStore(ctx, routing.RedisConfig{
			Addr:     cfg.Redis.Addr,
			Password: cfg.Redis.Password,
			DB:       cfg.Redis.DB,
		})
		if err != nil {
			return stickyStoreRuntime{}, err
		}
		return stickyStoreRuntime{
			backend: routing.RuntimeBackendRedis,
			store:   store,
		}, nil
	default:
		return stickyStoreRuntime{}, fmt.Errorf("unsupported sticky backend %q", backend)
	}
}

func defaultStickyStoreRuntime() stickyStoreRuntime {
	return stickyStoreRuntime{
		backend: routing.RuntimeBackendMemory,
		store:   routing.NewInMemoryStickyStore(),
	}
}

func handleSetStickyBinding(w http.ResponseWriter, r *http.Request, fn func(context.Context, SetStickyBindingRequest) (StickyBindingInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "set-sticky-binding action is not configured"})
		return
	}
	var req SetStickyBindingRequest
	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.StatusCreated, map[string]any{"binding": info})
}

func handleGetStickyBinding(w http.ResponseWriter, r *http.Request, fn func(context.Context, GetStickyBindingRequest) (StickyBindingInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-sticky-binding action is not configured"})
		return
	}
	req, err := decodeGetStickyBindingRequest(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	info, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"binding": info})
}

func handleSetRouteFailure(w http.ResponseWriter, r *http.Request, fn func(context.Context, SetRouteFailureRequest) (RouteFailureInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "set-route-failure action is not configured"})
		return
	}
	var req SetRouteFailureRequest
	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.StatusCreated, map[string]any{"route_failure": info})
}

func handleGetRouteFailure(w http.ResponseWriter, r *http.Request, fn func(context.Context, GetRouteFailureRequest) (RouteFailureInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-route-failure action is not configured"})
		return
	}
	req, err := decodeGetRouteFailureRequest(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	info, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"route_failure": info})
}

func handleSetRouteCooldown(w http.ResponseWriter, r *http.Request, fn func(context.Context, SetRouteCooldownRequest) (RouteCooldownInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "set-route-cooldown action is not configured"})
		return
	}
	var req SetRouteCooldownRequest
	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.StatusCreated, map[string]any{"route_cooldown": info})
}

func handleGetRouteCooldown(w http.ResponseWriter, r *http.Request, fn func(context.Context, GetRouteCooldownRequest) (RouteCooldownInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-route-cooldown action is not configured"})
		return
	}
	req, err := decodeGetRouteCooldownRequest(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	info, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"route_cooldown": info})
}

func buildSetStickyBindingAction(runtime stickyStoreRuntime) func(context.Context, SetStickyBindingRequest) (StickyBindingInfo, error) {
	return func(ctx context.Context, req SetStickyBindingRequest) (StickyBindingInfo, error) {
		key, err := routing.BuildStickyKey(req.Scope, req.LogicalGroupID, req.PublicModel, req.SubjectID)
		if err != nil {
			return StickyBindingInfo{}, err
		}
		ttl, err := secondsToDuration(req.TTLSeconds, defaultStickyTTLSeconds)
		if err != nil {
			return StickyBindingInfo{}, err
		}
		binding := routing.StickyBinding{
			LogicalGroupID: strings.TrimSpace(req.LogicalGroupID),
			PublicModel:    strings.TrimSpace(req.PublicModel),
			RouteID:        strings.TrimSpace(req.RouteID),
			ShadowGroupID:  strings.TrimSpace(req.ShadowGroupID),
		}
		if err := runtime.store.Set(ctx, key, binding, ttl); err != nil {
			return StickyBindingInfo{}, err
		}
		stored, ok, err := runtime.store.Get(ctx, key)
		if err != nil {
			return StickyBindingInfo{}, err
		}
		if !ok {
			return StickyBindingInfo{}, fmt.Errorf("sticky binding %q not found after set", key)
		}
		return stickyBindingToInfo(runtime.backend, req.Scope, req.SubjectID, key, stored), nil
	}
}

func buildGetStickyBindingAction(runtime stickyStoreRuntime) func(context.Context, GetStickyBindingRequest) (StickyBindingInfo, error) {
	return func(ctx context.Context, req GetStickyBindingRequest) (StickyBindingInfo, error) {
		key, err := routing.BuildStickyKey(req.Scope, req.LogicalGroupID, req.PublicModel, req.SubjectID)
		if err != nil {
			return StickyBindingInfo{}, err
		}
		stored, ok, err := runtime.store.Get(ctx, key)
		if err != nil {
			return StickyBindingInfo{}, err
		}
		if !ok {
			return StickyBindingInfo{}, fmt.Errorf("sticky binding %q not found", key)
		}
		return stickyBindingToInfo(runtime.backend, req.Scope, req.SubjectID, key, stored), nil
	}
}

func buildSetRouteFailureAction(runtime stickyStoreRuntime) func(context.Context, SetRouteFailureRequest) (RouteFailureInfo, error) {
	return func(ctx context.Context, req SetRouteFailureRequest) (RouteFailureInfo, error) {
		key, err := routing.BuildRouteFailureKey(req.RouteID)
		if err != nil {
			return RouteFailureInfo{}, err
		}
		ttl, err := secondsToDuration(req.TTLSeconds, defaultStickyTTLSeconds)
		if err != nil {
			return RouteFailureInfo{}, err
		}
		state := routing.RouteFailureState{
			RouteID:        strings.TrimSpace(req.RouteID),
			FailureCount:   req.FailureCount,
			LastErrorClass: strings.TrimSpace(req.LastErrorClass),
		}
		if err := runtime.store.SetRouteFailure(ctx, req.RouteID, state, ttl); err != nil {
			return RouteFailureInfo{}, err
		}
		stored, ok, err := runtime.store.GetRouteFailure(ctx, req.RouteID)
		if err != nil {
			return RouteFailureInfo{}, err
		}
		if !ok {
			return RouteFailureInfo{}, fmt.Errorf("route failure %q not found after set", req.RouteID)
		}
		return routeFailureToInfo(runtime.backend, key, stored), nil
	}
}

func buildGetRouteFailureAction(runtime stickyStoreRuntime) func(context.Context, GetRouteFailureRequest) (RouteFailureInfo, error) {
	return func(ctx context.Context, req GetRouteFailureRequest) (RouteFailureInfo, error) {
		key, err := routing.BuildRouteFailureKey(req.RouteID)
		if err != nil {
			return RouteFailureInfo{}, err
		}
		stored, ok, err := runtime.store.GetRouteFailure(ctx, req.RouteID)
		if err != nil {
			return RouteFailureInfo{}, err
		}
		if !ok {
			return RouteFailureInfo{}, fmt.Errorf("route failure %q not found", req.RouteID)
		}
		return routeFailureToInfo(runtime.backend, key, stored), nil
	}
}

func buildSetRouteCooldownAction(runtime stickyStoreRuntime) func(context.Context, SetRouteCooldownRequest) (RouteCooldownInfo, error) {
	return func(ctx context.Context, req SetRouteCooldownRequest) (RouteCooldownInfo, error) {
		key, err := routing.BuildRouteCooldownKey(req.RouteID)
		if err != nil {
			return RouteCooldownInfo{}, err
		}
		ttl, err := secondsToDuration(req.TTLSeconds, defaultStickyTTLSeconds)
		if err != nil {
			return RouteCooldownInfo{}, err
		}
		state := routing.RouteCooldownState{
			RouteID: strings.TrimSpace(req.RouteID),
			Reason:  strings.TrimSpace(req.Reason),
		}
		if err := runtime.store.SetCooldown(ctx, req.RouteID, state, ttl); err != nil {
			return RouteCooldownInfo{}, err
		}
		stored, ok, err := runtime.store.GetCooldown(ctx, req.RouteID)
		if err != nil {
			return RouteCooldownInfo{}, err
		}
		if !ok {
			return RouteCooldownInfo{}, fmt.Errorf("route cooldown %q not found after set", req.RouteID)
		}
		return routeCooldownToInfo(runtime.backend, key, stored), nil
	}
}

func buildGetRouteCooldownAction(runtime stickyStoreRuntime) func(context.Context, GetRouteCooldownRequest) (RouteCooldownInfo, error) {
	return func(ctx context.Context, req GetRouteCooldownRequest) (RouteCooldownInfo, error) {
		key, err := routing.BuildRouteCooldownKey(req.RouteID)
		if err != nil {
			return RouteCooldownInfo{}, err
		}
		stored, ok, err := runtime.store.GetCooldown(ctx, req.RouteID)
		if err != nil {
			return RouteCooldownInfo{}, err
		}
		if !ok {
			return RouteCooldownInfo{}, fmt.Errorf("route cooldown %q not found", req.RouteID)
		}
		return routeCooldownToInfo(runtime.backend, key, stored), nil
	}
}

func decodeGetStickyBindingRequest(r *http.Request) (GetStickyBindingRequest, *httpError) {
	req := GetStickyBindingRequest{
		Scope:          strings.TrimSpace(r.URL.Query().Get("scope")),
		LogicalGroupID: strings.TrimSpace(r.URL.Query().Get("logical_group_id")),
		PublicModel:    strings.TrimSpace(r.URL.Query().Get("public_model")),
		SubjectID:      strings.TrimSpace(r.URL.Query().Get("subject_id")),
	}
	if req.Scope == "" || req.LogicalGroupID == "" || req.PublicModel == "" || req.SubjectID == "" {
		return GetStickyBindingRequest{}, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "scope, logical_group_id, public_model, subject_id are required"}
	}
	return req, nil
}

func decodeGetRouteFailureRequest(r *http.Request) (GetRouteFailureRequest, *httpError) {
	req := GetRouteFailureRequest{
		RouteID: strings.TrimSpace(r.URL.Query().Get("route_id")),
	}
	if req.RouteID == "" {
		return GetRouteFailureRequest{}, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "route_id is required"}
	}
	return req, nil
}

func decodeGetRouteCooldownRequest(r *http.Request) (GetRouteCooldownRequest, *httpError) {
	req := GetRouteCooldownRequest{
		RouteID: strings.TrimSpace(r.URL.Query().Get("route_id")),
	}
	if req.RouteID == "" {
		return GetRouteCooldownRequest{}, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "route_id is required"}
	}
	return req, nil
}

func secondsToDuration(raw int, defaultSeconds int) (time.Duration, error) {
	if raw == 0 {
		raw = defaultSeconds
	}
	if raw < 0 {
		return 0, fmt.Errorf("ttl_seconds must be >= 0")
	}
	if raw == 0 {
		return 0, fmt.Errorf("ttl_seconds must be > 0")
	}
	return time.Duration(raw) * time.Second, nil
}

func stickyBindingToInfo(backend, scope, subjectID, key string, binding routing.StickyBinding) StickyBindingInfo {
	return StickyBindingInfo{
		Backend:        backend,
		Key:            key,
		Scope:          strings.TrimSpace(scope),
		SubjectID:      strings.TrimSpace(subjectID),
		LogicalGroupID: binding.LogicalGroupID,
		PublicModel:    binding.PublicModel,
		RouteID:        binding.RouteID,
		ShadowGroupID:  binding.ShadowGroupID,
		BoundAt:        binding.BoundAt,
		ExpiresAt:      binding.ExpiresAt,
	}
}

func routeFailureToInfo(backend, key string, state routing.RouteFailureState) RouteFailureInfo {
	return RouteFailureInfo{
		Backend:        backend,
		Key:            key,
		RouteID:        state.RouteID,
		FailureCount:   state.FailureCount,
		LastErrorClass: state.LastErrorClass,
		LastFailureAt:  state.LastFailureAt,
		ExpiresAt:      state.ExpiresAt,
	}
}

func routeCooldownToInfo(backend, key string, state routing.RouteCooldownState) RouteCooldownInfo {
	return RouteCooldownInfo{
		Backend:   backend,
		Key:       key,
		RouteID:   state.RouteID,
		Reason:    state.Reason,
		Until:     state.Until,
		ExpiresAt: state.ExpiresAt,
	}
}

func parseTTLQuery(raw string) (int, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, nil
	}
	value, err := strconv.Atoi(raw)
	if err != nil {
		return 0, fmt.Errorf("ttl_seconds must be a positive integer")
	}
	return value, nil
}
