package querycodec

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

const (
	queryLocatorVersion = 2
	defaultBatchSize    = 2000
	minimumBatchSize    = 200
	maximumBatchSize    = 2000
	historyBatchSize    = 200
)

const aggregateQueryMoreUnsupportedMessage = "Aggregate query does not support queryMore(), use LIMIT to restrict the results to a single batch"
const historySemiJoinUnsupportedMessage = "semi-join subqueries (IN (SELECT ...)) are not supported for history queries"
const toolingQueryMoreUnsupportedMessage = "Tooling query result exceeds the supported initial batch; use LIMIT to request one bounded result"

var supportedVersions = [...]string{"v59.0", "v60.0", "v61.0", "v62.0", "v63.0"}

type RouteKind string

const (
	RouteQuery        RouteKind = "QUERY"
	RouteQueryAll     RouteKind = "QUERY_ALL"
	RouteMore         RouteKind = "QUERY_MORE"
	RouteToolingQuery RouteKind = "TOOLING_QUERY"
)

type Route struct {
	Kind    RouteKind
	Version string
	Locator string
}

type RouteSpec struct {
	Kind         RouteKind
	Method       string
	RelativePath string
}

var routeSpecs = [...]RouteSpec{
	{Kind: RouteQuery, Method: http.MethodGet, RelativePath: "/query"},
	{Kind: RouteQueryAll, Method: http.MethodGet, RelativePath: "/queryAll"},
	{Kind: RouteMore, Method: http.MethodGet, RelativePath: "/query/:locator"},
	{Kind: RouteToolingQuery, Method: http.MethodGet, RelativePath: "/tooling/query"},
}

// SupportedVersions returns an independent copy of the CRM API versions that
// own this query family. The legacy partition and AppService must not maintain
// parallel version lists.
func SupportedVersions() []string { return append([]string(nil), supportedVersions[:]...) }

// RouteSpecs returns an independent copy of the product route declarations so
// the transitional dialect manifest and the AppService matcher cannot drift.
func RouteSpecs() []RouteSpec { return append([]RouteSpec(nil), routeSpecs[:]...) }

// Match is the single source for the independently deployed AppService and
// the transitional in-process dialect's query route grammar.
func Match(path string) (Route, bool) {
	for _, version := range supportedVersions {
		prefix := "/services/data/" + version
		for _, spec := range routeSpecs {
			if spec.Kind != RouteMore && path == prefix+spec.RelativePath {
				return Route{Kind: spec.Kind, Version: version}, true
			}
			if spec.Kind == RouteMore {
				routePrefix := prefix + strings.TrimSuffix(spec.RelativePath, ":locator")
				if strings.HasPrefix(path, routePrefix) {
					locator := strings.TrimPrefix(path, routePrefix)
					if locator != "" && !strings.Contains(locator, "/") {
						return Route{Kind: spec.Kind, Version: version, Locator: locator}, true
					}
				}
			}
		}
	}
	return Route{}, false
}

type Request struct {
	Route  Route
	Query  url.Values
	Header http.Header
}

type Response struct {
	Status int
	Header http.Header
	Body   any
}

type Record struct {
	ObjectType string
	Fields     map[string]any
}

type Result struct {
	TotalSize        int
	TotalSizeUnknown bool
	Done             bool
	Aggregate        bool
	Records          []Record
}

type HistoryRequest struct {
	ParentObjectAPIName string
	SelectFields        []string
	ParentID            string
	Filter              Predicate
	Limit               int
	Offset              int
	SortDescending      bool
	IncludeDeleted      bool
	PageSize            int
	CursorOffset        int
}

type HistoryRecord struct {
	ID          string
	ParentID    string
	Field       string
	OldValue    *string
	NewValue    *string
	DataType    string
	CreatedDate string
	CreatedByID string
}

type HistoryResult struct {
	TotalSize int
	Done      bool
	Rows      []HistoryRecord
}

type Executor struct {
	Dates   func(context.Context, *QueryRequest, *DateAnchor) (context.Context, error)
	Query   func(context.Context, QueryRequest, int) (Result, error)
	History func(context.Context, HistoryRequest) (HistoryResult, error)
}

