package mailruntime

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/mailstore"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/token"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/transport"
	"github.com/tidbcloud/AgentTeam/internal/store/extension"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const maxTokenTTL = 10 * time.Minute
const defaultCursorTTL = 2 * time.Minute

// Service executes direct, bounded IMAP Actions without mailbox runtime state.
type Service struct {
	connections         ConnectionLoader
	credentialKey       []byte
	legacyCredentialKey [][]byte
	imap                protocol.IMAP
	smtp                protocol.SMTP
	tokens              *token.Codec
	attachments         AttachmentLoader
	materializer        AttachmentMaterializer
	authorizer          InvocationAuthorizer
	now                 func() time.Time
	tokenTTL            time.Duration
	cursorTTL           time.Duration
	initializationErr   error
}

// Options carries production collaborators and deterministic test seams.
type Options struct {
	Connections          ConnectionLoader
	LegacyCredentialKeys [][]byte
	IMAP                 protocol.IMAP
	SMTP                 protocol.SMTP
	Tokens               *token.Codec
	Attachments          AttachmentLoader
	Materializer         AttachmentMaterializer
	Authorizer           InvocationAuthorizer
	TokenAudience        string
	Now                  func() time.Time
	TokenTTL             time.Duration
	CursorTTL            time.Duration
}

// New constructs the stateless Mail Action runtime.
func New(
	db coredb.DB,
	credentialKey []byte,
	options Options,
) *Service {
	now := options.Now
	if now == nil {
		now = time.Now
	}
	connections := options.Connections
	if connections == nil {
		connections = mailstore.New(db)
	}
	imap := options.IMAP
	if imap == nil {
		imap = protocol.NewIMAPClient(protocol.ClientOptions{
			Connector: &transport.Dialer{
				Resolver: transport.NetResolver{},
			},
		})
	}
	smtp := options.SMTP
	if smtp == nil {
		smtp = protocol.NewSMTPClient(protocol.ClientOptions{
			Connector: &transport.Dialer{
				Resolver: transport.NetResolver{},
			},
		})
	}
	legacyKeys := cloneByteSlices(options.LegacyCredentialKeys)
	tokens := options.Tokens
	var initializationErr error
	if tokens == nil {
		audience := strings.TrimSpace(options.TokenAudience)
		if audience == "" {
			audience = "agentteam"
		}
		keyring, err := runtimeTokenKeyring(
			credentialKey,
			legacyKeys,
		)
		if err == nil {
			tokens, err = token.New(keyring, audience, now)
		}
		initializationErr = err
	}
	tokenTTL := options.TokenTTL
	if tokenTTL <= 0 || tokenTTL > maxTokenTTL {
		tokenTTL = maxTokenTTL
	}
	cursorTTL := options.CursorTTL
	if cursorTTL <= 0 {
		cursorTTL = defaultCursorTTL
	}
	if cursorTTL >= tokenTTL {
		cursorTTL = tokenTTL / 2
	}
	if cursorTTL <= 0 {
		cursorTTL = tokenTTL
	}
	return &Service{
		connections:         connections,
		credentialKey:       append([]byte(nil), credentialKey...),
		legacyCredentialKey: legacyKeys,
		imap:                imap,
		smtp:                smtp,
		tokens:              tokens,
		attachments:         options.Attachments,
		materializer:        options.Materializer,
		authorizer:          options.Authorizer,
		now:                 now,
		tokenTTL:            tokenTTL,
		cursorTTL:           cursorTTL,
		initializationErr:   initializationErr,
	}
}

// authorizeMutation is the fresh fence re-resolving installation, selection,
// definition digest, and explicit agent authorization right before a provider
// or AgentTeam-side mutation. It fails closed when no authorizer is configured.
func (service *Service) authorizeMutation(
	ctx context.Context,
	invocation Invocation,
) error {
	if service.authorizer == nil {
		return mailError(
			ErrorPermissionDenied,
			"mail mutation authorization is unavailable",
			nil,
		)
	}
	err := service.authorizer.ReauthorizeInvocation(
		ctx,
		InvocationAuthorization{
			WorkspaceID:          invocation.WorkspaceID,
			AgentID:              invocation.AgentID,
			AgentMemberID:        invocation.AgentMemberID,
			AuthorizedByMemberID: invocation.AuthorizedByMemberID,
			InstallationID:       invocation.ServiceInstallationID,
			ConnectionID:         invocation.ServiceConnectionID,
			ConnectionGeneration: invocation.ServiceConnectionGeneration,
			SelectionID:          invocation.ServiceInstallationExtensionID,
			Action:               invocation.Action,
			ExtensionName:        invocation.ExtensionName,
			DefinitionDigest:     invocation.DefinitionDigest,
			DefinitionVersion:    invocation.DefinitionVersion,
			CatalogVersion:       invocation.CatalogVersion,
			RequiredScopes:       actionRequiredScopes(invocation.Action),
		},
	)
	if err != nil {
		switch {
		case errors.Is(err, storeerr.ErrForbidden),
			errors.Is(err, storeerr.ErrNotFound):
			return mailError(
				ErrorPermissionDenied,
				"mail action is no longer authorized",
				nil,
			)
		case errors.Is(err, storeerr.ErrBadRequest),
			errors.Is(err, storeerr.ErrUnprocessable):
			return mailError(
				ErrorValidation,
				"mail action authorization is invalid",
				nil,
			)
		default:
			return mailError(
				ErrorUpstream,
				"mail action authorization is unavailable",
				err,
			)
		}
	}
	return nil
}

