package integrationservice

import (
	"database/sql"
	"errors"
	"fmt"
	"slices"
	"sort"
	"strings"

	"github.com/nexuscrm/kernel/pkg/ports"
	httpclient "github.com/nexuscrm/outbound/http_client"
	connectedmailhttp "github.com/pingcap-inc/nexuscrm-connectors/features/connector/connectedmailhttp"
	connectormail "github.com/pingcap-inc/nexuscrm-connectors/features/connector/mail"
	connectoroauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
	connectoroutboundintent "github.com/pingcap-inc/nexuscrm-connectors/features/connector/outboundintent"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

const connectedMailCipherKeyID = "connected-mail-master-v1"

// connectedMailRuntime is the provider-neutral process composition shared by
// the browser authorization surface and the outbound mail preparer.
type connectedMailRuntime struct {
	connectorKey string
	oauth        *connectoroauth.Service
	providers    *connectedMailProviderResolver
	policy       connectedMailAuthorizationPolicy
	preparer     *connectoroutboundintent.MailPreparer
}

type connectedMailProviderResolver struct {
	connectorKey string
	oauth        connectoroauth.OAuthProvider
	mail         connectormail.Provider
}

func (resolver *connectedMailProviderResolver) Provider(connectorKey string) (connectoroauth.OAuthProvider, bool) {
	if resolver == nil || resolver.oauth == nil || connectorKey != resolver.connectorKey {
		return nil, false
	}
	return resolver.oauth, true
}

func (resolver *connectedMailProviderResolver) MailProvider(connectorKey string) (connectormail.Provider, bool) {
	if resolver == nil || resolver.mail == nil || connectorKey != resolver.connectorKey {
		return nil, false
	}
	return resolver.mail, true
}

type connectedMailAuthorizationPolicy struct {
	connectorKey string
	callbackURI  string
	scopes       []string
}

func (policy connectedMailAuthorizationPolicy) ValidateRedirect(connectorKey, redirectURI string) error {
	if connectorKey != policy.connectorKey || redirectURI != policy.callbackURI {
		return connectoroauth.ErrAuthorizationDenied
	}
	return nil
}

func (policy connectedMailAuthorizationPolicy) ValidateScopes(connectorKey string, scopes []string) error {
	if connectorKey != policy.connectorKey || !sameConnectedMailScopeSet(scopes, policy.scopes) {
		return connectoroauth.ErrAuthorizationDenied
	}
	return nil
}

func sameConnectedMailScopeSet(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	left = append([]string(nil), left...)
	right = append([]string(nil), right...)
	for index := range left {
		left[index] = strings.TrimSpace(left[index])
	}
	for index := range right {
		right[index] = strings.TrimSpace(right[index])
	}
	sort.Strings(left)
	sort.Strings(right)
	return slices.Equal(left, right)
}

// withConnectedMailPreparer leaves the existing outbound path byte-for-byte
// selected when no connected-mail configuration is present. When configured,
// it adds only the SEND_MAIL preparer in front of that existing fallback.
func withConnectedMailPreparer(
	db *sql.DB,
	config *ConnectedMailConfig,
	view catalogv1.IntegrationBindingView,
	throttler connectoroutboundintent.ResolvedMailThrottler,
	allowlist ports.URLAllowlist,
	fallback connectoroutboundintent.Preparer,
	attachmentReaders ...connectoroutboundintent.MailAttachmentReader,
) (connectoroutboundintent.Preparer, *connectedMailRuntime, error) {
	if fallback == nil {
		return nil, nil, errors.New("integration service: outbound intent fallback is required")
	}
	if config == nil {
		return fallback, nil, nil
	}
	runtime, err := newConnectedMailRuntime(db, config, view, throttler, allowlist, attachmentReaders...)
	if err != nil {
		return nil, nil, err
	}
	preparer, err := connectoroutboundintent.NewOperationPreparer(runtime.preparer, fallback)
	if err != nil {
		return nil, nil, fmt.Errorf("integration service: compose connected-mail outbound owner: %w", err)
	}
	return preparer, runtime, nil
}

func newConnectedMailRuntime(
	db *sql.DB,
	config *ConnectedMailConfig,
	view catalogv1.IntegrationBindingView,
	throttler connectoroutboundintent.ResolvedMailThrottler,
	allowlist ports.URLAllowlist,
	attachmentReaders ...connectoroutboundintent.MailAttachmentReader,
) (*connectedMailRuntime, error) {
	if db == nil || config == nil || throttler == nil || allowlist == nil || len(attachmentReaders) > 1 {
		return nil, errors.New("integration service: connected-mail composition dependencies are required")
	}
	binding, err := config.Definition.MailBindingDefinition(view)
	if err != nil {
		return nil, fmt.Errorf("integration service: bind connected-mail Catalog authority: %w", err)
	}
	transport := httpclient.NewOutboundRoundTripper(allowlist)
	oauthProvider, err := connectedmailhttp.NewOAuthAdapter(
		config.Definition.OAuthConfig(config.OAuthClientID, config.OAuthClientSecret),
		transport,
		allowlist,
	)
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail OAuth provider: %w", err)
	}
	mailProvider, err := connectedmailhttp.NewMailAdapter(config.Definition.MailConfig(), transport, allowlist)
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail delivery provider: %w", err)
	}
	providers := &connectedMailProviderResolver{
		connectorKey: binding.ConnectorKey,
		oauth:        oauthProvider,
		mail:         mailProvider,
	}
	policy := connectedMailAuthorizationPolicy{
		connectorKey: binding.ConnectorKey,
		callbackURI:  config.CallbackURI,
		scopes:       append([]string(nil), binding.RequiredScopes...),
	}
	cipher, err := connectoroauth.NewAESGCMCipher(
		connectedMailCipherKeyID,
		map[string][]byte{connectedMailCipherKeyID: config.MasterKey},
	)
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail secret cipher: %w", err)
	}
	repository, err := connectoroauth.NewSQLRepository(db)
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail OAuth repository: %w", err)
	}
	oauth, err := connectoroauth.NewService(repository, cipher, providers, policy)
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail OAuth owner: %w", err)
	}
	authority, err := connectoroutboundintent.NewCatalogMailAuthority(db, view, []connectoroutboundintent.MailBindingDefinition{binding})
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail Catalog owner: %w", err)
	}
	var preparer *connectoroutboundintent.MailPreparer
	if len(attachmentReaders) == 1 && attachmentReaders[0] != nil {
		preparer, err = connectoroutboundintent.NewResolvedMailPreparerWithAttachments(
			authority, oauth, providers, throttler, attachmentReaders[0],
		)
	} else {
		preparer, err = connectoroutboundintent.NewResolvedMailPreparer(authority, oauth, providers, throttler)
	}
	if err != nil {
		return nil, fmt.Errorf("integration service: build connected-mail intent preparer: %w", err)
	}
	return &connectedMailRuntime{
		connectorKey: binding.ConnectorKey,
		oauth:        oauth,
		providers:    providers,
		policy:       policy,
		preparer:     preparer,
	}, nil
}

var (
	_ connectoroauth.ProviderResolver              = (*connectedMailProviderResolver)(nil)
	_ connectoroutboundintent.MailProviderResolver = (*connectedMailProviderResolver)(nil)
	_ connectoroauth.AuthorizationPolicy           = connectedMailAuthorizationPolicy{}
)