type Handler struct {
	executor Executor
}

func NewHandler(executor Executor) (*Handler, error) {
	if executor.Query == nil || executor.History == nil {
		return nil, errors.New("sfcrm query codec: query and history executors are required")
	}
	return &Handler{executor: executor}, nil
}

type FailureCategory string

const (
	FailureInvalid          FailureCategory = "INVALID"
	FailureMalformed        FailureCategory = "MALFORMED"
	FailureInvalidRequest   FailureCategory = "INVALID_REQUEST"
	FailureInvalidReference FailureCategory = "INVALID_REFERENCE"
	FailureFieldIntegrity   FailureCategory = "FIELD_INTEGRITY"
	FailureInvalidOperation FailureCategory = "INVALID_OPERATION"
	FailureInvalidField     FailureCategory = "INVALID_FIELD"
	FailureRequestTooLarge  FailureCategory = "REQUEST_TOO_LARGE"
	FailureUnauthenticated  FailureCategory = "UNAUTHENTICATED"
	FailureForbidden        FailureCategory = "FORBIDDEN"
	FailureMissing          FailureCategory = "MISSING"
	FailureConflict         FailureCategory = "CONFLICT"
	FailureCanceled         FailureCategory = "CANCELED"
	FailureDeadline         FailureCategory = "DEADLINE"
	FailureExhausted        FailureCategory = "EXHAUSTED"
	FailureResponseTooLarge FailureCategory = "RESPONSE_TOO_LARGE"
	FailureUnavailable      FailureCategory = "UNAVAILABLE"
	FailureInternal         FailureCategory = "INTERNAL"
)

type Failure struct {
	Category FailureCategory
	Message  string
	Fields   []string
}

func (e *Failure) Error() string { return e.Message }

type errorEnvelope struct {
	ErrorCode string   `json:"errorCode"`
	Message   string   `json:"message"`
	Fields    []string `json:"fields,omitempty"`
}

func (h *Handler) Handle(ctx context.Context, request Request) Response {
	if h == nil {
		return crmError(http.StatusInternalServerError, "INTERNAL_ERROR", genericInternalMessage)
	}
	if err := ctx.Err(); err != nil {
		return projectFailure(err)
	}
	switch request.Route.Kind {
	case RouteQuery:
		return h.execute(ctx, request, false, false)
	case RouteQueryAll:
		return h.execute(ctx, request, true, false)
	case RouteToolingQuery:
		return h.execute(ctx, request, false, true)
	case RouteMore:
		return h.queryMore(ctx, request)
	default:
		return crmError(http.StatusNotFound, "NOT_FOUND", "Resource not found")
	}
}

type resultWindow struct {
	limit  int
	offset int
}

type locatorData struct {
	DateAnchor *DateAnchor `json:"dateAnchor,omitempty"`
	Version    int         `json:"v,omitempty"`
	Query      string      `json:"q"`
	Offset     int         `json:"offset"`
	Limit      int         `json:"limit"`
	ScanAll    bool        `json:"scanAll"`
	SOQLLimit  int         `json:"soqlLimit,omitempty"`
	SOQLOffset int         `json:"soqlOffset,omitempty"`
}

