package threading

import (
	"context"
	"errors"
	"regexp"
	"strings"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

var messageIDToken = regexp.MustCompile(`<[^>]+>`)

// NormalizeMessageID returns a canonical angle-bracket Message-ID when possible.
func NormalizeMessageID(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	if strings.HasPrefix(raw, "<") && strings.HasSuffix(raw, ">") {
		return raw
	}
	return "<" + strings.Trim(raw, "<>") + ">"
}

// ParseMessageIDs extracts Message-IDs from a References or In-Reply-To header value.
func ParseMessageIDs(header string) []string {
	header = strings.TrimSpace(header)
	if header == "" {
		return nil
	}
	matches := messageIDToken.FindAllString(header, -1)
	if len(matches) == 0 {
		if id := NormalizeMessageID(header); id != "" {
			return []string{id}
		}
		return nil
	}
	seen := make(map[string]struct{}, len(matches))
	out := make([]string, 0, len(matches))
	for _, m := range matches {
		id := NormalizeMessageID(m)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}

// BuildReferences returns the References chain for a reply (ancestors + parent).
func BuildReferences(parentMessageID string, parentReferences []string) []string {
	parentMessageID = NormalizeMessageID(parentMessageID)
	if parentMessageID == "" {
		return nil
	}
	seen := make(map[string]struct{}, len(parentReferences)+1)
	out := make([]string, 0, len(parentReferences)+1)
	for _, ref := range parentReferences {
		id := NormalizeMessageID(ref)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	if _, ok := seen[parentMessageID]; !ok {
		out = append(out, parentMessageID)
	}
	return out
}

func candidateMessageIDs(inReplyTo string, references []string) []string {
	seen := make(map[string]struct{})
	var out []string
	add := func(id string) {
		id = NormalizeMessageID(id)
		if id == "" {
			return
		}
		if _, ok := seen[id]; ok {
			return
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	add(inReplyTo)
	for _, ref := range references {
		add(ref)
	}
	return out
}

// AssignThreadID picks an existing thread for the account or allocates a new one.
func AssignThreadID(ctx context.Context, db *pgxpool.Pool, accountID, inReplyTo string, references []string) (string, error) {
	ids := candidateMessageIDs(inReplyTo, references)
	if len(ids) > 0 {
		var threadID *uuid.UUID
		err := db.QueryRow(ctx, `
			SELECT thread_id FROM messages
			WHERE account_id = $1 AND message_id = ANY($2) AND thread_id IS NOT NULL
			ORDER BY date ASC
			LIMIT 1
		`, accountID, ids).Scan(&threadID)
		if err == nil && threadID != nil {
			return threadID.String(), nil
		}
		if err != nil && !errors.Is(err, pgx.ErrNoRows) {
			return "", err
		}
	}
	return uuid.New().String(), nil
}
