package app

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

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

type AppendRouteDecisionLogRequest struct {
	RequestID             string `json:"request_id"`
	LogicalGroupID        string `json:"logical_group_id"`
	PublicModel           string `json:"public_model"`
	UserKey               string `json:"user_key,omitempty"`
	ConversationKey       string `json:"conversation_key,omitempty"`
	StickyKey             string `json:"sticky_key,omitempty"`
	StickyKeyType         string `json:"sticky_key_type,omitempty"`
	StickyHit             bool   `json:"sticky_hit,omitempty"`
	SelectedRouteID       string `json:"selected_route_id"`
	SelectedShadowGroupID string `json:"selected_shadow_group_id"`
	FallbackUsed          bool   `json:"fallback_used,omitempty"`
	ErrorClass            string `json:"error_class,omitempty"`
	UpstreamStatus        int    `json:"upstream_status,omitempty"`
	LatencyMS             int    `json:"latency_ms,omitempty"`
	Sync                  bool   `json:"sync,omitempty"`
}

type ListRouteDecisionLogsRequest struct {
	RequestID       string
	LogicalGroupID  string
	PublicModel     string
	SelectedRouteID string
	StickyKey       string
	Limit           int
}

type RouteDecisionLogInfo struct {
	ID                    int64  `json:"id"`
	RequestID             string `json:"request_id"`
	LogicalGroupID        string `json:"logical_group_id"`
	PublicModel           string `json:"public_model"`
	UserKey               string `json:"user_key,omitempty"`
	ConversationKey       string `json:"conversation_key,omitempty"`
	StickyKey             string `json:"sticky_key,omitempty"`
	StickyKeyType         string `json:"sticky_key_type,omitempty"`
	StickyHit             bool   `json:"sticky_hit"`
	SelectedRouteID       string `json:"selected_route_id"`
	SelectedShadowGroupID string `json:"selected_shadow_group_id"`
	FallbackUsed          bool   `json:"fallback_used"`
	ErrorClass            string `json:"error_class,omitempty"`
	UpstreamStatus        int    `json:"upstream_status,omitempty"`
	LatencyMS             int    `json:"latency_ms,omitempty"`
	CreatedAt             string `json:"created_at,omitempty"`
}

type AppendRouteFailoverEventRequest struct {
	RequestID      string `json:"request_id"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	FromRouteID    string `json:"from_route_id"`
	ToRouteID      string `json:"to_route_id"`
	Reason         string `json:"reason"`
	FailureCount   int    `json:"failure_count,omitempty"`
	Sync           bool   `json:"sync,omitempty"`
}

type ListRouteFailoverEventsRequest struct {
	RequestID      string
	LogicalGroupID string
	PublicModel    string
	FromRouteID    string
	ToRouteID      string
	Limit          int
}

type RouteFailoverEventInfo struct {
	ID             int64  `json:"id"`
	RequestID      string `json:"request_id"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	FromRouteID    string `json:"from_route_id"`
	ToRouteID      string `json:"to_route_id"`
	Reason         string `json:"reason"`
	FailureCount   int    `json:"failure_count"`
	CreatedAt      string `json:"created_at,omitempty"`
}