func (h *Handler) execute(ctx context.Context, request Request, scanAll, tooling bool) Response {
	raw := firstQueryValue(request.Query, "q")
	if raw == "" {
		if tooling {
			return crmError(http.StatusBadRequest, "INVALID_QUERY", "q parameter is required")
		}
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", "query string has to be specified")
	}
	parsed, err := ParseSOQLWith(raw, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", normalizeParseError(err))
	}
	if ctx, err = h.resolveDates(ctx, parsed, nil); err != nil {
		return projectFailure(err)
	}
	parsed.ScanAll = scanAll
	parsed.SkipTotalSize = strings.EqualFold(firstQueryValue(request.Query, "includeTotalSize"), "false")
	toolingTable := ""
	if tooling {
		toolingTable = translateToolingAliases(parsed)
	}
	if historySemiJoin(parsed) {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", historySemiJoinUnsupportedMessage)
	}
	if IsHistoryObject(parsed.ObjectAPIName) {
		return h.executeHistory(ctx, request, parsed, raw, historyBatchSize, 0)
	}
	if err := lowerSemiJoins(parsed); err != nil {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", "Invalid query")
	}

	soqlWindow := initialWindow(parsed)
	window := soqlWindow
	hasPageParams := firstQueryValue(request.Query, "offset") != "" || firstQueryValue(request.Query, "limit") != ""
	if hasPageParams {
		window = resultWindow{}
	}
	skipTotalSupported := parsed.SkipTotalSize && hasPageParams && soqlWindow.limit == 0 && soqlWindow.offset == 0
	if value := firstQueryValue(request.Query, "offset"); value != "" {
		parsed.Offset, _ = strconv.Atoi(value)
	}
	if value := firstQueryValue(request.Query, "limit"); value != "" {
		parsed.Limit, _ = strconv.Atoi(value)
	}
	parsed.SkipTotalSize = skipTotalSupported && parsed.Offset == 0

	batchSize := batchSizeFromHeader(request.Header.Get("Sforce-Query-Options"))
	aggregateFetchLimit := 0
	if parsed.GroupBy != "" && hasPageParams {
		return crmError(http.StatusBadRequest, "EXCEEDED_ID_LIMIT", aggregateQueryMoreUnsupportedMessage)
	}
	if window.limit > 0 {
		parsed.Limit = pageSize(batchSize, window, parsed.Offset)
		if parsed.GroupBy != "" {
			aggregateFetchLimit = groupedAggregateProbeLimit(window.limit, batchSize) + window.offset
		}
	} else if parsed.GroupBy == "" && (parsed.Limit <= 0 || !hasPageParams) {
		parsed.Limit = batchSize
	} else if parsed.GroupBy != "" {
		aggregateFetchLimit = parsed.Offset + batchSize + 1
	}
	parsed.SkipTotalSize = parsed.SkipTotalSize || canSkipExactTotalSize(parsed, window, batchSize)

	result, err := h.executor.Query(ctx, *parsed, aggregateFetchLimit)
	if err != nil {
		return projectFailure(err)
	}
	if groupedAggregateExceedsBatch(parsed, result, window, batchSize) {
		return crmError(http.StatusBadRequest, "EXCEEDED_ID_LIMIT", aggregateQueryMoreUnsupportedMessage)
	}
	responseWindow := window
	if parsed.GroupBy != "" && responseWindow.limit <= 0 && parsed.Offset > 0 {
		responseWindow = resultWindow{limit: batchSize, offset: parsed.Offset}
	}
	response, returned, done := formatResult(request.Route.Version, parsed, result, responseWindow, parsed.Limit)
	if tooling && !done {
		return crmError(http.StatusBadRequest, "EXCEEDED_ID_LIMIT", toolingQueryMoreUnsupportedMessage)
	}
	if tooling {
		if err := projectToolingAttributes(response, request.Route.Version, toolingTable, result.Aggregate); err != nil {
			return crmError(http.StatusInternalServerError, "INTERNAL_ERROR", genericInternalMessage)
		}
	}
	if !done {
		response.Body.(map[string]any)["nextRecordsUrl"] = nextURL(request.Route.Version, parsed, raw, window, returned)
	}
	return response
}

