package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/mail"
	"net/url"
	"sort"
	"strings"
	"time"

	connectordelivery "github.com/pingcap-inc/nexuscrm-connectors/features/connector/delivery"
	connectoroauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
)

const (
	GmailConnectedMailboxKey = "gmail"
	gmailProviderTimeout     = 20 * time.Second

	googleAuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
	googleTokenEndpoint         = "https://oauth2.googleapis.com/token"
	googleUserInfoEndpoint      = "https://openidconnect.googleapis.com/v1/userinfo"
	googleRevokeEndpoint        = "https://oauth2.googleapis.com/revoke"
	gmailSendEndpoint           = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
	googleUserInfoEmailScope    = "https://www.googleapis.com/auth/userinfo.email"
	gmailSendScope              = "https://www.googleapis.com/auth/gmail.send"

	googleIssuer = "https://accounts.google.com"
)

var gmailConnectedMailboxScopes = []string{
	"openid",
	"email",
	gmailSendScope,
}

func GmailConnectedMailboxScopes() []string {
	return append([]string(nil), gmailConnectedMailboxScopes...)
}

type GmailConnectedMailboxConfig struct {
	ClientID     string
	ClientSecret string
	HTTPClient   *http.Client
}

type GmailConnectedMailboxProvider struct {
	clientID     string
	clientSecret string
	httpClient   *http.Client
}

func NewGmailConnectedMailboxProvider(config GmailConnectedMailboxConfig) (*GmailConnectedMailboxProvider, error) {
	clientID := strings.TrimSpace(config.ClientID)
	clientSecret := strings.TrimSpace(config.ClientSecret)
	if clientID == "" || clientSecret == "" {
		return nil, fmt.Errorf("gmail connected mailbox OAuth client configuration is incomplete")
	}
	httpClient := config.HTTPClient
	if httpClient == nil {
		httpClient = &http.Client{Timeout: gmailProviderTimeout}
	}
	client := *httpClient
	if client.Timeout <= 0 || client.Timeout > gmailProviderTimeout {
		client.Timeout = gmailProviderTimeout
	}
	client.CheckRedirect = func(*http.Request, []*http.Request) error {
		return http.ErrUseLastResponse
	}
	return &GmailConnectedMailboxProvider{
		clientID:     clientID,
		clientSecret: clientSecret,
		httpClient:   &client,
	}, nil
}

func (p *GmailConnectedMailboxProvider) EffectTerminationBound() time.Duration {
	if p == nil || p.httpClient == nil || p.httpClient.Timeout <= 0 {
		return gmailProviderTimeout
	}
	return p.httpClient.Timeout
}

func (p *GmailConnectedMailboxProvider) AuthorizationURL(request connectoroauth.AuthorizationURLRequest) (string, error) {
	if p == nil || p.clientID == "" {
		return "", gmailProviderError("authorization", connectoroauth.ProviderErrorAuthorization, "unauthorized")
	}
	if !gmailStringSetsEqual(request.Scopes, gmailConnectedMailboxScopes) {
		return "", gmailProviderError("authorization", connectoroauth.ProviderErrorAuthorization, "invalid_scope")
	}
	values := url.Values{
		"client_id":             {p.clientID},
		"redirect_uri":          {strings.TrimSpace(request.RedirectURI)},
		"response_type":         {"code"},
		"scope":                 {strings.Join(request.Scopes, " ")},
		"state":                 {strings.TrimSpace(request.State)},
		"code_challenge":        {strings.TrimSpace(request.CodeChallenge)},
		"code_challenge_method": {strings.TrimSpace(request.ChallengeMethod)},
		"access_type":           {"offline"},
		"prompt":                {"consent"},
	}
	for _, value := range values {
		if len(value) == 0 || strings.TrimSpace(value[0]) == "" {
			return "", gmailProviderError("authorization", connectoroauth.ProviderErrorAuthorization, "provider_rejected")
		}
	}
	return googleAuthorizationEndpoint + "?" + values.Encode(), nil
}

func (p *GmailConnectedMailboxProvider) Exchange(ctx context.Context, request connectoroauth.TokenExchangeRequest) (connectoroauth.TokenSet, error) {
	values := url.Values{
		"client_id":     {p.clientID},
		"client_secret": {p.clientSecret},
		"code":          {strings.TrimSpace(request.Code)},
		"code_verifier": {strings.TrimSpace(request.CodeVerifier)},
		"redirect_uri":  {strings.TrimSpace(request.RedirectURI)},
		"grant_type":    {"authorization_code"},
	}
	return p.tokenRequest(ctx, "exchange", values)
}

