package domain

import (
	"fmt"
	"strings"
)

const (
	ProductFitBandStrong   = "strong"
	ProductFitBandPossible = "possible"
	ProductFitBandWeak     = "weak"
	ProductFitPain         = "pain"
	ProductFitScenario     = "scenario"
	ProductFitAudience     = "audience"
	ProductFitCapability   = "capability"
)

type ProductFitReason struct {
	Dimension        string `bson:"dimension" json:"dimension"`
	Score            int    `bson:"score" json:"score"`
	Reason           string `bson:"reason" json:"reason"`
	CandidateExcerpt string `bson:"candidate_excerpt,omitempty" json:"candidate_excerpt,omitempty"`
	ProductBasis     string `bson:"product_basis,omitempty" json:"product_basis,omitempty"`
}

type ProductMatch struct {
	BrandID              string             `bson:"brand_id" json:"brand_id"`
	ProductID            string             `bson:"product_id" json:"product_id"`
	BrandNameSnapshot    string             `bson:"brand_name_snapshot" json:"brand_name_snapshot"`
	ProductLabelSnapshot string             `bson:"product_label_snapshot" json:"product_label_snapshot"`
	BrandUpdatedAt       int64              `bson:"brand_updated_at" json:"brand_updated_at"`
	ProductUpdatedAt     int64              `bson:"product_updated_at" json:"product_updated_at"`
	ProductFitScore      int                `bson:"product_fit_score" json:"product_fit_score"`
	ProductFitBand       string             `bson:"product_fit_band" json:"product_fit_band"`
	Eligible             bool               `bson:"eligible" json:"eligible"`
	Excluded             bool               `bson:"excluded" json:"excluded"`
	ExcludeReason        string             `bson:"exclude_reason,omitempty" json:"exclude_reason,omitempty"`
	Reasons              []ProductFitReason `bson:"reasons" json:"reasons"`
	Risks                []string           `bson:"risks" json:"risks"`
	WatchIDs             []string           `bson:"watch_ids" json:"watch_ids"`
	MatchedTerms         []string           `bson:"matched_terms" json:"matched_terms"`
	MatchedAt            int64              `bson:"matched_at" json:"matched_at"`
}

var productFitDimensions = map[string]int{
	ProductFitPain: 35, ProductFitScenario: 25, ProductFitAudience: 20, ProductFitCapability: 20,
}

func ProductFitBandFromScore(score int) string {
	if score >= 75 {
		return ProductFitBandStrong
	}
	if score >= 45 {
		return ProductFitBandPossible
	}
	return ProductFitBandWeak
}

func IsProductFitBand(s string) bool {
	return s == ProductFitBandStrong || s == ProductFitBandPossible || s == ProductFitBandWeak
}

// ValidateForWrite enforces four dimensions, bounded scores, and evidence for
// every positive claim. Product name alone is therefore never enough.
func (m *ProductMatch) ValidateForWrite() error {
	if m == nil || strings.TrimSpace(m.BrandID) == "" || strings.TrimSpace(m.ProductID) == "" {
		return fmt.Errorf("%w: product match requires brand_id and product_id", ErrValidation)
	}
	if len(m.Reasons) != len(productFitDimensions) {
		return fmt.Errorf("%w: product match requires four fit reasons", ErrValidation)
	}
	seen := map[string]bool{}
	total, painOrScenario := 0, false
	for _, reason := range m.Reasons {
		max, ok := productFitDimensions[reason.Dimension]
		if !ok || seen[reason.Dimension] {
			return fmt.Errorf("%w: invalid or duplicate product fit dimension %q", ErrValidation, reason.Dimension)
		}
		if reason.Score < 0 || reason.Score > max {
			return fmt.Errorf("%w: product fit score for %s out of range", ErrValidation, reason.Dimension)
		}
		if strings.TrimSpace(reason.Reason) == "" {
			return fmt.Errorf("%w: product fit reason %s is empty", ErrValidation, reason.Dimension)
		}
		if reason.Score > 0 && (strings.TrimSpace(reason.CandidateExcerpt) == "" || strings.TrimSpace(reason.ProductBasis) == "") {
			return fmt.Errorf("%w: positive product fit reason %s requires excerpt and basis", ErrValidation, reason.Dimension)
		}
		if reason.Score > 0 && (reason.Dimension == ProductFitPain || reason.Dimension == ProductFitScenario) {
			painOrScenario = true
		}
		seen[reason.Dimension], total = true, total+reason.Score
	}
	if total != m.ProductFitScore || total > 100 {
		return fmt.Errorf("%w: product fit score does not equal reason total", ErrValidation)
	}
	if m.ProductFitBand == "" {
		m.ProductFitBand = ProductFitBandFromScore(total)
	}
	if !IsProductFitBand(m.ProductFitBand) || m.ProductFitBand != ProductFitBandFromScore(total) {
		return fmt.Errorf("%w: product fit band does not match score", ErrValidation)
	}
	if m.Excluded && strings.TrimSpace(m.ExcludeReason) == "" {
		return fmt.Errorf("%w: excluded product match requires exclude_reason", ErrValidation)
	}
	m.Eligible = ProductFitEligible(total, painOrScenario, m.Excluded)
	m.WatchIDs = uniqueStrings(m.WatchIDs)
	m.MatchedTerms = NormalizeMatchedTerms(m.MatchedTerms)
	if m.Risks == nil {
		m.Risks = []string{}
	}
	if m.WatchIDs == nil {
		m.WatchIDs = []string{}
	}
	if m.MatchedTerms == nil {
		m.MatchedTerms = []string{}
	}
	return nil
}

func uniqueStrings(in []string) []string {
	out := make([]string, 0, len(in))
	seen := map[string]bool{}
	for _, raw := range in {
		s := strings.TrimSpace(raw)
		if s != "" && !seen[s] {
			seen[s] = true
			out = append(out, s)
		}
	}
	return out
}

func CloneProductMatch(m *ProductMatch) *ProductMatch {
	if m == nil {
		return nil
	}
	cp := *m
	cp.Reasons = append([]ProductFitReason(nil), m.Reasons...)
	cp.Risks, cp.WatchIDs, cp.MatchedTerms = append([]string(nil), m.Risks...), append([]string(nil), m.WatchIDs...), append([]string(nil), m.MatchedTerms...)
	return &cp
}
