package discovery

import (
	"context"
	"fmt"
	"strings"
)

const (
	DefaultOtherGroupsPageSize = 12
	MaxOtherGroupsPageSize     = 50
)

type OtherProfileGroupsPage struct {
	Groups  []ProfileGroup `json:"groups"`
	Total   int            `json:"total"`
	Limit   int            `json:"limit"`
	Offset  int            `json:"offset"`
	HasMore bool           `json:"has_more"`
}

const otherProfilesBaseWhere = `
	FROM contact_discovered_profiles p
	JOIN users u ON p.user_id = u.id
	WHERE u.external_id = $1
	  AND p.status = 'suggested'
	  AND NOT p.is_mailing_list
	  AND NOT p.is_disposable
	  AND NOT p.is_spam_heavy` + noReplyProfilesSQL + suggestableProfilesSQL

func otherProfileTokenMatchesParam(param string) string {
	return fmt.Sprintf(`(
		position(%[1]s in lower(p.display_name)) > 0
		OR position(%[1]s in lower(p.primary_email)) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'first_name', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'last_name', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'company', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'department', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'job_title', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'website', ''))) > 0
		OR position(%[1]s in lower(COALESCE(p.enriched_data->>'notes', ''))) > 0
		OR EXISTS (
			SELECT 1
			FROM jsonb_array_elements(COALESCE(p.all_emails, '[]'::jsonb)) AS e
			WHERE position(%[1]s in lower(COALESCE(e->>'email', ''))) > 0
			   OR position(%[1]s in lower(COALESCE(e->>'display_name', ''))) > 0
		)
		OR EXISTS (
			SELECT 1
			FROM jsonb_array_elements(COALESCE(p.enriched_data->'emails', '[]'::jsonb)) AS ee
			WHERE position(%[1]s in lower(COALESCE(ee->>'value', ''))) > 0
		)
		OR EXISTS (
			SELECT 1
			FROM jsonb_array_elements(COALESCE(p.enriched_data->'phones', '[]'::jsonb)) AS ph
			WHERE position(%[1]s in lower(COALESCE(ph->>'value', ''))) > 0
		)
		OR EXISTS (
			SELECT 1
			FROM jsonb_array_elements(COALESCE(p.detected_in_accounts, '[]'::jsonb)) AS a
			WHERE position(%[1]s in lower(COALESCE(a->>'account_email', ''))) > 0
			   OR position(%[1]s in lower(COALESCE(a->>'account_name', ''))) > 0
		)
	)`, param)
}

func otherProfileSearchClause(search string, paramIndex int) (clause string, terms []string) {
	tokens := discoveryQueryTokens(search)
	if len(tokens) == 0 {
		return "", nil
	}
	parts := make([]string, 0, len(tokens))
	for i, token := range tokens {
		parts = append(parts, otherProfileTokenMatchesParam(fmt.Sprintf("$%d", paramIndex+i)))
		terms = append(terms, token)
	}
	return " AND " + strings.Join(parts, " AND "), terms
}

func (s *Service) countOtherProfileGroups(ctx context.Context, externalUserID string, search string) (int, error) {
	searchClause, searchTerms := otherProfileSearchClause(search, 2)
	args := []any{externalUserID}
	for _, term := range searchTerms {
		args = append(args, term)
	}
	var total int
	err := s.db.QueryRow(ctx, `
		SELECT COUNT(*)::int FROM (
			SELECT DISTINCT COALESCE(p.person_group_id, p.id) AS grp_id
			`+otherProfilesBaseWhere+searchClause+`
		) g
	`, args...).Scan(&total)
	return total, err
}

