package querycodec

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// RelativeDateValue preserves unquoted product syntax until metadata and the
// authenticated user's calendar context are available at the L1 boundary.
type RelativeDateValue string

type DateAnchor struct {
	Instant  time.Time `json:"instant"`
	TimeZone string    `json:"timeZone"`
}

type DateContext struct {
	Now         time.Time
	Location    *time.Location
	FieldType   func(context.Context, string, string) (string, error)
	ChildObject func(context.Context, string, string) (string, error)
}

func relativeDateDays(value string) (int, error) {
	if value == "TODAY" || value == "THIS_YEAR" {
		return 0, nil
	}
	raw, ok := strings.CutPrefix(value, "LAST_N_DAYS:")
	n, err := strconv.Atoi(raw)
	// Date scalars have four-digit years. Bound arithmetic before AddDate rather
	// than allowing an integer overflow to silently change the requested range.
	if !ok || err != nil || n < 0 || n > 3652058 {
		return 0, newValidationError("SOQL", "Invalid LAST_N_DAYS date literal")
	}
	return n, nil
}

func hasRelativeOperand(o Operand) bool {
	if _, ok := o.Value.(RelativeDateValue); ok {
		return true
	}
	for _, a := range o.Args {
		if hasRelativeOperand(a) {
			return true
		}
	}
	for _, a := range o.Elements {
		if hasRelativeOperand(a) {
			return true
		}
	}
	return false
}
func hasRelativePredicate(p Predicate) bool {
	if _, ok := p.Value.(RelativeDateValue); ok {
		return true
	}
	if hasRelativeOperand(p.Left) || hasRelativeOperand(p.Right) || hasRelativeOperand(p.Operand) {
		return true
	}
	for _, c := range p.Children {
		if hasRelativePredicate(c) {
			return true
		}
	}
	return p.CorrelatedMembership != nil && hasRelativePredicate(p.CorrelatedMembership.InnerFilter)
}
func hasRelativeDates(q *QueryRequest) bool {
	if hasRelativePredicate(q.Filter) || hasRelativePredicate(q.HavingPredicate) {
		return true
	}
	for _, child := range q.SubQueries {
		if hasRelativePredicate(child.Filter) {
			return true
		}
	}
	for _, join := range q.SemiJoins {
		if join.InnerQuery != nil && hasRelativeDates(join.InnerQuery) {
			return true
		}
	}
	return false
}

func (h *Handler) resolveDates(ctx context.Context, q *QueryRequest, anchor *DateAnchor) (context.Context, error) {
	if !hasRelativeDates(q) {
		return ctx, nil
	}
	if h.executor.Dates == nil {
		return ctx, &Failure{Category: FailureUnavailable, Message: "Query calendar context is unavailable"}
	}
	return h.executor.Dates(ctx, q, anchor)
}

// dateAnchorLocation validates the complete continuation calendar before Go's
// empty-name UTC default can turn missing input into an execution decision.
func dateAnchorLocation(anchor *DateAnchor) (*time.Location, error) {
	if anchor == nil || anchor.Instant.IsZero() || anchor.Instant.Year() < 1 || anchor.Instant.Year() > 9999 ||
		strings.TrimSpace(anchor.TimeZone) == "" || anchor.TimeZone == "Local" {
		return nil, fmt.Errorf("invalid query date anchor")
	}
	location, err := time.LoadLocation(anchor.TimeZone)
	if err != nil {
		return nil, err
	}
	if year := anchor.Instant.In(location).Year(); year < 1 || year > 9999 {
		return nil, fmt.Errorf("query date anchor is outside the supported calendar")
	}
	return location, nil
}

// ResolveRelativeDates freezes one user-calendar anchor and lowers product
// literals to ordinary typed comparisons before any executor sees the query.
func ResolveRelativeDates(ctx context.Context, q *QueryRequest, calendar DateContext, anchor *DateAnchor) error {
	var err error
	if anchor != nil {
		calendar.Now = anchor.Instant
		calendar.Location, err = dateAnchorLocation(anchor)
	}
	if err != nil || calendar.Location == nil || calendar.Location.String() == "Local" || calendar.Now.IsZero() || calendar.FieldType == nil {
		return &Failure{Category: FailureUnavailable, Message: "Query calendar context is unavailable"}
	}
	q.DateAnchor = &DateAnchor{Instant: calendar.Now, TimeZone: calendar.Location.String()}
	return resolveQueryDates(ctx, q, calendar)
}

