package sfcrmappservice

import (
	"context"
	"strings"
	"time"

	querycodec "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/querycodec"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
)

type queryDateMetadataKey struct{}
type runtimeQueryDates struct {
	query     RuntimeQueryFunc
	metadata  RuntimeGetAppMetadataViewV3Func
	reconcile RuntimeReconcileAppMetadataV3ReceiptsFunc
}

func (d *runtimeQueryDates) resolve(ctx context.Context, request *querycodec.QueryRequest, anchor *querycodec.DateAnchor) (context.Context, error) {
	view, specs, err := readObjectSpecs(ctx, d.metadata)
	if err != nil {
		return ctx, runtimeQueryFailure(err, false)
	}
	calendar := querycodec.DateContext{Now: time.Now(), FieldType: func(_ context.Context, object, field string) (string, error) {
		return dateFieldType(specs, object, field)
	}, ChildObject: func(_ context.Context, parent, relationship string) (string, error) {
		return dateChildObject(specs, parent, relationship)
	}}
	if anchor == nil {
		bearer, _ := ctx.Value(queryBearerContextKey{}).(string)
		if bearer == "" || d.query == nil {
			return ctx, unavailableQueryCalendar()
		}
		req := semantic.Request{ObjectID: "User", Selections: []semantic.Selection{{Kind: semantic.SelectionField, Field: "TimeZoneSidKey"}}, Limit: 1, MinCatalogRevision: pointerTo(view.Receipts.CatalogRevision), Filter: semantic.Predicate{Kind: semantic.PredicateComparison, Operator: semantic.ComparisonEqual, Left: semantic.Operand{Kind: semantic.OperandField, Field: "Id"}, Right: semantic.Operand{Kind: semantic.OperandTrustedContext, TrustedContext: semantic.TrustedContextPrincipalID}}}
		if err := semantic.ValidateRequest(ctx, req); err != nil {
			return ctx, runtimeQueryFailure(err, false)
		}
		response, err := d.query(ctx, bearer, req)
		if err != nil {
			return ctx, runtimeQueryFailure(err, false)
		}
		if err := semantic.ValidateResponse(response); err != nil {
			return ctx, runtimeQueryFailure(err, false)
		}
		if err := validateSearchMetadataAuthority(ctx, d.reconcile, view, response.Receipts); err != nil {
			return ctx, runtimeQueryFailure(err, false)
		}
		if len(response.Records) != 1 {
			return ctx, unavailableQueryCalendar()
		}
		var zone string
		for _, f := range response.Records[0].Fields {
			if strings.EqualFold(f.Name, "TimeZoneSidKey") && f.Value.String != nil && !f.Value.Null {
				zone = *f.Value.String
			}
		}
		if strings.TrimSpace(zone) == "" {
			return ctx, unavailableQueryCalendar()
		}
		calendar.Location, err = time.LoadLocation(zone)
		if err != nil {
			return ctx, unavailableQueryCalendar()
		}
	}
	if err := querycodec.ResolveRelativeDates(ctx, request, calendar, anchor); err != nil {
		return ctx, err
	}
	return context.WithValue(ctx, queryDateMetadataKey{}, view), nil
}

func unavailableQueryCalendar() error {
	return &querycodec.Failure{Category: querycodec.FailureUnavailable, Message: "Query calendar context is unavailable"}
}

func dateFieldType(specs map[string]searchObjectSpec, object, path string) (string, error) {
	spec, ok := specs[strings.ToLower(object)]
	if !ok {
		return "", invalidQuery()
	}
	parts := strings.Split(path, ".")
	if len(parts) == 2 {
		var target string
		for _, field := range spec.Fields {
			if strings.EqualFold(field.APIName, parts[0]) || strings.EqualFold(field.RelationshipName, parts[0]) {
				if field.IsPolymorphic || len(field.ReferenceTo) != 1 {
					return "", invalidQuery()
				}
				if target != "" {
					return "", invalidQuery()
				}
				target = field.ReferenceTo[0]
			}
		}
		if target == "" {
			return "", invalidQuery()
		}
		return dateFieldType(specs, target, parts[1])
	}
	if len(parts) != 1 {
		return "", invalidQuery()
	}
	field, ok := findSearchField(spec, path)
	if !ok {
		return "", invalidQuery()
	}
	if strings.EqualFold(field.Type, "formula") {
		return field.ReturnType, nil
	}
	return field.Type, nil
}

func dateChildObject(specs map[string]searchObjectSpec, parent, relationship string) (string, error) {
	var target string
	for _, spec := range specs {
		for _, field := range spec.Fields {
			if !strings.EqualFold(field.ChildRelationshipName, relationship) {
				continue
			}
			for _, reference := range field.ReferenceTo {
				if strings.EqualFold(reference, parent) {
					if target != "" {
						return "", invalidQuery()
					}
					target = spec.APIName
				}
			}
		}
	}
	if target == "" {
		return "", invalidQuery()
	}
	return target, nil
}

func (c *RuntimeQueryAdapter) dateMetadata(ctx context.Context) (catalogsemantic.AppMetadataViewV3, bool) {
	view, ok := ctx.Value(queryDateMetadataKey{}).(catalogsemantic.AppMetadataViewV3)
	return view, ok
}