func (h *Handler) queryMore(ctx context.Context, request Request) Response {
	data, err := base64.URLEncoding.DecodeString(request.Route.Locator)
	if err != nil {
		return crmError(http.StatusBadRequest, "INVALID_QUERY_LOCATOR", "bad id")
	}
	locator, err := decodeLocator(data)
	if err != nil {
		return crmError(http.StatusBadRequest, "INVALID_QUERY_LOCATOR", "invalid query locator")
	}
	raw, err := url.QueryUnescape(locator.Query)
	if err != nil {
		raw = locator.Query
	}
	parsed, err := ParseSOQLWith(raw, ParseOpts{AllowSemiJoin: true})
	if err != nil {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", normalizeParseError(err))
	}
	// Relative-date continuations must carry their original calendar. Missing
	// anchors must not be treated as a fresh query with a newly sampled clock.
	if locator.DateAnchor != nil || hasRelativeDates(parsed) {
		if _, err := dateAnchorLocation(locator.DateAnchor); err != nil {
			return crmError(http.StatusBadRequest, "INVALID_QUERY_LOCATOR", "invalid query date anchor")
		}
	}
	if ctx, err = h.resolveDates(ctx, parsed, locator.DateAnchor); err != nil {
		return projectFailure(err)
	}
	parsed.ScanAll = locator.ScanAll
	if historySemiJoin(parsed) {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", historySemiJoinUnsupportedMessage)
	}
	if IsHistoryObject(parsed.ObjectAPIName) {
		return h.executeHistory(ctx, request, parsed, raw, locator.Limit, locator.Offset)
	}
	if err := lowerSemiJoins(parsed); err != nil {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", "Invalid query")
	}
	if parsed.GroupBy != "" {
		return crmError(http.StatusBadRequest, "EXCEEDED_ID_LIMIT", aggregateQueryMoreUnsupportedMessage)
	}
	window := resultWindow{}
	if locator.Version == queryLocatorVersion {
		window = resultWindow{limit: locator.SOQLLimit, offset: locator.SOQLOffset}
	}
	parsed.Offset, parsed.Limit = locator.Offset, locator.Limit
	if locator.Version == queryLocatorVersion {
		parsed.Limit = pageSize(locator.Limit, window, parsed.Offset)
		if window.limit > 0 && parsed.Limit <= 0 {
			return Response{Status: http.StatusOK, Body: map[string]any{"totalSize": window.limit, "done": true, "records": []any{}}}
		}
	}
	result, err := h.executor.Query(ctx, *parsed, 0)
	if err != nil {
		return projectFailure(err)
	}
	response, returned, done := formatResult(request.Route.Version, parsed, result, window, parsed.Limit)
	if !done {
		response.Body.(map[string]any)["nextRecordsUrl"] = nextURL(request.Route.Version, parsed, raw, window, returned)
	}
	return response
}

func decodeLocator(data []byte) (locatorData, error) {
	var locator locatorData
	if err := json.Unmarshal(data, &locator); err != nil {
		return locatorData{}, err
	}
	if locator.Version != 0 && locator.Version != queryLocatorVersion {
		return locatorData{}, fmt.Errorf("unsupported query locator version %d", locator.Version)
	}
	if locator.Query == "" {
		return locatorData{}, errors.New("missing query")
	}
	return locator, nil
}

func initialWindow(request *QueryRequest) resultWindow {
	if request == nil || request.Limit <= 0 {
		return resultWindow{}
	}
	return resultWindow{limit: request.Limit, offset: request.Offset}
}

func batchSizeFromHeader(header string) int {
	if strings.TrimSpace(header) == "" {
		return defaultBatchSize
	}
	for _, part := range strings.FieldsFunc(header, func(r rune) bool { return r == ';' || r == ',' }) {
		key, value, ok := strings.Cut(part, "=")
		if !ok || !strings.EqualFold(strings.TrimSpace(key), "batchSize") {
			continue
		}
		batch, err := strconv.Atoi(strings.TrimSpace(value))
		if err != nil || batch < minimumBatchSize || batch > maximumBatchSize {
			return defaultBatchSize
		}
		return batch
	}
	return defaultBatchSize
}

func pageSize(batch int, window resultWindow, offset int) int {
	if batch <= 0 {
		batch = defaultBatchSize
	}
	if window.limit <= 0 {
		return batch
	}
	remaining := window.limit - max(0, offset-window.offset)
	if remaining <= 0 {
		return 0
	}
	return min(batch, remaining)
}

func canSkipExactTotalSize(request *QueryRequest, window resultWindow, batch int) bool {
	if request == nil || request.GroupBy != "" || window.limit <= 0 || window.offset > 0 {
		return false
	}
	if batch <= 0 {
		batch = defaultBatchSize
	}
	return window.limit <= batch
}

func groupedAggregateProbeLimit(limit, batch int) int {
	if batch <= 0 {
		batch = defaultBatchSize
	}
	if limit <= 0 || limit > batch+1 {
		return batch + 1
	}
	return limit
}