func actionRequiredScopes(action string) []string {
	switch action {
	case actionSendDraft:
		return []string{ScopeDraftWrite, ScopeSend}
	case actionCreateDraft, actionUpdateDraft:
		return []string{ScopeDraftWrite}
	case actionCreateReplyDraft, actionCreateForwardDraft:
		return []string{
			ScopeMetadataRead, ScopeContentRead, ScopeDraftWrite,
		}
	case actionGetAttachment:
		return []string{ScopeMetadataRead, ScopeContentRead}
	default:
		return nil
	}
}

func (service *Service) validate() error {
	if service == nil ||
		service.connections == nil ||
		service.imap == nil ||
		service.smtp == nil ||
		service.tokens == nil ||
		service.now == nil ||
		len(service.credentialKey) != 32 ||
		service.initializationErr != nil {
		return fmt.Errorf("mail runtime is not configured")
	}
	return nil
}

// Invoke validates the trusted envelope, re-resolves its exact Connection, and
// dispatches exactly one of the nine P0 Actions.
func (service *Service) Invoke(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	if err := service.validate(); err != nil {
		return nil, mailError(
			ErrorInternal,
			"mail execution is unavailable",
			err,
		)
	}
	if err := validateInvocation(invocation); err != nil {
		return nil, err
	}
	switch invocation.Action {
	case actionSearchMessages:
		return service.searchMessages(ctx, invocation)
	case actionGetMessage:
		return service.getMessage(ctx, invocation)
	case actionListAttachments:
		return service.listAttachments(ctx, invocation)
	case actionGetAttachment:
		return service.getAttachment(ctx, invocation)
	case actionCreateDraft:
		return service.createDraft(ctx, invocation)
	case actionUpdateDraft:
		return service.updateDraft(ctx, invocation)
	case actionCreateReplyDraft:
		return service.createReplyDraft(ctx, invocation)
	case actionCreateForwardDraft:
		return service.createForwardDraft(ctx, invocation)
	case actionSendDraft:
		return service.sendDraft(ctx, invocation)
	default:
		return nil, mailError(
			ErrorNotFound,
			"mail action is unavailable",
			nil,
		)
	}
}

type runtimeContext struct {
	invocation Invocation
	material   connectionMaterial
}

func (service *Service) resolveRuntime(
	ctx context.Context,
	invocation Invocation,
) (runtimeContext, error) {
	connection, err := service.connections.LoadActiveConnection(
		ctx,
		invocation.WorkspaceID,
		invocation.ServiceInstallationID,
		invocation.ServiceConnectionID,
	)
	if err != nil {
		return runtimeContext{}, classifyConnectionError(err)
	}
	if connection.ID != invocation.ServiceConnectionID ||
		connection.WorkspaceID != invocation.WorkspaceID ||
		connection.ServiceInstallationID !=
			invocation.ServiceInstallationID ||
		strings.TrimSpace(connection.Generation) == "" ||
		extension.RuntimeConnectionGeneration(
			connection.ID,
			connection.ExternalSubjectRef,
			mailstore.CredentialEnvelopeDigest(
				connection.EncryptedCredentials,
			),
			connection.Config,
		) != invocation.ServiceConnectionGeneration {
		return runtimeContext{}, mailError(
			ErrorConflict,
			"mail Connection identity changed",
			nil,
		)
	}
	material, err := decodeConnectionMaterial(
		service.credentialKey,
		service.legacyCredentialKey,
		connection,
	)
	if err != nil {
		return runtimeContext{}, mailError(
			ErrorProviderAuth,
			"mail Connection material is unavailable",
			err,
		)
	}
	if !material.supports(invocation.Action) {
		return runtimeContext{}, mailError(
			ErrorPermissionDenied,
			"mail action is unsupported by this Connection",
			nil,
		)
	}
	return runtimeContext{
		invocation: invocation,
		material:   material,
	}, nil
}

