package usecase

import (
	"net/url"
	"strings"
	"unicode/utf8"

	"apps/backend/internal/module/radar/domain"
)

const (
	PrefilterPass     = "pass"
	PrefilterReview   = "review"
	PrefilterRejected = "rejected"
)

type PrefilterStats struct {
	Deduped, Pass, Review, Rejected int
}

// NormalizeCandidate removes transport noise before any score or meter is
// touched. It also establishes one stable external identity for a URL hit.
func NormalizeCandidate(c *domain.CandidatePost) *domain.CandidatePost {
	if c == nil {
		return nil
	}
	out := *c
	out.ExternalID = canonicalCandidateURL(out.ExternalID)
	out.Permalink = canonicalCandidateURL(out.Permalink)
	if out.ExternalID == "" {
		out.ExternalID = out.Permalink
	}
	if out.Permalink == "" {
		out.Permalink = out.ExternalID
	}
	out.Text = strings.TrimSpace(strings.Join(strings.Fields(out.Text), " "))
	out.Title = strings.TrimSpace(strings.Join(strings.Fields(out.Title), " "))
	out.AuthorHandle = strings.TrimPrefix(strings.TrimSpace(out.AuthorHandle), "@")
	out.Classification = strings.ToLower(strings.TrimSpace(out.Classification))
	if out.Classification == "" {
		out.Classification = classifyCandidate(strings.ToLower(out.Text + " " + out.Title))
	}
	return &out
}

func canonicalCandidateURL(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	u, err := url.Parse(raw)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return raw
	}
	u.Scheme = strings.ToLower(u.Scheme)
	u.Host = strings.ToLower(u.Host)
	u.Fragment = ""
	u.RawQuery = ""
	return strings.TrimRight(u.String(), "/")
}

// PrefilterCandidates is cheap, deterministic and deliberately conservative:
// only obvious provider/noise/announcement posts are rejected. A weak demand
// signal is retained as review so the judge can still rescue an unusual but
// relevant user wording.
func PrefilterCandidates(in []*domain.CandidatePost, watch *domain.RadarWatch, product *ProductContextSnapshot) ([]*domain.CandidatePost, PrefilterStats) {
	terms := []string{}
	if watch != nil {
		terms = append(terms, watch.Terms...)
	}
	if product != nil {
		terms = append(terms, product.PainPoints...)
		terms = append(terms, product.ProductContext)
		terms = append(terms, product.MatchTags...)
		terms = append(terms, product.ProviderCapabilityTerms...)
	}
	seen := map[string]bool{}
	out := make([]*domain.CandidatePost, 0, len(in))
	stats := PrefilterStats{}
	for _, raw := range in {
		c := NormalizeCandidate(raw)
		if c == nil || c.ExternalID == "" || c.Permalink == "" || utf8.RuneCountInString(c.Text) < 2 {
			stats.Rejected++
			continue
		}
		if seen[c.ExternalID] {
			stats.Deduped++
			continue
		}
		seen[c.ExternalID] = true
		if c.Classification == "provider_offer" || c.Classification == "announcement" || c.Classification == "noise" {
			stats.Rejected++
			// Keep hard-reject candidates in the stream so the existing judge
			// path can persist an auditable rejected opportunity without an AI
			// call. This preserves the inbox's explanation surface.
			out = append(out, c)
			continue
		}
		blob := strings.ToLower(c.Text + " " + c.Title)
		matched := false
		for _, term := range terms {
			term = strings.ToLower(strings.TrimSpace(term))
			if term != "" && strings.Contains(blob, term) {
				matched = true
				break
			}
		}
		if matched || c.Classification == "seeking_help" || c.Classification == "seeking_recommendation" {
			stats.Pass++
		} else {
			stats.Review++
		}
		out = append(out, c)
	}
	return out, stats
}
