package identity

import (
	"context"
	"errors"
	"fmt"
	"net/mail"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/passwords"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

// OrganizationDirectoryUserParams describes one provider-owned human identity
// that must be materialized into the core user table before organization
// projection.
type OrganizationDirectoryUserParams struct {
	Email            string
	DisplayName      string
	Provider         string
	ProviderTenantID string
	ExternalID       string
	EmailVerified    bool
}

// EnsureOrganizationDirectoryUser materializes or reuses one local user for an
// externally managed organization directory person without creating a usable
// password.
func (store *Store) EnsureOrganizationDirectoryUser(ctx context.Context, params OrganizationDirectoryUserParams) (model.User, bool, error) {
	initialized, err := store.SystemInitialized(ctx)
	if err != nil {
		return model.User{}, false, err
	}
	if !initialized {
		return model.User{}, false, ErrSystemNotInitialized
	}

	resolvedEmail, hasRealEmail := resolveOrganizationDirectoryUserEmail(params)
	user, err := store.FindUserByEmail(ctx, resolvedEmail)
	if err == nil {
		return user, false, nil
	}
	if !errors.Is(err, ErrNotFound) {
		return model.User{}, false, err
	}

	resolvedDisplayName := strings.TrimSpace(params.DisplayName)
	if resolvedDisplayName == "" {
		resolvedDisplayName = organizationDirectoryDisplayNameFromEmail(resolvedEmail)
	}
	passwordHash, err := newOrganizationDirectoryUserPasswordHash()
	if err != nil {
		return model.User{}, false, err
	}

	result, err := withTxValue(ctx, store.db, func(tx coredb.Tx) (organizationDirectoryUserResult, error) {
		now := time.Now().UTC()
		var emailVerifiedAt *time.Time
		if hasRealEmail && params.EmailVerified {
			emailVerifiedAt = &now
		}

		user, err := insertRegularUser(
			ctx,
			tx,
			resolvedEmail,
			organizationDirectoryUserName(params.Provider, params.ProviderTenantID, params.ExternalID),
			resolvedDisplayName,
			"",
			passwordHash,
			emailVerifiedAt,
		)
		if err != nil {
			existingUser, lookupErr := store.FindUserByEmail(ctx, resolvedEmail)
			if lookupErr == nil {
				return organizationDirectoryUserResult{user: existingUser, created: false}, nil
			}
			return organizationDirectoryUserResult{}, err
		}
		return organizationDirectoryUserResult{user: user, created: true}, nil
	})
	if err != nil {
		return model.User{}, false, err
	}
	return result.user, result.created, nil
}

type organizationDirectoryUserResult struct {
	user    model.User
	created bool
}

func organizationDirectoryDisplayNameFromEmail(email string) string {
	localPart := strings.TrimSpace(strings.SplitN(email, "@", 2)[0])
	if localPart == "" {
		return "user"
	}
	return localPart
}

func resolveOrganizationDirectoryUserEmail(params OrganizationDirectoryUserParams) (string, bool) {
	trimmedEmail := strings.TrimSpace(params.Email)
	if trimmedEmail != "" {
		if parsed, err := mail.ParseAddress(trimmedEmail); err == nil {
			return strings.ToLower(strings.TrimSpace(parsed.Address)), true
		}
	}
	return syntheticOrganizationDirectoryUserEmail(params.Provider, params.ProviderTenantID, params.ExternalID), false
}

func syntheticOrganizationDirectoryUserEmail(provider string, providerTenantID string, externalID string) string {
	localPart := organizationDirectoryUserName(provider, providerTenantID, externalID)
	if len(localPart) > 48 {
		localPart = strings.TrimRight(localPart[:48], "-")
	}
	if localPart == "" {
		localPart = "directory-user"
	}
	return localPart + "@directory.invalid"
}

func organizationDirectoryUserName(provider string, providerTenantID string, externalID string) string {
	parts := []string{
		slugifyDirectoryIdentityPart(provider),
		slugifyDirectoryIdentityPart(providerTenantID),
		slugifyDirectoryIdentityPart(externalID),
	}

	filtered := make([]string, 0, len(parts))
	for _, part := range parts {
		if part == "" {
			continue
		}
		filtered = append(filtered, part)
	}
	if len(filtered) == 0 {
		return "directory-user"
	}
	return strings.Join(filtered, "-")
}

func slugifyDirectoryIdentityPart(value string) string {
	slug := strings.ToLower(strings.TrimSpace(value))
	slug = strings.Map(func(r rune) rune {
		switch {
		case r >= 'a' && r <= 'z':
			return r
		case r >= '0' && r <= '9':
			return r
		default:
			return '-'
		}
	}, slug)
	slug = strings.Trim(slug, "-")
	for strings.Contains(slug, "--") {
		slug = strings.ReplaceAll(slug, "--", "-")
	}
	return slug
}

func newOrganizationDirectoryUserPasswordHash() (string, error) {
	token, err := secrets.NewToken(32)
	if err != nil {
		return "", fmt.Errorf("generate organization directory password seed: %w", err)
	}
	hash, err := passwords.Hash(token)
	if err != nil {
		return "", fmt.Errorf("hash organization directory password seed: %w", err)
	}
	return hash, nil
}