func (p *GmailConnectedMailboxProvider) Identity(ctx context.Context, accessToken string) (connectoroauth.RemoteIdentity, error) {
	if strings.TrimSpace(accessToken) == "" {
		return connectoroauth.RemoteIdentity{}, gmailProviderError("identity", connectoroauth.ProviderErrorAuthorization, "unauthorized")
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, googleUserInfoEndpoint, nil)
	if err != nil {
		return connectoroauth.RemoteIdentity{}, gmailProviderError("identity", connectoroauth.ProviderErrorRejected, "provider_rejected")
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(accessToken))
	resp, err := p.httpClient.Do(req)
	if err != nil {
		return connectoroauth.RemoteIdentity{}, gmailProviderError("identity", connectoroauth.ProviderErrorTransient, "transport_error")
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return connectoroauth.RemoteIdentity{}, gmailHTTPProviderError("identity", resp.StatusCode)
	}
	var payload struct {
		Subject       string `json:"sub"`
		Email         string `json:"email"`
		EmailVerified bool   `json:"email_verified"`
		Name          string `json:"name"`
	}
	if err := decodeBoundedJSON(resp.Body, &payload); err != nil || !payload.EmailVerified ||
		strings.TrimSpace(payload.Subject) == "" || !isBareEmailAddress(payload.Email) {
		return connectoroauth.RemoteIdentity{}, gmailProviderError("identity", connectoroauth.ProviderErrorRejected, "provider_rejected")
	}
	return connectoroauth.RemoteIdentity{
		Issuer:           googleIssuer,
		Subject:          strings.TrimSpace(payload.Subject),
		PrincipalAddress: strings.TrimSpace(payload.Email),
		DisplayName:      strings.TrimSpace(payload.Name),
	}, nil
}

func (p *GmailConnectedMailboxProvider) Refresh(ctx context.Context, refreshToken string) (connectoroauth.TokenSet, error) {
	values := url.Values{
		"client_id":     {p.clientID},
		"client_secret": {p.clientSecret},
		"refresh_token": {strings.TrimSpace(refreshToken)},
		"grant_type":    {"refresh_token"},
	}
	return p.tokenRequest(ctx, "refresh", values)
}

func (p *GmailConnectedMailboxProvider) Revoke(ctx context.Context, token string) error {
	if strings.TrimSpace(token) == "" {
		return gmailProviderError("revoke", connectoroauth.ProviderErrorRejected, "provider_rejected")
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, googleRevokeEndpoint, strings.NewReader(url.Values{"token": {strings.TrimSpace(token)}}.Encode()))
	if err != nil {
		return gmailProviderError("revoke", connectoroauth.ProviderErrorRejected, "provider_rejected")
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := p.httpClient.Do(req)
	if err != nil {
		return gmailProviderError("revoke", connectoroauth.ProviderErrorUncertain, "transport_error")
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return gmailHTTPProviderError("revoke", resp.StatusCode)
	}
	return nil
}

type ConnectedMailboxEmail struct {
	From    string
	To      string
	Subject string
	Body    string
}

func (p *GmailConnectedMailboxProvider) Dispatch(ctx context.Context, request connectordelivery.DispatchRequest) (connectordelivery.DispatchResult, error) {
	if p == nil || p.httpClient == nil || strings.TrimSpace(request.AccessToken) == "" || len(request.Payload) == 0 {
		return connectordelivery.DispatchResult{}, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_request")
	}
	payload, err := json.Marshal(struct {
		Raw string `json:"raw"`
	}{Raw: base64.RawURLEncoding.EncodeToString(request.Payload)})
	if err != nil {
		return connectordelivery.DispatchResult{}, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_message")
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, gmailSendEndpoint, bytes.NewReader(payload))
	if err != nil {
		return connectordelivery.DispatchResult{}, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_request")
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(request.AccessToken))
	req.Header.Set("Content-Type", "application/json")
	resp, err := p.httpClient.Do(req)
	if err != nil {
		return connectordelivery.DispatchResult{}, gmailDeliveryError(connectordelivery.ProviderErrorUncertain, "transport_error")
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		kind := connectordelivery.ProviderErrorRejected
		if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
			kind = connectordelivery.ProviderErrorAuthorization
		}
		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= http.StatusInternalServerError {
			kind = connectordelivery.ProviderErrorUncertain
		}
		return connectordelivery.DispatchResult{ResponseStatus: resp.StatusCode}, gmailDeliveryError(kind, gmailSafeHTTPCode(resp.StatusCode))
	}
	var response struct {
		ID       string `json:"id"`
		ThreadID string `json:"threadId"`
	}
	if err := decodeBoundedJSON(resp.Body, &response); err != nil || strings.TrimSpace(response.ID) == "" {
		return connectordelivery.DispatchResult{ResponseStatus: resp.StatusCode}, gmailDeliveryError(connectordelivery.ProviderErrorUncertain, "invalid_response")
	}
	return connectordelivery.DispatchResult{
		Outcome: connectordelivery.DispatchAccepted, RemoteReceiptID: strings.TrimSpace(response.ID),
		RemoteConversationID: strings.TrimSpace(response.ThreadID), ResponseStatus: resp.StatusCode,
	}, nil
}

