package mailapi

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/connection"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/mailstore"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
)

type IMAPProber interface {
	Probe(
		context.Context,
		protocol.Endpoint,
	) (protocol.Capabilities, error)
}

type SMTPProber interface {
	ProbeSender(
		context.Context,
		protocol.Endpoint,
		string,
	) (protocol.Capabilities, error)
}

type Persistence interface {
	RequireWorkspaceAppManagementForUser(
		context.Context,
		string,
		string,
	) error
	FinalizeStatelessInstallation(
		context.Context,
		string,
		string,
		mailstore.StatelessConnectionMaterial,
	) (mailstore.StatelessInstallationView, error)
	LoadStatelessReconnectIdentity(
		context.Context,
		string,
		string,
	) (mailstore.ReconnectIdentity, error)
	FinalizeStatelessReconnect(
		context.Context,
		string,
		string,
		string,
		mailstore.StatelessConnectionMaterial,
	) (runtime.ConnectionView, error)
	DisconnectStatelessConnection(
		context.Context,
		string,
		string,
		string,
		string,
	) error
}

type InstallResult struct {
	InstallationID string
	ConnectionID   string
	ExtensionName  string
	DisplayName    string
	SenderAddress  string
	Capabilities   []string
}

type ReconnectResult struct {
	ConnectionID  string
	SenderAddress string
	Capabilities  []string
}

type Service struct {
	imap          IMAPProber
	smtp          SMTPProber
	persistence   Persistence
	credentialKey []byte
}

func NewService(
	imap IMAPProber,
	smtp SMTPProber,
	persistence Persistence,
	credentialKey []byte,
) *Service {
	return &Service{
		imap:          imap,
		smtp:          smtp,
		persistence:   persistence,
		credentialKey: append([]byte(nil), credentialKey...),
	}
}

func (service *Service) Install(
	ctx context.Context,
	userID string,
	workspaceID string,
	request mail.InstallRequest,
) (InstallResult, error) {
	request, err := mail.NormalizeInstallRequest(request)
	if err != nil {
		return InstallResult{}, err
	}
	if err := service.validate(); err != nil {
		return InstallResult{}, err
	}
	if err := service.persistence.RequireWorkspaceAppManagementForUser(
		ctx,
		strings.TrimSpace(userID),
		strings.TrimSpace(workspaceID),
	); err != nil {
		return InstallResult{}, err
	}
	material, capabilities, err := service.verify(
		ctx,
		request.SenderAddress,
		request.IMAP,
		request.SMTP,
	)
	if err != nil {
		return InstallResult{}, err
	}
	material.DisplayName = request.DisplayName
	material.ExtensionName = request.ExtensionName
	material.ExternalSubjectRef, err = mail.ExternalSubjectRef(
		service.credentialKey,
		request.SenderAddress,
	)
	if err != nil {
		return InstallResult{}, err
	}
	view, err := service.persistence.FinalizeStatelessInstallation(
		ctx,
		strings.TrimSpace(userID),
		strings.TrimSpace(workspaceID),
		material,
	)
	if err != nil {
		return InstallResult{}, err
	}
	return InstallResult{
		InstallationID: view.Installation.ID,
		ConnectionID:   view.Connection.ID,
		ExtensionName:  view.Extension.ExtensionName,
		DisplayName:    request.DisplayName,
		SenderAddress:  request.SenderAddress,
		Capabilities:   capabilities,
	}, nil
}

func (service *Service) Reconnect(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	request mail.ReconnectRequest,
) (ReconnectResult, error) {
	request, err := mail.NormalizeReconnectRequest(request)
	if err != nil {
		return ReconnectResult{}, err
	}
	if err := service.validate(); err != nil {
		return ReconnectResult{}, err
	}
	if err := service.persistence.RequireWorkspaceAppManagementForUser(
		ctx,
		strings.TrimSpace(userID),
		strings.TrimSpace(workspaceID),
	); err != nil {
		return ReconnectResult{}, err
	}
	identity, err := service.persistence.LoadStatelessReconnectIdentity(
		ctx,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
	)
	if err != nil {
		return ReconnectResult{}, err
	}
	material, capabilities, err := service.verify(
		ctx,
		identity.SenderAddress,
		request.IMAP,
		request.SMTP,
	)
	if err != nil {
		return ReconnectResult{}, err
	}
	material.ExternalSubjectRef = identity.ExternalSubjectRef
	connection, err := service.persistence.FinalizeStatelessReconnect(
		ctx,
		strings.TrimSpace(userID),
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
		material,
	)
	if err != nil {
		return ReconnectResult{}, err
	}
	return ReconnectResult{
		ConnectionID:  connection.ID,
		SenderAddress: identity.SenderAddress,
		Capabilities:  capabilities,
	}, nil
}