func groupedAggregateExceedsBatch(request *QueryRequest, result Result, _ resultWindow, batch int) bool {
	if request == nil || request.GroupBy == "" || !result.Aggregate {
		return false
	}
	if batch <= 0 {
		batch = defaultBatchSize
	}
	records := recordsFromOffset(result.Records, request.Offset)
	return len(records) > batch
}

func formatResult(version string, request *QueryRequest, result Result, window resultWindow, batch int) (Response, int, bool) {
	records, total, done := applyWindow(result, window, request.Offset, batch)
	childRelationships := make(map[string]struct{}, len(request.SubQueries))
	for _, subquery := range request.SubQueries {
		childRelationships[subquery.RelationshipName] = struct{}{}
	}
	projected := make([]any, 0, len(records))
	for _, record := range records {
		row, err := projectRecord(version, request.ObjectAPIName, record, result.Aggregate, true, childRelationships)
		if err != nil {
			return crmError(http.StatusInternalServerError, "INTERNAL_ERROR", genericInternalMessage), 0, true
		}
		projected = append(projected, row)
	}
	return Response{Status: http.StatusOK, Body: map[string]any{"totalSize": total, "done": done, "records": projected}}, len(records), done
}

func applyWindow(result Result, window resultWindow, physicalOffset, batch int) ([]Record, int, bool) {
	records := result.Records
	if window.limit <= 0 {
		if result.TotalSizeUnknown && !result.Aggregate {
			return records, 0, result.Done
		}
		return records, result.TotalSize, result.Done
	}
	cursorOffset := max(0, physicalOffset-window.offset)
	if result.TotalSizeUnknown && !result.Aggregate {
		total := window.limit
		if result.Done && cursorOffset+len(records) < total {
			total = cursorOffset + len(records)
		}
		remaining := max(0, total-cursorOffset)
		if len(records) > remaining {
			records = records[:remaining]
		}
		return records, total, cursorOffset+len(records) >= total
	}
	total := result.TotalSize
	if window.offset > 0 {
		total = max(0, total-window.offset)
	}
	total = min(total, window.limit)
	if result.Aggregate {
		records = recordsFromOffset(records, physicalOffset)
	}
	remaining := max(0, total-cursorOffset)
	if result.Aggregate && batch > 0 {
		remaining = min(remaining, batch)
	}
	if len(records) > remaining {
		records = records[:remaining]
	}
	done := cursorOffset+len(records) >= total
	if !result.Aggregate {
		done = result.Done || done
	}
	return records, total, done
}

func recordsFromOffset(records []Record, offset int) []Record {
	if offset <= 0 {
		return records
	}
	if offset >= len(records) {
		return nil
	}
	return records[offset:]
}

func nextURL(version string, request *QueryRequest, raw string, window resultWindow, returned int) string {
	limit := request.Limit
	if limit == 0 {
		limit = defaultBatchSize
	}
	nextOffset := request.Offset + limit
	locator := locatorData{Query: url.QueryEscape(raw), Offset: nextOffset, Limit: limit, ScanAll: request.ScanAll}
	if window.limit > 0 {
		nextOffset = request.Offset + returned
		if returned <= 0 {
			nextOffset = request.Offset + limit
		}
		locator = locatorData{Version: queryLocatorVersion, Query: url.QueryEscape(raw), Offset: nextOffset, Limit: limit, ScanAll: request.ScanAll, SOQLLimit: window.limit, SOQLOffset: window.offset}
	}
	locator.DateAnchor = request.DateAnchor
	data, _ := json.Marshal(locator)
	return fmt.Sprintf("/services/data/%s/query/%s", versionOrDefault(version), base64.URLEncoding.EncodeToString(data))
}

func firstQueryValue(values url.Values, key string) string {
	if values == nil {
		return ""
	}
	return values.Get(key)
}

func versionOrDefault(version string) string {
	if version == "" {
		return "v63.0"
	}
	return version
}

func historySemiJoin(request *QueryRequest) bool {
	if request == nil || len(request.SemiJoins) == 0 {
		return false
	}
	if IsHistoryObject(request.ObjectAPIName) {
		return true
	}
	for _, clause := range request.SemiJoins {
		if clause.InnerQuery != nil && IsHistoryObject(clause.InnerQuery.ObjectAPIName) {
			return true
		}
	}
	return false
}