// recheckRuntime closes disconnect, reconnect, credential replacement, config,
// and capability-digest races before provider I/O and before releasing results.
func (service *Service) recheckRuntime(
	ctx context.Context,
	current runtimeContext,
) (runtimeContext, error) {
	next, err := service.resolveRuntime(ctx, current.invocation)
	if err != nil {
		return runtimeContext{}, err
	}
	if next.material.Connection.Generation !=
		current.material.Connection.Generation ||
		next.material.Connection.ID != current.material.Connection.ID ||
		next.material.Connection.ExternalSubjectRef !=
			current.material.Connection.ExternalSubjectRef {
		return runtimeContext{}, mailError(
			ErrorConflict,
			"mail Connection generation changed",
			nil,
		)
	}
	return next, nil
}

func validateInvocation(invocation Invocation) error {
	if strings.TrimSpace(invocation.WorkspaceID) == "" ||
		strings.TrimSpace(invocation.AgentID) == "" ||
		strings.TrimSpace(invocation.AgentMemberID) == "" ||
		strings.TrimSpace(invocation.ServiceInstallationID) == "" ||
		strings.TrimSpace(invocation.ServiceConnectionID) == "" ||
		strings.TrimSpace(invocation.ServiceConnectionGeneration) == "" ||
		strings.TrimSpace(invocation.ServiceInstallationExtensionID) == "" ||
		strings.TrimSpace(invocation.ExtensionName) == "" ||
		strings.TrimSpace(invocation.Action) == "" ||
		invocation.DefinitionVersion <= 0 ||
		strings.TrimSpace(invocation.DefinitionDigest) == "" ||
		invocation.CatalogVersion <= 0 ||
		strings.TrimSpace(invocation.RequestHash) == "" ||
		len(invocation.Input) == 0 {
		return mailError(
			ErrorValidation,
			"mail invocation identity is invalid",
			nil,
		)
	}
	return nil
}

func classifyConnectionError(err error) error {
	if _, ok := ErrorCodeOf(err); ok {
		return err
	}
	switch {
	case errors.Is(err, storeerr.ErrNotFound):
		return mailError(
			ErrorNotFound,
			"mail Connection is unavailable",
			err,
		)
	case errors.Is(err, storeerr.ErrConflict):
		return mailError(
			ErrorConflict,
			"mail Connection changed",
			err,
		)
	case errors.Is(err, storeerr.ErrBadRequest),
		errors.Is(err, storeerr.ErrUnprocessable):
		return mailError(
			ErrorValidation,
			"mail Connection identity is invalid",
			err,
		)
	default:
		return mailError(
			ErrorUpstream,
			"mail Connection could not be resolved",
			err,
		)
	}
}

func classifyProtocolError(err error) error {
	switch protocol.ErrorClass(err) {
	case protocol.ErrorUnsupported:
		return mailError(
			ErrorPermissionDenied,
			"mail action is unsupported by the mailbox server",
			err,
		)
	case protocol.ErrorConflict:
		return mailError(
			ErrorConflict,
			"mailbox resource changed",
			err,
		)
	case protocol.ErrorNotFound:
		return mailError(
			ErrorNotFound,
			"mailbox resource is unavailable",
			err,
		)
	case protocol.ErrorInvalid:
		return mailError(
			ErrorValidation,
			"mailbox request is invalid",
			err,
		)
	case protocol.ErrorLimit:
		return mailError(
			ErrorUpstream,
			"mailbox response exceeded a safety limit",
			err,
		)
	default:
		return mailError(
			ErrorUpstream,
			"mailbox request failed",
			err,
		)
	}
}

func runtimeTokenKeyring(
	current []byte,
	legacy [][]byte,
) (token.Keyring, error) {
	if len(current) != 32 {
		return token.Keyring{}, fmt.Errorf(
			"mail credential key is not configured",
		)
	}
	keyring := token.Keyring{Current: token.Key{
		ID:       runtimeTokenKeyID(current),
		Material: append([]byte(nil), current...),
	}}
	for _, material := range legacy {
		if len(material) != 32 {
			return token.Keyring{}, fmt.Errorf(
				"mail legacy credential key is invalid",
			)
		}
		keyring.Legacy = append(keyring.Legacy, token.Key{
			ID:       runtimeTokenKeyID(material),
			Material: append([]byte(nil), material...),
		})
	}
	return keyring, nil
}

func runtimeTokenKeyID(material []byte) string {
	digest := sha256.Sum256(material)
	return "k_" + hex.EncodeToString(digest[:8])
}

func cloneByteSlices(values [][]byte) [][]byte {
	result := make([][]byte, 0, len(values))
	for _, value := range values {
		result = append(result, append([]byte(nil), value...))
	}
	return result
}