func (service *Service) Disconnect(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	connectionID string,
) error {
	if err := service.validate(); err != nil {
		return err
	}
	return service.persistence.DisconnectStatelessConnection(
		ctx,
		strings.TrimSpace(userID),
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
		strings.TrimSpace(connectionID),
	)
}

func (service *Service) verify(
	ctx context.Context,
	senderAddress string,
	imap mail.IMAPSettings,
	smtp mail.SMTPSettings,
) (
	mailstore.StatelessConnectionMaterial,
	[]string,
	error,
) {
	imapCapabilities, err := service.imap.Probe(
		ctx,
		protocol.Endpoint{
			Host:     imap.Host,
			Port:     imap.Port,
			TLSMode:  imap.TLSMode,
			Username: imap.Auth.Username,
			Password: imap.Auth.Password,
		},
	)
	if err != nil {
		return mailstore.StatelessConnectionMaterial{}, nil,
			fmt.Errorf("verify IMAP connection: %w", err)
	}
	if !imapCapabilities.Has("IMAP4REV1") &&
		!imapCapabilities.Has("IMAP4REV2") {
		return mailstore.StatelessConnectionMaterial{}, nil,
			fmt.Errorf("IMAP server does not expose a supported revision")
	}
	smtpAuth := smtp.Auth
	if smtpAuth == nil {
		return mailstore.StatelessConnectionMaterial{}, nil,
			fmt.Errorf("SMTP authentication is unavailable")
	}
	smtpCapabilities, err := service.smtp.ProbeSender(
		ctx,
		protocol.Endpoint{
			Host:     smtp.Host,
			Port:     smtp.Port,
			TLSMode:  smtp.TLSMode,
			Username: smtpAuth.Username,
			Password: smtpAuth.Password,
		},
		senderAddress,
	)
	if err != nil {
		return mailstore.StatelessConnectionMaterial{}, nil,
			fmt.Errorf("verify SMTP sender: %w", err)
	}
	imapValues := sortedCapabilities(imapCapabilities)
	smtpValues := sortedCapabilities(smtpCapabilities)
	config := connection.Config{
		SchemaVersion:         1,
		SenderAddress:         senderAddress,
		SenderAddressAccepted: true,
		IMAP: connection.Endpoint{
			Host:         imap.Host,
			Port:         imap.Port,
			TLSMode:      imap.TLSMode,
			Capabilities: imapValues,
			DraftMailbox: imapCapabilities.DraftMailbox,
		},
		SMTP: connection.Endpoint{
			Host:         smtp.Host,
			Port:         smtp.Port,
			TLSMode:      smtp.TLSMode,
			Capabilities: smtpValues,
		},
	}
	digest, err := config.ComputeDigest()
	if err != nil {
		return mailstore.StatelessConnectionMaterial{}, nil, err
	}
	config.CapabilityDigest = digest
	allCapabilities := make([]string, 0, len(imapValues)+len(smtpValues))
	for _, value := range imapValues {
		allCapabilities = append(allCapabilities, "imap:"+value)
	}
	for _, value := range smtpValues {
		allCapabilities = append(allCapabilities, "smtp:"+value)
	}
	return mailstore.StatelessConnectionMaterial{
		SenderAddress: senderAddress,
		Credentials: map[string]any{
			"schemaVersion": 1,
			"imap": map[string]any{
				"username": imap.Auth.Username,
				"password": imap.Auth.Password,
			},
			"smtp": map[string]any{
				"username": smtpAuth.Username,
				"password": smtpAuth.Password,
			},
		},
		Config: config.JSONObject(),
	}, allCapabilities, nil
}

func (service *Service) validate() error {
	if service == nil ||
		service.imap == nil ||
		service.smtp == nil ||
		service.persistence == nil ||
		len(service.credentialKey) < 32 {
		return fmt.Errorf("Email connection service is not configured")
	}
	return nil
}

func sortedCapabilities(
	capabilities protocol.Capabilities,
) []string {
	set := make(map[string]struct{}, len(capabilities.Values)+1)
	for value, supported := range capabilities.Values {
		if supported {
			set[strings.ToUpper(value)] = struct{}{}
		}
	}
	if capabilities.DraftMailbox != "" {
		set["DRAFTS"] = struct{}{}
	}
	result := make([]string, 0, len(set))
	for value := range set {
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}
