package resolver

import (
	"context"
	"errors"
	"time"

	"github.com/tech/sendico/billing/fees/storage"
	"github.com/tech/sendico/billing/fees/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.uber.org/zap"
)

type planFinder interface {
	FindActiveOrgPlan(ctx context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error)
	FindActiveGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error)
}

type feeResolver struct {
	plans  storage.PlansStore
	finder planFinder
	logger *zap.Logger
}

func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver {
	var finder planFinder
	if pf, ok := plans.(planFinder); ok {
		finder = pf
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	return &feeResolver{
		plans:  plans,
		finder: finder,
		logger: logger.Named("resolver"),
	}
}

func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *primitive.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
	if r.plans == nil {
		return nil, nil, merrors.InvalidArgument("fees: plans store is required")
	}

	// Try org-specific first if provided.
	if orgRef != nil && !orgRef.IsZero() {
		if plan, err := r.getOrgPlan(ctx, *orgRef, at); err == nil {
			if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil {
				return plan, rule, nil
			} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
				r.logger.Warn("failed selecting rule for org plan", zap.Error(selErr), zap.String("org_ref", orgRef.Hex()))
				return nil, nil, selErr
			}
			r.logger.Debug("no matching rule in org plan; falling back to global", zap.String("org_ref", orgRef.Hex()))
		} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
			r.logger.Warn("failed resolving org fee plan", zap.Error(err), zap.String("org_ref", orgRef.Hex()))
			return nil, nil, err
		}
	}

	plan, err := r.getGlobalPlan(ctx, at)
	if err != nil {
		if errors.Is(err, storage.ErrFeePlanNotFound) {
			return nil, nil, merrors.NoData("fees: no applicable fee rule found")
		}
		r.logger.Warn("failed resolving global fee plan", zap.Error(err))
		return nil, nil, err
	}

	rule, err := selectRule(plan, trigger, at, attrs)
	if err != nil {
		if !errors.Is(err, ErrNoFeeRuleFound) {
			r.logger.Warn("failed selecting rule in global plan", zap.Error(err))
		}
		return nil, nil, err
	}

	return plan, rule, nil
}

func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) {
	if r.finder != nil {
		return r.finder.FindActiveOrgPlan(ctx, orgRef, at)
	}
	return r.plans.GetActivePlan(ctx, orgRef, at)
}

func (r *feeResolver) getGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error) {
	if r.finder != nil {
		return r.finder.FindActiveGlobalPlan(ctx, at)
	}
	// Treat zero ObjectID as global in legacy path.
	return r.plans.GetActivePlan(ctx, primitive.NilObjectID, at)
}

func selectRule(plan *model.FeePlan, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeeRule, error) {
	if plan == nil {
		return nil, merrors.NoData("fees: no applicable fee rule found")
	}

	var selected *model.FeeRule
	var highestPriority int
	for _, rule := range plan.Rules {
		if rule.Trigger != trigger {
			continue
		}
		if rule.EffectiveFrom.After(at) {
			continue
		}
		if rule.EffectiveTo != nil && !rule.EffectiveTo.After(at) {
			continue
		}
		if !matchesAppliesTo(rule.AppliesTo, attrs) {
			continue
		}

		if selected == nil || rule.Priority > highestPriority {
			copy := rule
			selected = &copy
			highestPriority = rule.Priority
			continue
		}
		if rule.Priority == highestPriority {
			return nil, merrors.DataConflict("fees: conflicting fee rules")
		}
	}

	if selected == nil {
		return nil, merrors.NoData("fees: no applicable fee rule found")
	}
	return selected, nil
}

func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool {
	if len(appliesTo) == 0 {
		return true
	}
	for key, value := range appliesTo {
		if attrs == nil {
			return false
		}
		if attrs[key] != value {
			return false
		}
	}
	return true
}
