package migration

import (
	"strings"

	"github.com/ultisuite/ulti-backend/internal/auth"
)

// ClaimIdentity holds SSO identity fields checked against a migration invite.
type ClaimIdentity struct {
	Email             string
	PreferredUsername string
	UPN               string
	TenantID          string
}

func ClaimIdentityFromAuth(c *auth.Claims) ClaimIdentity {
	if c == nil {
		return ClaimIdentity{}
	}
	return ClaimIdentity{
		Email:             c.Email,
		PreferredUsername: c.PreferredUsername,
		UPN:               c.UPN,
		TenantID:          c.TID,
	}
}

func normalizeInviteEmail(email string) string {
	return strings.ToLower(strings.TrimSpace(email))
}

func isEmailAddress(s string) bool {
	s = strings.TrimSpace(s)
	at := strings.LastIndex(s, "@")
	return at > 0 && at < len(s)-1
}

func identityCandidateEmails(id ClaimIdentity) []string {
	seen := make(map[string]struct{})
	var out []string
	for _, raw := range []string{id.Email, id.PreferredUsername, id.UPN} {
		raw = strings.TrimSpace(raw)
		if raw == "" || !isEmailAddress(raw) {
			continue
		}
		norm := normalizeInviteEmail(raw)
		if _, ok := seen[norm]; ok {
			continue
		}
		seen[norm] = struct{}{}
		out = append(out, norm)
	}
	return out
}

func normalizeEmailLocalPart(local string) string {
	local = strings.ToLower(strings.TrimSpace(local))
	if plus := strings.Index(local, "+"); plus >= 0 {
		local = local[:plus]
	}
	return strings.ReplaceAll(local, ".", "")
}

func emailLocalAndDomain(email string) (local, domain string, ok bool) {
	email = normalizeInviteEmail(email)
	at := strings.LastIndex(email, "@")
	if at <= 0 || at == len(email)-1 {
		return "", "", false
	}
	return email[:at], email[at+1:], true
}

func inviteMatchTargets(inviteEmail string, alternateEmails []string) []string {
	seen := make(map[string]struct{})
	var out []string
	add := func(e string) {
		e = normalizeInviteEmail(e)
		if e == "" || !isEmailAddress(e) {
			return
		}
		if _, ok := seen[e]; ok {
			return
		}
		seen[e] = struct{}{}
		out = append(out, e)
	}
	add(inviteEmail)
	for _, alt := range alternateEmails {
		add(alt)
	}
	return out
}

func isGmailAliasDomain(domain string) bool {
	switch strings.ToLower(strings.TrimSpace(domain)) {
	case "gmail.com", "googlemail.com":
		return true
	default:
		return false
	}
}

// gmailLocalPartAliasMatch applies Gmail dot/plus normalization only on gmail.com / googlemail.com.
func gmailLocalPartAliasMatch(a, b string) bool {
	aLocal, aDomain, okA := emailLocalAndDomain(a)
	bLocal, bDomain, okB := emailLocalAndDomain(b)
	if !okA || !okB {
		return false
	}
	if !strings.EqualFold(aDomain, bDomain) {
		return false
	}
	if !isGmailAliasDomain(aDomain) {
		return false
	}
	return normalizeEmailLocalPart(aLocal) == normalizeEmailLocalPart(bLocal)
}

// projectDomainUPNMatch accepts claims when the invite is on the hosted project domain
// and a UPN-style identity (preferred_username / upn) shares the same mailbox local-part.
// Typical Microsoft case: invite alice@acme.com, SSO preferred_username alice@tenant.onmicrosoft.com.
func projectDomainUPNMatch(inviteEmail, projectDomain string, identity ClaimIdentity) bool {
	if projectDomain == "" {
		return false
	}
	projectDomain = strings.ToLower(strings.TrimSpace(projectDomain))
	invLocal, invDomain, ok := emailLocalAndDomain(inviteEmail)
	if !ok || !strings.EqualFold(invDomain, projectDomain) {
		return false
	}
	for _, raw := range []string{identity.PreferredUsername, identity.UPN} {
		candLocal, _, ok := emailLocalAndDomain(raw)
		if ok && strings.EqualFold(candLocal, invLocal) {
			return true
		}
	}
	return false
}

// InviteEmailMatchesIdentity reports whether SSO identity may claim the invite.
// projectDomain is the hosted mail domain when the migration project is domain-bound.
func InviteEmailMatchesIdentity(inviteEmail string, alternateEmails []string, projectDomain string, identity ClaimIdentity) bool {
	targets := inviteMatchTargets(inviteEmail, alternateEmails)
	if len(targets) == 0 {
		return false
	}
	candidates := identityCandidateEmails(identity)
	if len(candidates) == 0 {
		return false
	}

	for _, target := range targets {
		for _, candidate := range candidates {
			if candidate == target {
				return true
			}
			if gmailLocalPartAliasMatch(target, candidate) {
				return true
			}
		}
	}

	return projectDomainUPNMatch(inviteEmail, projectDomain, identity)
}

// validateMicrosoftTenantClaim rejects claims when the OIDC tid does not match the project's pinned tenant.
func validateMicrosoftTenantClaim(proj Project, tokenTenantID string) error {
	if strings.ToLower(strings.TrimSpace(proj.SourceProvider)) != "microsoft" {
		return nil
	}
	expected := strings.TrimSpace(proj.MicrosoftTenantID)
	if expected == "" {
		return nil
	}
	got := strings.TrimSpace(tokenTenantID)
	if got == "" || !strings.EqualFold(got, expected) {
		return ErrTenantMismatch
	}
	return nil
}