func normalizeParseError(err error) string {
	if err == nil {
		return ""
	}
	if strings.Contains(strings.ToLower(err.Error()), "unexpected keyword") {
		return "unexpected token"
	}
	return err.Error()
}

const genericInternalMessage = "An internal server error occurred. Contact your system administrator."

func crmError(status int, code, message string) Response {
	return crmErrorWithFields(status, code, message, nil)
}

func crmErrorWithFields(status int, code, message string, fields []string) Response {
	return Response{Status: status, Body: []errorEnvelope{{ErrorCode: code, Message: message, Fields: append([]string(nil), fields...)}}}
}

func projectFailure(err error) Response {
	if errors.Is(err, context.Canceled) {
		return crmError(499, "UNKNOWN_EXCEPTION", "Request canceled")
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return crmError(http.StatusGatewayTimeout, "UNKNOWN_EXCEPTION", "Request deadline exceeded")
	}
	var failure *Failure
	if !errors.As(err, &failure) {
		return crmError(http.StatusInternalServerError, "INTERNAL_ERROR", genericInternalMessage)
	}
	switch failure.Category {
	case FailureInvalid:
		return crmError(http.StatusBadRequest, "INVALID_QUERY", fallback(failure.Message, "Invalid query"))
	case FailureMalformed:
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", fallback(failure.Message, "Malformed query"))
	case FailureInvalidRequest:
		return crmErrorWithFields(http.StatusBadRequest, "INVALID_REQUEST", fallback(failure.Message, "Invalid request"), failure.Fields)
	case FailureInvalidReference:
		return crmErrorWithFields(http.StatusBadRequest, "INVALID_CROSS_REFERENCE_KEY", fallback(failure.Message, "Invalid reference"), failure.Fields)
	case FailureFieldIntegrity:
		return crmErrorWithFields(http.StatusBadRequest, "FIELD_INTEGRITY_EXCEPTION", fallback(failure.Message, "Field integrity error"), failure.Fields)
	case FailureInvalidOperation:
		return crmErrorWithFields(http.StatusBadRequest, "INVALID_OPERATION", fallback(failure.Message, "Invalid operation"), failure.Fields)
	case FailureInvalidField:
		return crmErrorWithFields(http.StatusBadRequest, "INVALID_FIELD", fallback(failure.Message, "Invalid field"), failure.Fields)
	case FailureRequestTooLarge:
		return crmErrorWithFields(http.StatusRequestEntityTooLarge, "INVALID_REQUEST", fallback(failure.Message, "Request too large"), failure.Fields)
	case FailureUnauthenticated:
		return crmError(http.StatusUnauthorized, "INVALID_SESSION_ID", fallback(failure.Message, "Session expired or invalid"))
	case FailureForbidden:
		return crmError(http.StatusForbidden, "FORBIDDEN", fallback(failure.Message, "Insufficient access"))
	case FailureMissing:
		return crmError(http.StatusNotFound, "NOT_FOUND", fallback(failure.Message, "Resource not found"))
	case FailureConflict:
		return crmError(http.StatusConflict, "CONFLICT", fallback(failure.Message, "Request conflict"))
	case FailureCanceled:
		return crmError(499, "UNKNOWN_EXCEPTION", "Request canceled")
	case FailureDeadline:
		return crmError(http.StatusGatewayTimeout, "UNKNOWN_EXCEPTION", "Request deadline exceeded")
	case FailureResponseTooLarge:
		return crmError(http.StatusBadRequest, "LIMIT_EXCEEDED", fallback(failure.Message, "Query response exceeds the supported size limit"))
	case FailureExhausted:
		return crmError(http.StatusTooManyRequests, "LIMIT_EXCEEDED", fallback(failure.Message, "Request limit exceeded"))
	case FailureUnavailable:
		return crmError(http.StatusServiceUnavailable, "UNKNOWN_EXCEPTION", "Service temporarily unavailable")
	default:
		return crmError(http.StatusInternalServerError, "INTERNAL_ERROR", genericInternalMessage)
	}
}

func fallback(value, defaultValue string) string {
	if strings.TrimSpace(value) == "" {
		return defaultValue
	}
	return value
}