func resolveQueryDates(ctx context.Context, q *QueryRequest, c DateContext) (err error) {
	q.Filter, err = resolveDatePredicate(ctx, q.ObjectAPIName, q.Filter, c)
	if err != nil {
		return err
	}
	q.HavingPredicate, err = resolveDatePredicate(ctx, q.ObjectAPIName, q.HavingPredicate, c)
	if err != nil {
		return err
	}
	for i := range q.SubQueries {
		child := &q.SubQueries[i]
		if !hasRelativePredicate(child.Filter) {
			continue
		}
		if c.ChildObject == nil {
			return dateFailure("Cannot resolve child relationship for date literal")
		}
		object, e := c.ChildObject(ctx, q.ObjectAPIName, child.RelationshipName)
		if e != nil {
			return e
		}
		child.Filter, err = resolveDatePredicate(ctx, object, child.Filter, c)
		if err != nil {
			return err
		}
	}
	for i := range q.SemiJoins {
		if q.SemiJoins[i].InnerQuery != nil {
			if err = resolveQueryDates(ctx, q.SemiJoins[i].InnerQuery, c); err != nil {
				return err
			}
		}
	}
	return nil
}

func dateFailure(message string) error { return &Failure{Category: FailureMalformed, Message: message} }

func resolveDatePredicate(ctx context.Context, object string, p Predicate, c DateContext) (Predicate, error) {
	for i := range p.Children {
		child, err := resolveDatePredicate(ctx, object, p.Children[i], c)
		if err != nil {
			return Predicate{}, err
		}
		p.Children[i] = child
	}
	if !hasRelativePredicate(p) {
		return p, nil
	}
	value, ok := p.Right.Value.(RelativeDateValue)
	if p.Kind != PredicateComparison || p.Left.Kind != OperandField || !ok || hasRelativeOperand(p.Left) {
		return Predicate{}, dateFailure("A date literal must be compared with a Date or DateTime field")
	}
	typ, err := c.FieldType(ctx, object, p.Left.Field)
	if err != nil {
		return Predicate{}, err
	}
	isDate := strings.EqualFold(typ, "date")
	if !isDate && !strings.EqualFold(typ, "datetime") {
		return Predicate{}, dateFailure("A date literal requires a Date or DateTime field")
	}
	now := c.Now.In(c.Location)
	start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, c.Location)
	end := start.AddDate(0, 0, 1)
	inclusive := false
	switch string(value) {
	case "TODAY":
	case "THIS_YEAR":
		start = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, c.Location)
		end = start.AddDate(1, 0, 0)
	default:
		days, e := relativeDateDays(string(value))
		if e != nil {
			return Predicate{}, e
		}
		start = start.AddDate(0, 0, -days)
		if !isDate {
			end, inclusive = now, true
		}
	}
	if start.Year() < 1 || end.Year() > 9999 {
		return Predicate{}, dateFailure("Date literal is outside the supported calendar range")
	}
	scalar := func(t time.Time) Operand {
		if isDate {
			return LiteralOperand(DateLiteralValue(t.Format("2006-01-02")))
		}
		return LiteralOperand(DateTimeLiteralValue(t.UTC().Format(time.RFC3339Nano)))
	}
	compare := func(op string, boundary time.Time) Predicate {
		return ComparisonPredicate(p.Left, op, scalar(boundary))
	}
	upper, after := "<", ">="
	if inclusive {
		upper, after = "<=", ">"
	}
	switch p.Op {
	case "==", "=":
		return And(compare(">=", start), compare(upper, end)), nil
	case "!=", "<>":
		return Or(compare("<", start), compare(after, end)), nil
	case ">":
		return compare(after, end), nil
	case ">=":
		return compare(">=", start), nil
	case "<":
		return compare("<", start), nil
	case "<=":
		return compare(upper, end), nil
	default:
		return Predicate{}, dateFailure(fmt.Sprintf("Operator %s does not support a date literal", p.Op))
	}
}

// RequiresDateContext lets retained parse-only adapters reject unresolved
// calendar syntax before conversion to an execution model without that type.
func RequiresDateContext(q *QueryRequest) bool      { return q != nil && hasRelativeDates(q) }
func PredicateRequiresDateContext(p Predicate) bool { return hasRelativePredicate(p) }