type AppendRouteStickyAuditRequest struct {
	StickyKey      string `json:"sticky_key"`
	StickyKeyType  string `json:"sticky_key_type"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	RouteID        string `json:"route_id"`
	Action         string `json:"action"`
	ExpiresAt      string `json:"expires_at,omitempty"`
	Sync           bool   `json:"sync,omitempty"`
}

type ListRouteStickyAuditRequest struct {
	StickyKey      string
	StickyKeyType  string
	LogicalGroupID string
	PublicModel    string
	RouteID        string
	Action         string
	Limit          int
}

type RouteStickyAuditInfo struct {
	ID             int64  `json:"id"`
	StickyKey      string `json:"sticky_key"`
	StickyKeyType  string `json:"sticky_key_type"`
	LogicalGroupID string `json:"logical_group_id"`
	PublicModel    string `json:"public_model"`
	RouteID        string `json:"route_id"`
	Action         string `json:"action"`
	ExpiresAt      string `json:"expires_at,omitempty"`
	CreatedAt      string `json:"created_at,omitempty"`
}

func handleAppendRouteDecisionLog(w http.ResponseWriter, r *http.Request, fn func(context.Context, AppendRouteDecisionLogRequest) (RouteDecisionLogInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "append-route-decision-log action is not configured"})
		return
	}
	var req AppendRouteDecisionLogRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	item, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"decision_log": item})
}

func handleListRouteDecisionLogs(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListRouteDecisionLogsRequest) ([]RouteDecisionLogInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-route-decision-logs action is not configured"})
		return
	}
	req, err := decodeRouteDecisionLogFilter(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	items, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"decision_logs": items})
}

func handleAppendRouteFailoverEvent(w http.ResponseWriter, r *http.Request, fn func(context.Context, AppendRouteFailoverEventRequest) (RouteFailoverEventInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "append-route-failover-event action is not configured"})
		return
	}
	var req AppendRouteFailoverEventRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	item, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"failover_event": item})
}

func handleListRouteFailoverEvents(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListRouteFailoverEventsRequest) ([]RouteFailoverEventInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-route-failover-events action is not configured"})
		return
	}
	req, err := decodeRouteFailoverEventFilter(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	items, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"failover_events": items})
}

func handleAppendRouteStickyAudit(w http.ResponseWriter, r *http.Request, fn func(context.Context, AppendRouteStickyAuditRequest) (RouteStickyAuditInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "append-route-sticky-audit action is not configured"})
		return
	}
	var req AppendRouteStickyAuditRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	item, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"sticky_audit": item})
}

func handleListRouteStickyAudit(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListRouteStickyAuditRequest) ([]RouteStickyAuditInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-route-sticky-audit action is not configured"})
		return
	}
	req, err := decodeRouteStickyAuditFilter(r)
	if err != nil {
		writeHTTPError(w, err)
		return
	}
	items, actionErr := fn(r.Context(), req)
	if actionErr != nil {
		writeHTTPError(w, classifyError(actionErr))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"sticky_audits": items})
}

type lazyRouteLogWriter struct {
	sqliteDSN string
	mu        sync.Mutex
	writer    *routing.AsyncLogWriter
}

func newLazyRouteLogWriter(sqliteDSN string) *lazyRouteLogWriter {
	return &lazyRouteLogWriter{sqliteDSN: sqliteDSN}
}

func (l *lazyRouteLogWriter) get(ctx context.Context) (*routing.AsyncLogWriter, error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	if l.writer != nil {
		return l.writer, nil
	}

	writer, err := routing.NewSQLiteLogWriter(ctx, l.sqliteDSN, routing.AsyncLogWriterOptions{})
	if err != nil {
		return nil, err
	}
	l.writer = writer
	return l.writer, nil
}

func buildAppendRouteDecisionLogAction(writerSource *lazyRouteLogWriter, sqliteDSN string) func(context.Context, AppendRouteDecisionLogRequest) (RouteDecisionLogInfo, error) {
	return func(ctx context.Context, req AppendRouteDecisionLogRequest) (RouteDecisionLogInfo, error) {
		writer, err := writerSource.get(ctx)
		if err != nil {
			return RouteDecisionLogInfo{}, err
		}

		event := routing.RouteDecisionEvent{
			RequestID:             strings.TrimSpace(req.RequestID),
			LogicalGroupID:        strings.TrimSpace(req.LogicalGroupID),
			PublicModel:           strings.TrimSpace(req.PublicModel),
			UserKey:               strings.TrimSpace(req.UserKey),
			ConversationKey:       strings.TrimSpace(req.ConversationKey),
			StickyKey:             strings.TrimSpace(req.StickyKey),
			StickyKeyType:         strings.TrimSpace(req.StickyKeyType),
			StickyHit:             req.StickyHit,
			SelectedRouteID:       strings.TrimSpace(req.SelectedRouteID),
			SelectedShadowGroupID: strings.TrimSpace(req.SelectedShadowGroupID),
			FallbackUsed:          req.FallbackUsed,
			ErrorClass:            strings.TrimSpace(req.ErrorClass),
			UpstreamStatus:        req.UpstreamStatus,
			LatencyMS:             req.LatencyMS,
		}
		if err := writer.AppendDecision(ctx, event); err != nil {
			return RouteDecisionLogInfo{}, err
		}
		if !req.Sync {
			return routeDecisionLogRowToInfo(sqlite.RouteDecisionLog{
				RequestID:             event.RequestID,
				LogicalGroupID:        event.LogicalGroupID,
				PublicModel:           event.PublicModel,
				UserKey:               event.UserKey,
				ConversationKey:       event.ConversationKey,
				StickyKey:             event.StickyKey,
				StickyKeyType:         event.StickyKeyType,
				StickyHit:             event.StickyHit,
				SelectedRouteID:       event.SelectedRouteID,
				SelectedShadowGroupID: event.SelectedShadowGroupID,
				FallbackUsed:          event.FallbackUsed,
				ErrorClass:            event.ErrorClass,
				UpstreamStatus:        event.UpstreamStatus,
				LatencyMS:             event.LatencyMS,
			}), nil
		}
		if err := writer.Flush(ctx); err != nil {
			return RouteDecisionLogInfo{}, err
		}

		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return RouteDecisionLogInfo{}, err
		}
		defer store.Close()

		items, err := store.RouteDecisionLogs().ListRecent(ctx, sqlite.RouteDecisionLogFilter{
			RequestID: event.RequestID,
			Limit:     1,
		})
		if err != nil {
			return RouteDecisionLogInfo{}, err
		}
		if len(items) == 0 {
			return RouteDecisionLogInfo{}, fmt.Errorf("route decision log %q not found after append", event.RequestID)
		}
		return routeDecisionLogRowToInfo(items[0]), nil
	}
}

func buildListRouteDecisionLogsAction(sqliteDSN string) func(context.Context, ListRouteDecisionLogsRequest) ([]RouteDecisionLogInfo, error) {
	return func(ctx context.Context, req ListRouteDecisionLogsRequest) ([]RouteDecisionLogInfo, error) {
		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return nil, err
		}
		defer store.Close()

		rows, err := store.RouteDecisionLogs().ListRecent(ctx, sqlite.RouteDecisionLogFilter{
			RequestID:       strings.TrimSpace(req.RequestID),
			LogicalGroupID:  strings.TrimSpace(req.LogicalGroupID),
			PublicModel:     strings.TrimSpace(req.PublicModel),
			SelectedRouteID: strings.TrimSpace(req.SelectedRouteID),
			StickyKey:       strings.TrimSpace(req.StickyKey),
			Limit:           req.Limit,
		})
		if err != nil {
			return nil, err
		}
		return routeDecisionLogRowsToInfo(rows), nil
	}
}

func buildAppendRouteFailoverEventAction(writerSource *lazyRouteLogWriter, sqliteDSN string) func(context.Context, AppendRouteFailoverEventRequest) (RouteFailoverEventInfo, error) {
	return func(ctx context.Context, req AppendRouteFailoverEventRequest) (RouteFailoverEventInfo, error) {
		writer, err := writerSource.get(ctx)
		if err != nil {
			return RouteFailoverEventInfo{}, err
		}

		event := routing.RouteFailoverEvent{
			RequestID:      strings.TrimSpace(req.RequestID),
			LogicalGroupID: strings.TrimSpace(req.LogicalGroupID),
			PublicModel:    strings.TrimSpace(req.PublicModel),
			FromRouteID:    strings.TrimSpace(req.FromRouteID),
			ToRouteID:      strings.TrimSpace(req.ToRouteID),
			Reason:         strings.TrimSpace(req.Reason),
			FailureCount:   req.FailureCount,
		}
		if err := writer.AppendFailover(ctx, event); err != nil {
			return RouteFailoverEventInfo{}, err
		}
		if !req.Sync {
			return routeFailoverEventRowToInfo(sqlite.RouteFailoverEvent{
				RequestID:      event.RequestID,
				LogicalGroupID: event.LogicalGroupID,
				PublicModel:    event.PublicModel,
				FromRouteID:    event.FromRouteID,
				ToRouteID:      event.ToRouteID,
				Reason:         event.Reason,
				FailureCount:   event.FailureCount,
			}), nil
		}
		if err := writer.Flush(ctx); err != nil {
			return RouteFailoverEventInfo{}, err
		}

		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return RouteFailoverEventInfo{}, err
		}
		defer store.Close()

		items, err := store.RouteFailoverEvents().ListRecent(ctx, sqlite.RouteFailoverEventFilter{
			RequestID: event.RequestID,
			Limit:     1,
		})
		if err != nil {
			return RouteFailoverEventInfo{}, err
		}
		if len(items) == 0 {
			return RouteFailoverEventInfo{}, fmt.Errorf("route failover event %q not found after append", event.RequestID)
		}
		return routeFailoverEventRowToInfo(items[0]), nil
	}
}

func buildListRouteFailoverEventsAction(sqliteDSN string) func(context.Context, ListRouteFailoverEventsRequest) ([]RouteFailoverEventInfo, error) {
	return func(ctx context.Context, req ListRouteFailoverEventsRequest) ([]RouteFailoverEventInfo, error) {
		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return nil, err
		}
		defer store.Close()

		rows, err := store.RouteFailoverEvents().ListRecent(ctx, sqlite.RouteFailoverEventFilter{
			RequestID:      strings.TrimSpace(req.RequestID),
			LogicalGroupID: strings.TrimSpace(req.LogicalGroupID),
			PublicModel:    strings.TrimSpace(req.PublicModel),
			FromRouteID:    strings.TrimSpace(req.FromRouteID),
			ToRouteID:      strings.TrimSpace(req.ToRouteID),
			Limit:          req.Limit,
		})
		if err != nil {
			return nil, err
		}
		return routeFailoverEventRowsToInfo(rows), nil
	}
}

func buildAppendRouteStickyAuditAction(writerSource *lazyRouteLogWriter, sqliteDSN string) func(context.Context, AppendRouteStickyAuditRequest) (RouteStickyAuditInfo, error) {
	return func(ctx context.Context, req AppendRouteStickyAuditRequest) (RouteStickyAuditInfo, error) {
		writer, err := writerSource.get(ctx)
		if err != nil {
			return RouteStickyAuditInfo{}, err
		}

		event := routing.RouteStickyAuditEvent{
			StickyKey:      strings.TrimSpace(req.StickyKey),
			StickyKeyType:  strings.TrimSpace(req.StickyKeyType),
			LogicalGroupID: strings.TrimSpace(req.LogicalGroupID),
			PublicModel:    strings.TrimSpace(req.PublicModel),
			RouteID:        strings.TrimSpace(req.RouteID),
			Action:         strings.TrimSpace(req.Action),
			ExpiresAt:      strings.TrimSpace(req.ExpiresAt),
		}
		if err := writer.AppendStickyAudit(ctx, event); err != nil {
			return RouteStickyAuditInfo{}, err
		}
		if !req.Sync {
			return routeStickyAuditRowToInfo(sqlite.RouteStickyAudit{
				StickyKey:      event.StickyKey,
				StickyKeyType:  event.StickyKeyType,
				LogicalGroupID: event.LogicalGroupID,
				PublicModel:    event.PublicModel,
				RouteID:        event.RouteID,
				Action:         event.Action,
				ExpiresAt:      event.ExpiresAt,
			}), nil
		}
		if err := writer.Flush(ctx); err != nil {
			return RouteStickyAuditInfo{}, err
		}

		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return RouteStickyAuditInfo{}, err
		}
		defer store.Close()

		items, err := store.RouteStickyAudit().ListRecent(ctx, sqlite.RouteStickyAuditFilter{
			StickyKey: event.StickyKey,
			Action:    event.Action,
			Limit:     1,
		})
		if err != nil {
			return RouteStickyAuditInfo{}, err
		}
		if len(items) == 0 {
			return RouteStickyAuditInfo{}, fmt.Errorf("route sticky audit %q not found after append", event.StickyKey)
		}
		return routeStickyAuditRowToInfo(items[0]), nil
	}
}

func buildListRouteStickyAuditAction(sqliteDSN string) func(context.Context, ListRouteStickyAuditRequest) ([]RouteStickyAuditInfo, error) {
	return func(ctx context.Context, req ListRouteStickyAuditRequest) ([]RouteStickyAuditInfo, error) {
		store, err := sqlite.Open(ctx, sqliteDSN)
		if err != nil {
			return nil, err
		}
		defer store.Close()

		rows, err := store.RouteStickyAudit().ListRecent(ctx, sqlite.RouteStickyAuditFilter{
			StickyKey:      strings.TrimSpace(req.StickyKey),
			StickyKeyType:  strings.TrimSpace(req.StickyKeyType),
			LogicalGroupID: strings.TrimSpace(req.LogicalGroupID),
			PublicModel:    strings.TrimSpace(req.PublicModel),
			RouteID:        strings.TrimSpace(req.RouteID),
			Action:         strings.TrimSpace(req.Action),
			Limit:          req.Limit,
		})
		if err != nil {
			return nil, err
		}
		return routeStickyAuditRowsToInfo(rows), nil
	}
}

func decodeRouteDecisionLogFilter(r *http.Request) (ListRouteDecisionLogsRequest, *httpError) {
	limit, err := parseOptionalLimit(r.URL.Query().Get("limit"))
	if err != nil {
		return ListRouteDecisionLogsRequest{}, err
	}
	return ListRouteDecisionLogsRequest{
		RequestID:       strings.TrimSpace(r.URL.Query().Get("request_id")),
		LogicalGroupID:  strings.TrimSpace(r.URL.Query().Get("logical_group_id")),
		PublicModel:     strings.TrimSpace(r.URL.Query().Get("public_model")),
		SelectedRouteID: strings.TrimSpace(r.URL.Query().Get("selected_route_id")),
		StickyKey:       strings.TrimSpace(r.URL.Query().Get("sticky_key")),
		Limit:           limit,
	}, nil
}

func decodeRouteFailoverEventFilter(r *http.Request) (ListRouteFailoverEventsRequest, *httpError) {
	limit, err := parseOptionalLimit(r.URL.Query().Get("limit"))
	if err != nil {
		return ListRouteFailoverEventsRequest{}, err
	}
	return ListRouteFailoverEventsRequest{
		RequestID:      strings.TrimSpace(r.URL.Query().Get("request_id")),
		LogicalGroupID: strings.TrimSpace(r.URL.Query().Get("logical_group_id")),
		PublicModel:    strings.TrimSpace(r.URL.Query().Get("public_model")),
		FromRouteID:    strings.TrimSpace(r.URL.Query().Get("from_route_id")),
		ToRouteID:      strings.TrimSpace(r.URL.Query().Get("to_route_id")),
		Limit:          limit,
	}, nil
}

func decodeRouteStickyAuditFilter(r *http.Request) (ListRouteStickyAuditRequest, *httpError) {
	limit, err := parseOptionalLimit(r.URL.Query().Get("limit"))
	if err != nil {
		return ListRouteStickyAuditRequest{}, err
	}
	return ListRouteStickyAuditRequest{
		StickyKey:      strings.TrimSpace(r.URL.Query().Get("sticky_key")),
		StickyKeyType:  strings.TrimSpace(r.URL.Query().Get("sticky_key_type")),
		LogicalGroupID: strings.TrimSpace(r.URL.Query().Get("logical_group_id")),
		PublicModel:    strings.TrimSpace(r.URL.Query().Get("public_model")),
		RouteID:        strings.TrimSpace(r.URL.Query().Get("route_id")),
		Action:         strings.TrimSpace(r.URL.Query().Get("action")),
		Limit:          limit,
	}, nil
}

func parseOptionalLimit(raw string) (int, *httpError) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, nil
	}
	limit, err := strconv.Atoi(raw)
	if err != nil || limit <= 0 {
		return 0, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "limit must be a positive integer"}
	}
	return limit, nil
}

func routeDecisionLogRowToInfo(row sqlite.RouteDecisionLog) RouteDecisionLogInfo {
	return RouteDecisionLogInfo{
		ID:                    row.ID,
		RequestID:             row.RequestID,
		LogicalGroupID:        row.LogicalGroupID,
		PublicModel:           row.PublicModel,
		UserKey:               row.UserKey,
		ConversationKey:       row.ConversationKey,
		StickyKey:             row.StickyKey,
		StickyKeyType:         row.StickyKeyType,
		StickyHit:             row.StickyHit,
		SelectedRouteID:       row.SelectedRouteID,
		SelectedShadowGroupID: row.SelectedShadowGroupID,
		FallbackUsed:          row.FallbackUsed,
		ErrorClass:            row.ErrorClass,
		UpstreamStatus:        row.UpstreamStatus,
		LatencyMS:             row.LatencyMS,
		CreatedAt:             row.CreatedAt,
	}
}

func routeDecisionLogRowsToInfo(rows []sqlite.RouteDecisionLog) []RouteDecisionLogInfo {
	result := make([]RouteDecisionLogInfo, 0, len(rows))
	for _, row := range rows {
		result = append(result, routeDecisionLogRowToInfo(row))
	}
	return result
}

func routeFailoverEventRowToInfo(row sqlite.RouteFailoverEvent) RouteFailoverEventInfo {
	return RouteFailoverEventInfo{
		ID:             row.ID,
		RequestID:      row.RequestID,
		LogicalGroupID: row.LogicalGroupID,
		PublicModel:    row.PublicModel,
		FromRouteID:    row.FromRouteID,
		ToRouteID:      row.ToRouteID,
		Reason:         row.Reason,
		FailureCount:   row.FailureCount,
		CreatedAt:      row.CreatedAt,
	}
}

func routeFailoverEventRowsToInfo(rows []sqlite.RouteFailoverEvent) []RouteFailoverEventInfo {
	result := make([]RouteFailoverEventInfo, 0, len(rows))
	for _, row := range rows {
		result = append(result, routeFailoverEventRowToInfo(row))
	}
	return result
}

func routeStickyAuditRowToInfo(row sqlite.RouteStickyAudit) RouteStickyAuditInfo {
	return RouteStickyAuditInfo{
		ID:             row.ID,
		StickyKey:      row.StickyKey,
		StickyKeyType:  row.StickyKeyType,
		LogicalGroupID: row.LogicalGroupID,
		PublicModel:    row.PublicModel,
		RouteID:        row.RouteID,
		Action:         row.Action,
		ExpiresAt:      row.ExpiresAt,
		CreatedAt:      row.CreatedAt,
	}
}

func routeStickyAuditRowsToInfo(rows []sqlite.RouteStickyAudit) []RouteStickyAuditInfo {
	result := make([]RouteStickyAuditInfo, 0, len(rows))
	for _, row := range rows {
		result = append(result, routeStickyAuditRowToInfo(row))
	}
	return result
}