func (s *Service) listOtherProfileGroupIDs(ctx context.Context, externalUserID string, limit, offset int, search string) ([]string, error) {
	searchClause, searchTerms := otherProfileSearchClause(search, 2)
	args := []any{externalUserID}
	limitIdx := 2
	offsetIdx := 3
	for _, term := range searchTerms {
		args = append(args, term)
		limitIdx++
		offsetIdx++
	}
	args = append(args, limit, offset)
	rows, err := s.db.Query(ctx, fmt.Sprintf(`
		SELECT grp_id::text FROM (
			SELECT COALESCE(p.person_group_id, p.id) AS grp_id,
			       SUM(p.outbound_count) AS total_outbound,
			       SUM(p.inbound_from_cc_count) AS total_inbound,
			       SUM(p.copresence_cc_bcc_count) AS total_copresence,
			       SUM(p.message_count) AS total_messages,
			       MAX(p.last_message_at) AS last_message_at
			`+otherProfilesBaseWhere+searchClause+`
			GROUP BY COALESCE(p.person_group_id, p.id)
		) ranked
		ORDER BY `+groupInteractionOrderBy+`
		LIMIT $%d OFFSET $%d
	`, limitIdx, offsetIdx), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ids []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	return ids, rows.Err()
}

func (s *Service) listAllOtherProfileGroupIDs(ctx context.Context, externalUserID string, search string) ([]string, error) {
	searchClause, searchTerms := otherProfileSearchClause(search, 2)
	args := []any{externalUserID}
	for _, term := range searchTerms {
		args = append(args, term)
	}
	rows, err := s.db.Query(ctx, `
		SELECT DISTINCT COALESCE(p.person_group_id, p.id)::text AS grp_id
		`+otherProfilesBaseWhere+searchClause+`
	`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ids []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	return ids, rows.Err()
}

func (s *Service) listOtherProfilesForGroupIDs(ctx context.Context, externalUserID string, groupIDs []string) ([]Profile, error) {
	if len(groupIDs) == 0 {
		return nil, nil
	}
	rows, err := s.db.Query(ctx, `
		SELECT `+profileSelectColumns+`
		`+otherProfilesBaseWhere+`
		  AND COALESCE(p.person_group_id, p.id) = ANY($2::uuid[])
		ORDER BY `+profileInteractionOrderBy+`
	`, externalUserID, groupIDs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var profiles []Profile
	for rows.Next() {
		p, err := scanProfileRow(rows)
		if err != nil {
			return nil, err
		}
		profiles = append(profiles, p)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	s.attachSignaturesToProfiles(ctx, profiles)
	return profiles, nil
}

func (s *Service) attachSignaturesToProfiles(ctx context.Context, profiles []Profile) {
	if len(profiles) == 0 {
		return
	}
	ids := make([]string, 0, len(profiles))
	for _, p := range profiles {
		ids = append(ids, p.ID)
	}
	batch, err := s.loadProfileSignaturesBatch(ctx, ids)
	if err != nil {
		return
	}
	for i := range profiles {
		profiles[i].Signatures = batch[profiles[i].ID]
	}
}

func (s *Service) loadProfileSignaturesBatch(ctx context.Context, profileIDs []string) (map[string][]SignatureEntry, error) {
	out := make(map[string][]SignatureEntry, len(profileIDs))
	if len(profileIDs) == 0 {
		return out, nil
	}
	rows, err := s.db.Query(ctx, `
		SELECT profile_id::text, id::text, COALESCE(message_id::text, ''), signature_text, message_date, confidence
		FROM contact_discovered_signatures
		WHERE profile_id = ANY($1::uuid[])
		ORDER BY profile_id, message_date DESC
	`, profileIDs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var profileID string
		var se SignatureEntry
		if err := rows.Scan(&profileID, &se.ID, &se.MessageID, &se.SignatureText, &se.MessageDate, &se.Confidence); err != nil {
			return nil, err
		}
		list := out[profileID]
		if len(list) < 5 {
			out[profileID] = append(list, se)
		}
	}
	return out, rows.Err()
}

func (s *Service) ListOtherProfileGroupsPage(ctx context.Context, externalUserID string, limit, offset int, search string) (OtherProfileGroupsPage, error) {
	if limit <= 0 {
		limit = DefaultOtherGroupsPageSize
	}
	if limit > MaxOtherGroupsPageSize {
		limit = MaxOtherGroupsPageSize
	}
	if offset < 0 {
		offset = 0
	}
	search = strings.TrimSpace(search)

	if search != "" {
		return s.listOtherProfileGroupsSearchPage(ctx, externalUserID, limit, offset, search)
	}

	total, err := s.countOtherProfileGroups(ctx, externalUserID, search)
	if err != nil {
		return OtherProfileGroupsPage{}, err
	}
	if total == 0 || offset >= total {
		return OtherProfileGroupsPage{
			Groups:  []ProfileGroup{},
			Total:   total,
			Limit:   limit,
			Offset:  offset,
			HasMore: false,
		}, nil
	}

	groupIDs, err := s.listOtherProfileGroupIDs(ctx, externalUserID, limit, offset, search)
	if err != nil {
		return OtherProfileGroupsPage{}, err
	}
	groups, err := s.loadOtherProfileGroupsByIDs(ctx, externalUserID, groupIDs)
	if err != nil {
		return OtherProfileGroupsPage{}, err
	}

	nextOffset := offset + len(groups)
	hasMore := nextOffset < total

	return OtherProfileGroupsPage{
		Groups:  groups,
		Total:   total,
		Limit:   limit,
		Offset:  offset,
		HasMore: hasMore,
	}, nil
}

func (s *Service) listOtherProfileGroupsSearchPage(ctx context.Context, externalUserID string, limit, offset int, search string) (OtherProfileGroupsPage, error) {
	groupIDs, err := s.listAllOtherProfileGroupIDs(ctx, externalUserID, search)
	if err != nil {
		return OtherProfileGroupsPage{}, err
	}
	if len(groupIDs) == 0 {
		return OtherProfileGroupsPage{
			Groups:  []ProfileGroup{},
			Total:   0,
			Limit:   limit,
			Offset:  offset,
			HasMore: false,
		}, nil
	}

	groups, err := s.loadOtherProfileGroupsByIDs(ctx, externalUserID, groupIDs)
	if err != nil {
		return OtherProfileGroupsPage{}, err
	}
	ranked := rankProfileGroupsByQuery(groups, search)
	total := len(ranked)
	if offset >= total {
		return OtherProfileGroupsPage{
			Groups:  []ProfileGroup{},
			Total:   total,
			Limit:   limit,
			Offset:  offset,
			HasMore: false,
		}, nil
	}

	end := offset + limit
	if end > total {
		end = total
	}
	page := ranked[offset:end]

	return OtherProfileGroupsPage{
		Groups:  page,
		Total:   total,
		Limit:   limit,
		Offset:  offset,
		HasMore: end < total,
	}, nil
}

func (s *Service) loadOtherProfileGroupsByIDs(ctx context.Context, externalUserID string, groupIDs []string) ([]ProfileGroup, error) {
	profiles, err := s.listOtherProfilesForGroupIDs(ctx, externalUserID, groupIDs)
	if err != nil {
		return nil, err
	}
	groups := filterSuggestableGroups(groupProfiles(profiles))
	if groups == nil {
		return []ProfileGroup{}, nil
	}
	return groups, nil
}
