package mongo

import (
	"errors"
	"regexp"
	"strings"

	mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
)

var dupKeyFieldNamePattern = regexp.MustCompile(`(?:^|[,{]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:`)

// IsDuplicateKeyError reports whether err is a MongoDB E11000 duplicate key error.
func IsDuplicateKeyError(err error) bool {
	if err == nil {
		return false
	}

	if len(duplicateKeyMessages(err)) > 0 {
		return true
	}

	if hasStructuredWriteException(err) {
		return false
	}

	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "e11000") || strings.Contains(msg, "duplicate key")
}

// DuplicateKeyMatchesField reports whether a duplicate key error involves the given field
// by parsing WriteException messages (index name and dup key document keys), not broad
// substring matching on the full error text.
func DuplicateKeyMatchesField(err error, field string) bool {
	if err == nil || field == "" {
		return false
	}

	field = strings.ToLower(field)
	msgs := duplicateKeyMessages(err)
	if len(msgs) == 0 {
		if hasStructuredWriteException(err) {
			return false
		}
		if !IsDuplicateKeyError(err) {
			return false
		}
		msgs = []string{err.Error()}
	}
	for _, msg := range msgs {
		if duplicateKeyMessageMatchesField(msg, field) {
			return true
		}
	}
	return false
}

func duplicateKeyMessages(err error) []string {
	var msgs []string

	var writeException mongodriver.WriteException
	if errors.As(err, &writeException) {
		for _, writeErr := range writeException.WriteErrors {
			if writeErr.Code == 11000 {
				msgs = append(msgs, writeErr.Message)
			}
		}
	}

	var bulkWriteException mongodriver.BulkWriteException
	if errors.As(err, &bulkWriteException) {
		for _, writeErr := range bulkWriteException.WriteErrors {
			if writeErr.Code == 11000 {
				msgs = append(msgs, writeErr.Message)
			}
		}
	}

	return msgs
}

func hasStructuredWriteException(err error) bool {
	var writeException mongodriver.WriteException
	if errors.As(err, &writeException) {
		return true
	}

	var bulkWriteException mongodriver.BulkWriteException
	return errors.As(err, &bulkWriteException)
}

func duplicateKeyMessageMatchesField(message, field string) bool {
	message = strings.ToLower(message)

	if indexName := extractDuplicateIndexName(message); indexName != "" {
		if indexNameMatchesField(indexName, field) {
			return true
		}
	}

	for _, key := range extractDuplicateKeyFields(message) {
		if key == field {
			return true
		}
	}

	return false
}

func extractDuplicateIndexName(message string) string {
	const prefix = "index: "
	start := strings.Index(message, prefix)
	if start < 0 {
		return ""
	}

	rest := strings.TrimSpace(message[start+len(prefix):])
	if rest == "" {
		return ""
	}

	end := strings.IndexByte(rest, ' ')
	if end >= 0 {
		return rest[:end]
	}

	return rest
}

func indexNameMatchesField(indexName, field string) bool {
	switch indexName {
	case field, field + "_idx", field + "_1":
		return true
	default:
		return false
	}
}

func extractDuplicateKeyFields(message string) []string {
	const prefix = "dup key:"
	start := strings.Index(message, prefix)
	if start < 0 {
		return nil
	}

	document := strings.TrimSpace(message[start+len(prefix):])
	if !strings.HasPrefix(document, "{") {
		return nil
	}

	depth := 0
	end := -1
	for i, char := range document {
		switch char {
		case '{':
			depth++
		case '}':
			depth--
			if depth == 0 {
				end = i
			}
		}
		if end >= 0 {
			break
		}
	}
	if end <= 0 {
		return nil
	}

	inner := strings.TrimSpace(document[1:end])
	matches := dupKeyFieldNamePattern.FindAllStringSubmatch(inner, -1)
	if len(matches) == 0 {
		return nil
	}

	fields := make([]string, 0, len(matches))
	seen := make(map[string]struct{}, len(matches))
	for _, match := range matches {
		if len(match) < 2 {
			continue
		}
		key := strings.ToLower(match[1])
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		fields = append(fields, key)
	}

	return fields
}