func (p *GmailConnectedMailboxProvider) tokenRequest(ctx context.Context, operation string, values url.Values) (connectoroauth.TokenSet, error) {
	if p == nil || p.httpClient == nil || p.clientID == "" || p.clientSecret == "" {
		return connectoroauth.TokenSet{}, gmailProviderError(operation, connectoroauth.ProviderErrorAuthorization, "unauthorized")
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, googleTokenEndpoint, strings.NewReader(values.Encode()))
	if err != nil {
		return connectoroauth.TokenSet{}, gmailProviderError(operation, connectoroauth.ProviderErrorRejected, "provider_rejected")
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := p.httpClient.Do(req)
	if err != nil {
		return connectoroauth.TokenSet{}, gmailProviderError(operation, connectoroauth.ProviderErrorUncertain, "transport_error")
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return connectoroauth.TokenSet{}, gmailHTTPProviderError(operation, resp.StatusCode)
	}
	var payload struct {
		AccessToken  string `json:"access_token"`
		RefreshToken string `json:"refresh_token"`
		ExpiresIn    int64  `json:"expires_in"`
		Scope        string `json:"scope"`
		TokenType    string `json:"token_type"`
	}
	if err := decodeBoundedJSON(resp.Body, &payload); err != nil || strings.TrimSpace(payload.AccessToken) == "" ||
		strings.TrimSpace(payload.TokenType) == "" || (operation == "exchange" && strings.TrimSpace(payload.RefreshToken) == "") {
		return connectoroauth.TokenSet{}, gmailProviderError(operation, connectoroauth.ProviderErrorUncertain, "provider_error")
	}
	scopes := strings.Fields(payload.Scope)
	if operation == "exchange" {
		if !gmailGrantedScopesMatchMVP(scopes) {
			return connectoroauth.TokenSet{}, gmailProviderError(operation, connectoroauth.ProviderErrorAuthorization, "invalid_scope")
		}
		scopes = append([]string(nil), gmailConnectedMailboxScopes...)
	}
	var expiresAt *time.Time
	if payload.ExpiresIn > 0 {
		value := time.Now().UTC().Add(time.Duration(payload.ExpiresIn) * time.Second)
		expiresAt = &value
	}
	return connectoroauth.TokenSet{
		AccessToken:  payload.AccessToken,
		RefreshToken: payload.RefreshToken,
		TokenType:    payload.TokenType,
		Scopes:       scopes,
		ExpiresAt:    expiresAt,
	}, nil
}

func buildGmailRawMessage(message ConnectedMailboxEmail) ([]byte, error) {
	from, err := mail.ParseAddress(strings.TrimSpace(message.From))
	if err != nil || from.Address != strings.TrimSpace(message.From) {
		return nil, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_from")
	}
	to, err := mail.ParseAddress(strings.TrimSpace(message.To))
	if err != nil || to.Address != strings.TrimSpace(message.To) {
		return nil, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_to")
	}
	subject := strings.TrimSpace(message.Subject)
	if subject == "" || strings.ContainsAny(subject, "\r\n") {
		return nil, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_subject")
	}
	if strings.TrimSpace(message.Body) == "" {
		return nil, gmailDeliveryError(connectordelivery.ProviderErrorRejected, "invalid_body")
	}
	var raw strings.Builder
	raw.WriteString("From: ")
	raw.WriteString(from.Address)
	raw.WriteString("\r\nTo: ")
	raw.WriteString(to.Address)
	raw.WriteString("\r\n")
	encodedSubject := mime.QEncoding.Encode("utf-8", subject)
	writeFoldedGmailHeader(&raw, "Subject", encodedSubject, encodedSubject != subject)
	raw.WriteString("MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n")
	body := strings.ReplaceAll(message.Body, "\r\n", "\n")
	body = strings.ReplaceAll(body, "\r", "\n")
	raw.WriteString(strings.ReplaceAll(body, "\n", "\r\n"))
	return []byte(raw.String()), nil
}

func writeFoldedGmailHeader(raw *strings.Builder, name, value string, encodedWords bool) {
	prefix := name + ": "
	raw.WriteString(prefix)
	if !encodedWords {
		raw.WriteString(value)
		raw.WriteString("\r\n")
		return
	}
	lineLength := len(prefix)
	for index, word := range strings.Fields(value) {
		if index > 0 {
			if lineLength+1+len(word) > 78 {
				raw.WriteString("\r\n ")
				lineLength = 1
			} else {
				raw.WriteByte(' ')
				lineLength++
			}
		}
		raw.WriteString(word)
		lineLength += len(word)
	}
	raw.WriteString("\r\n")
}

func decodeBoundedJSON(reader io.Reader, target any) error {
	limited := &io.LimitedReader{R: reader, N: (1 << 20) + 1}
	decoder := json.NewDecoder(limited)
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		if err == nil {
			return fmt.Errorf("multiple JSON values")
		}
		return err
	}
	if limited.N <= 0 {
		return fmt.Errorf("JSON response exceeds limit")
	}
	return nil
}

