package resolver

import (
	"context"
	"errors"
	"strings"
	"time"

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

type planFinder interface {
	FindActiveOrgPlan(ctx context.Context, orgRef bson.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 *bson.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), mzap.ObjRef("org_ref", *orgRef))
				return nil, nil, selErr
			}
			orgFields := []zap.Field{
				mzap.ObjRef("org_ref", *orgRef),
				zap.String("trigger", string(trigger)),
				zap.Time("booked_at", at),
				zap.Any("attributes", attrs),
			}
			orgFields = append(orgFields, zapFieldsForPlan(plan)...)
			r.logger.Debug("No matching rule in org plan; falling back to global", orgFields...)
		} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
			r.logger.Warn("Failed resolving org fee plan", zap.Error(err), mzap.ObjRef("org_ref", *orgRef))
			return nil, nil, err
		}
	}

	plan, err := r.getGlobalPlan(ctx, at)
	if err != nil {
		if errors.Is(err, storage.ErrFeePlanNotFound) {
			r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)),
				zap.Time("booked_at", at), zap.Any("attributes", attrs),
			)
			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))
		} else {
			globalFields := []zap.Field{
				zap.String("trigger", string(trigger)),
				zap.Time("booked_at", at),
				zap.Any("attributes", attrs),
			}
			globalFields = append(globalFields, zapFieldsForPlan(plan)...)
			r.logger.Debug("No matching rule in global plan", globalFields...)
		}
		return nil, nil, err
	}

	selectedFields := []zap.Field{
		zap.String("trigger", string(trigger)),
		zap.Time("booked_at", at),
		zap.Any("attributes", attrs),
		zap.String("rule_id", rule.RuleID),
		zap.Int("rule_priority", rule.Priority),
		zap.Any("rule_applies_to", rule.AppliesTo),
		zap.Time("rule_effective_from", rule.EffectiveFrom),
	}
	if rule.EffectiveTo != nil {
		selectedFields = append(selectedFields, zap.Time("rule_effective_to", *rule.EffectiveTo))
	}
	if orgRef != nil && !orgRef.IsZero() {
		selectedFields = append(selectedFields, mzap.ObjRef("org_ref", *orgRef))
	}
	selectedFields = append(selectedFields, zapFieldsForPlan(plan)...)
	r.logger.Debug("Selected fee rule", selectedFields...)

	return plan, rule, nil
}

func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef bson.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, bson.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
		}
		attrValue, ok := attrs[key]
		if !ok {
			return false
		}
		if !matchesAppliesValue(value, attrValue) {
			return false
		}
	}
	return true
}

func matchesAppliesValue(expected, actual string) bool {
	trimmed := strings.TrimSpace(expected)
	if trimmed == "" {
		return actual == ""
	}

	values := strings.Split(trimmed, ",")
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if value == "*" || value == actual {
			return true
		}
	}
	return false
}

func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
	if plan == nil {
		return []zap.Field{zap.Bool("plan_present", false)}
	}
	fields := []zap.Field{
		zap.Bool("plan_present", true),
		zap.Bool("plan_active", plan.Active),
		zap.Time("plan_effective_from", plan.EffectiveFrom),
		zap.Int("plan_rules_count", len(plan.Rules)),
	}
	if plan.EffectiveTo != nil {
		fields = append(fields, zap.Time("plan_effective_to", *plan.EffectiveTo))
	} else {
		fields = append(fields, zap.Bool("plan_effective_to_set", false))
	}
	if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
		fields = append(fields, mzap.ObjRef("plan_org_ref", *plan.OrganizationRef))
	} else {
		fields = append(fields, zap.Bool("plan_org_ref_set", false))
	}
	if plan.GetID() != nil && !plan.GetID().IsZero() {
		fields = append(fields, mzap.StorableRef(plan))
	}
	return fields
}