func gmailStringSetsEqual(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	leftCopy := append([]string(nil), left...)
	rightCopy := append([]string(nil), right...)
	sort.Strings(leftCopy)
	sort.Strings(rightCopy)
	for index := range leftCopy {
		if leftCopy[index] != rightCopy[index] {
			return false
		}
	}
	return true
}

func gmailGrantedScopesMatchMVP(values []string) bool {
	normalized := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		if value == googleUserInfoEmailScope {
			value = "email"
		}
		switch value {
		case "openid", "email", gmailSendScope:
		default:
			return false
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		normalized = append(normalized, value)
	}
	return gmailStringSetsEqual(normalized, gmailConnectedMailboxScopes)
}

func gmailProviderError(operation string, kind connectoroauth.ProviderErrorKind, code string) error {
	return &connectoroauth.ProviderError{Operation: operation, Kind: kind, Code: code}
}

func gmailHTTPProviderError(operation string, status int) error {
	kind := connectoroauth.ProviderErrorRejected
	code := "provider_rejected"
	switch status {
	case http.StatusUnauthorized:
		kind, code = connectoroauth.ProviderErrorAuthorization, "unauthorized"
	case http.StatusForbidden:
		kind, code = connectoroauth.ProviderErrorAuthorization, "access_denied"
	case http.StatusTooManyRequests:
		kind, code = connectoroauth.ProviderErrorTransient, "rate_limited"
	default:
		if status >= http.StatusInternalServerError {
			kind, code = connectoroauth.ProviderErrorUncertain, "temporarily_unavailable"
		}
	}
	return gmailProviderError(operation, kind, code)
}

func gmailSafeHTTPCode(status int) string {
	switch status {
	case http.StatusUnauthorized:
		return "unauthorized"
	case http.StatusForbidden:
		return "access_denied"
	case http.StatusTooManyRequests:
		return "rate_limited"
	default:
		if status >= http.StatusInternalServerError {
			return "temporarily_unavailable"
		}
		return "provider_rejected"
	}
}

func gmailDeliveryError(kind connectordelivery.ProviderErrorKind, code string) error {
	return &connectordelivery.ProviderError{Kind: kind, Code: code}
}

var _ connectoroauth.OAuthProvider = (*GmailConnectedMailboxProvider)(nil)
var _ connectordelivery.Provider = (*GmailConnectedMailboxProvider)(nil)
