// Package connectedmailhttp contains configuration-driven HTTP adapters for
// connected mail providers. It owns provider protocol grammar, but no HTTP
// routes, durable OAuth state, delivery worker, or CRM record behavior.
package connectedmailhttp

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/mail"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"

	connector_oauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
)

const (
	maxProviderResponseBytes = 1024 * 1024
	maxEffectTermination     = time.Minute
	maxTokenLifetimeSeconds  = int64(10 * 365 * 24 * 60 * 60)
)

// URLAllowlist is supplied by the host so every provider-owned destination is
// checked by the same outbound policy as the rest of the process.
type URLAllowlist interface {
	EnsureAllowed(string) error
}

// OAuthConfig contains runtime configuration for the OAuth half of a
// connected-mail provider. ClientSecret is runtime-only secret material; the
// adapter never places it in an authorization URL or provider error.
type OAuthConfig struct {
	AuthorizationEndpoint string
	TokenEndpoint         string
	IdentityEndpoint      string
	RevocationEndpoint    string
	Issuer                string
	ClientID              string
	ClientSecret          string

	AuthorizationParameters  map[string]string
	IdentitySubjectField     string
	IdentityAddressField     string
	IdentityDisplayNameField string
	TerminationBound         time.Duration
}

// OAuthAdapter implements the provider-neutral OAuth interface with exactly
// one HTTP transaction per effect method. The injected RoundTripper must apply
// the host's dial-time address policy and honor request cancellation within
// TerminationBound, as required by OAuthProvider. Calling RoundTrip directly
// deliberately prevents net/http redirect replay of credential-bearing forms
// and Authorization headers.
type OAuthAdapter struct {
	config    OAuthConfig
	transport http.RoundTripper
	urls      URLAllowlist
}

func NewOAuthAdapter(config OAuthConfig, transport http.RoundTripper, urls URLAllowlist) (*OAuthAdapter, error) {
	if transport == nil || urls == nil || config.TerminationBound <= 0 || config.TerminationBound > maxEffectTermination ||
		!validEndpoint(config.AuthorizationEndpoint) || !validEndpoint(config.TokenEndpoint) ||
		!validEndpoint(config.IdentityEndpoint) || !validEndpoint(config.RevocationEndpoint) ||
		!validEndpoint(config.Issuer) || !credentialValue(config.ClientID, 4096) ||
		!credentialValue(config.ClientSecret, 16*1024) ||
		!validAuthorizationParameters(config.AuthorizationParameters) ||
		!fieldName(config.IdentitySubjectField) || !fieldName(config.IdentityAddressField) ||
		!fieldName(config.IdentityDisplayNameField) {
		return nil, connector_oauth.ErrInvalidInput
	}
	for _, endpoint := range []string{
		config.AuthorizationEndpoint,
		config.TokenEndpoint,
		config.IdentityEndpoint,
		config.RevocationEndpoint,
	} {
		if urls.EnsureAllowed(endpoint) != nil {
			return nil, connector_oauth.ErrInvalidInput
		}
	}
	config.AuthorizationParameters = cloneAuthorizationParameters(config.AuthorizationParameters)
	return &OAuthAdapter{config: config, transport: transport, urls: urls}, nil
}

func (adapter *OAuthAdapter) EffectTerminationBound() time.Duration {
	if adapter == nil {
		return 0
	}
	return adapter.config.TerminationBound
}

func (adapter *OAuthAdapter) AuthorizationURL(request connector_oauth.AuthorizationURLRequest) (string, error) {
	if adapter == nil || adapter.urls == nil || !plain(request.State, 4096) ||
		!pkceValue(request.CodeChallenge) || request.ChallengeMethod != "S256" ||
		!validRedirectURI(request.RedirectURI) || !validScopes(request.Scopes) {
		return "", connector_oauth.ErrInvalidInput
	}
	if adapter.urls.EnsureAllowed(adapter.config.AuthorizationEndpoint) != nil {
		return "", providerFailure("authorization", connector_oauth.ProviderErrorRejected, "provider_rejected")
	}
	endpoint, err := url.Parse(adapter.config.AuthorizationEndpoint)
	if err != nil {
		return "", connector_oauth.ErrInvalidInput
	}
	query := endpoint.Query()
	for key, value := range adapter.config.AuthorizationParameters {
		query.Set(key, value)
	}
	query.Set("client_id", adapter.config.ClientID)
	query.Set("response_type", "code")
	query.Set("redirect_uri", request.RedirectURI)
	query.Set("scope", strings.Join(normalizedScopes(request.Scopes), " "))
	query.Set("state", request.State)
	query.Set("code_challenge", request.CodeChallenge)
	query.Set("code_challenge_method", request.ChallengeMethod)
	endpoint.RawQuery = query.Encode()
	return endpoint.String(), nil
}

func (adapter *OAuthAdapter) Exchange(ctx context.Context, request connector_oauth.TokenExchangeRequest) (connector_oauth.TokenSet, error) {
	if adapter == nil || ctx == nil || !plain(request.Code, 4096) || !pkceValue(request.CodeVerifier) ||
		!validRedirectURI(request.RedirectURI) {
		return connector_oauth.TokenSet{}, connector_oauth.ErrInvalidInput
	}
	form := url.Values{
		"grant_type":    {"authorization_code"},
		"client_id":     {adapter.config.ClientID},
		"client_secret": {adapter.config.ClientSecret},
		"code":          {request.Code},
		"code_verifier": {request.CodeVerifier},
		"redirect_uri":  {request.RedirectURI},
	}
	return adapter.token(ctx, form, "exchange")
}

func (adapter *OAuthAdapter) Refresh(ctx context.Context, refreshToken string) (connector_oauth.TokenSet, error) {
	if adapter == nil || ctx == nil || !credentialValue(refreshToken, 64*1024) {
		return connector_oauth.TokenSet{}, connector_oauth.ErrInvalidInput
	}
	form := url.Values{
		"grant_type":    {"refresh_token"},
		"client_id":     {adapter.config.ClientID},
		"client_secret": {adapter.config.ClientSecret},
		"refresh_token": {refreshToken},
	}
	return adapter.token(ctx, form, "refresh")
}

func (adapter *OAuthAdapter) token(ctx context.Context, form url.Values, operation string) (connector_oauth.TokenSet, error) {
	response, err := adapter.form(ctx, adapter.config.TokenEndpoint, form, operation)
	if err != nil {
		return connector_oauth.TokenSet{}, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return connector_oauth.TokenSet{}, statusFailure(operation, response.StatusCode, true)
	}
	var payload struct {
		AccessToken  string          `json:"access_token"`
		RefreshToken string          `json:"refresh_token"`
		TokenType    string          `json:"token_type"`
		Scope        json.RawMessage `json:"scope"`
		ExpiresIn    int64           `json:"expires_in"`
	}
	if decodeLimited(response.Body, &payload) != nil || !headerValue(payload.AccessToken, 64*1024) ||
		!strings.EqualFold(strings.TrimSpace(payload.TokenType), "Bearer") ||
		(payload.RefreshToken != "" && !credentialValue(payload.RefreshToken, 64*1024)) ||
		payload.ExpiresIn < 0 || payload.ExpiresIn > maxTokenLifetimeSeconds {
		return connector_oauth.TokenSet{}, providerFailure(operation, connector_oauth.ProviderErrorUncertain, "provider_error")
	}
	scopes, err := responseScopes(payload.Scope)
	if err != nil {
		return connector_oauth.TokenSet{}, providerFailure(operation, connector_oauth.ProviderErrorUncertain, "provider_error")
	}
	var expiresAt *time.Time
	if payload.ExpiresIn > 0 {
		value := time.Now().UTC().Add(time.Duration(payload.ExpiresIn) * time.Second)
		expiresAt = &value
	}
	return connector_oauth.TokenSet{
		AccessToken: payload.AccessToken, RefreshToken: payload.RefreshToken,
		TokenType: "Bearer", Scopes: scopes, ExpiresAt: expiresAt,
	}, nil
}

func (adapter *OAuthAdapter) Identity(ctx context.Context, accessToken string) (connector_oauth.RemoteIdentity, error) {
	if adapter == nil || ctx == nil || !headerValue(accessToken, 64*1024) {
		return connector_oauth.RemoteIdentity{}, connector_oauth.ErrInvalidInput
	}
	response, err := adapter.bearer(ctx, http.MethodGet, adapter.config.IdentityEndpoint, accessToken, "identity")
	if err != nil {
		return connector_oauth.RemoteIdentity{}, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return connector_oauth.RemoteIdentity{}, statusFailure("identity", response.StatusCode, false)
	}
	values := map[string]any{}
	if decodeLimited(response.Body, &values) != nil {
		return connector_oauth.RemoteIdentity{}, providerFailure("identity", connector_oauth.ProviderErrorTransient, "provider_error")
	}
	identity := connector_oauth.RemoteIdentity{
		Issuer:           adapter.config.Issuer,
		Subject:          stringField(values, adapter.config.IdentitySubjectField),
		PrincipalAddress: stringField(values, adapter.config.IdentityAddressField),
		DisplayName:      stringField(values, adapter.config.IdentityDisplayNameField),
	}
	if !plain(identity.Subject, 4096) || !bareAddress(identity.PrincipalAddress) ||
		(identity.DisplayName != "" && !plain(identity.DisplayName, 4096)) {
		return connector_oauth.RemoteIdentity{}, providerFailure("identity", connector_oauth.ProviderErrorRejected, "provider_rejected")
	}
	return identity, nil
}

func (adapter *OAuthAdapter) Revoke(ctx context.Context, token string) error {
	if adapter == nil || ctx == nil || !credentialValue(token, 64*1024) {
		return connector_oauth.ErrInvalidInput
	}
	response, err := adapter.form(ctx, adapter.config.RevocationEndpoint, url.Values{"token": {token}}, "revoke")
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return statusFailure("revoke", response.StatusCode, true)
	}
	return nil
}

func (adapter *OAuthAdapter) form(ctx context.Context, endpoint string, form url.Values, operation string) (*http.Response, error) {
	payload := []byte(form.Encode())
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
	if err != nil {
		clearBytes(payload)
		return nil, connector_oauth.ErrInvalidInput
	}
	request.Body = &clearingRequestBody{ReadCloser: request.Body, payload: payload}
	request.GetBody = nil
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	return adapter.do(request, operation)
}

func (adapter *OAuthAdapter) bearer(ctx context.Context, method, endpoint, token, operation string) (*http.Response, error) {
	request, err := http.NewRequestWithContext(ctx, method, endpoint, nil)
	if err != nil {
		return nil, connector_oauth.ErrInvalidInput
	}
	request.Header.Set("Authorization", "Bearer "+token)
	return adapter.do(request, operation)
}

func (adapter *OAuthAdapter) do(request *http.Request, operation string) (*http.Response, error) {
	if adapter == nil || adapter.transport == nil || adapter.urls == nil || request == nil || request.URL == nil {
		closeRequestBody(request)
		return nil, connector_oauth.ErrInvalidInput
	}
	if request.Context().Err() != nil {
		closeRequestBody(request)
		return nil, providerFailure(operation, connector_oauth.ProviderErrorTransient, "transport_error")
	}
	if adapter.urls.EnsureAllowed(request.URL.String()) != nil {
		closeRequestBody(request)
		return nil, providerFailure(operation, connector_oauth.ProviderErrorRejected, "provider_rejected")
	}
	response, err := adapter.transport.RoundTrip(request)
	if err != nil || response == nil || response.Body == nil {
		if response != nil && response.Body != nil {
			response.Body.Close()
		}
		kind := connector_oauth.ProviderErrorTransient
		if operation != "identity" {
			kind = connector_oauth.ProviderErrorUncertain
		}
		return nil, providerFailure(operation, kind, "transport_error")
	}
	return response, nil
}

// clearingRequestBody follows the RoundTripper ownership contract: the
// transport may finish consuming or close Request.Body asynchronously after
// RoundTrip returns. Secret form bytes are therefore cleared by Body.Close,
// not by the caller at the RoundTrip return point.
type clearingRequestBody struct {
	io.ReadCloser
	payload []byte
	once    sync.Once
}

func (body *clearingRequestBody) Close() error {
	if body == nil {
		return nil
	}
	err := body.ReadCloser.Close()
	body.once.Do(func() { clearBytes(body.payload) })
	return err
}

func closeRequestBody(request *http.Request) {
	if request != nil && request.Body != nil {
		_ = request.Body.Close()
	}
}

func statusFailure(operation string, status int, effectful bool) error {
	if status == http.StatusUnauthorized || status == http.StatusForbidden {
		return providerFailure(operation, connector_oauth.ProviderErrorAuthorization, "unauthorized")
	}
	if status == http.StatusTooManyRequests {
		kind := connector_oauth.ProviderErrorTransient
		if effectful {
			kind = connector_oauth.ProviderErrorUncertain
		}
		return providerFailure(operation, kind, "rate_limited")
	}
	if status >= 300 && status < 400 || status >= 500 {
		kind := connector_oauth.ProviderErrorTransient
		if effectful {
			kind = connector_oauth.ProviderErrorUncertain
		}
		return providerFailure(operation, kind, "temporarily_unavailable")
	}
	return providerFailure(operation, connector_oauth.ProviderErrorRejected, "provider_rejected")
}

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

func decodeLimited(reader io.Reader, destination any) error {
	if reader == nil {
		return errors.New("provider response is missing")
	}
	raw, err := io.ReadAll(io.LimitReader(reader, maxProviderResponseBytes+1))
	if err != nil {
		return err
	}
	defer clearBytes(raw)
	if len(raw) > maxProviderResponseBytes {
		return errors.New("provider response is too large")
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	if err := decoder.Decode(destination); err != nil {
		return err
	}
	if decoder.Decode(&struct{}{}) != io.EOF {
		return errors.New("provider response has trailing data")
	}
	return nil
}

func responseScopes(raw json.RawMessage) ([]string, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		return nil, connector_oauth.ErrInvalidInput
	}
	var value string
	if err := json.Unmarshal(raw, &value); err != nil {
		return nil, err
	}
	if strings.TrimSpace(value) == "" {
		return []string{}, nil
	}
	scopes := strings.Fields(value)
	if !validScopes(scopes) {
		return nil, connector_oauth.ErrInvalidInput
	}
	return normalizedScopes(scopes), nil
}

func normalizedScopes(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, duplicate := seen[value]; duplicate {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}

func validScopes(values []string) bool {
	if len(values) == 0 || len(values) > 64 {
		return false
	}
	for _, value := range values {
		if !plain(value, 2048) || len(strings.Fields(value)) != 1 {
			return false
		}
	}
	return true
}

func validAuthorizationParameters(values map[string]string) bool {
	if len(values) > 2 {
		return false
	}
	for key, value := range values {
		switch key {
		case "access_type":
			if value != "offline" && value != "online" {
				return false
			}
		case "prompt":
			parts := strings.Fields(value)
			if len(parts) == 0 || strings.Join(parts, " ") != value {
				return false
			}
			seen := make(map[string]struct{}, len(parts))
			for _, part := range parts {
				switch part {
				case "none", "login", "consent", "select_account":
				default:
					return false
				}
				if _, duplicate := seen[part]; duplicate || part == "none" && len(parts) != 1 {
					return false
				}
				seen[part] = struct{}{}
			}
		default:
			return false
		}
	}
	return true
}

func cloneAuthorizationParameters(values map[string]string) map[string]string {
	if len(values) == 0 {
		return nil
	}
	cloned := make(map[string]string, len(values))
	for key, value := range values {
		cloned[key] = value
	}
	return cloned
}

func validEndpoint(value string) bool {
	parsed, err := url.Parse(value)
	return err == nil && parsed.Scheme == "https" && parsed.User == nil && parsed.Host != "" &&
		parsed.RawQuery == "" && parsed.Fragment == "" && parsed.Opaque == ""
}

func validRedirectURI(value string) bool {
	parsed, err := url.Parse(value)
	if err != nil || parsed.User != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" {
		return false
	}
	if parsed.Scheme == "https" {
		return true
	}
	host := parsed.Hostname()
	return parsed.Scheme == "http" && (host == "127.0.0.1" || host == "::1" || host == "localhost")
}

func fieldName(value string) bool {
	if !plain(value, 128) {
		return false
	}
	for _, current := range value {
		if !((current >= 'a' && current <= 'z') || (current >= 'A' && current <= 'Z') ||
			(current >= '0' && current <= '9') || current == '_' || current == '-') {
			return false
		}
	}
	return true
}

func pkceValue(value string) bool {
	if len(value) < 43 || len(value) > 128 {
		return false
	}
	for _, current := range value {
		if !((current >= 'a' && current <= 'z') || (current >= 'A' && current <= 'Z') ||
			(current >= '0' && current <= '9') || strings.ContainsRune("-._~", current)) {
			return false
		}
	}
	return true
}

func plain(value string, maximum int) bool {
	return value != "" && len(value) <= maximum && value == strings.TrimSpace(value) &&
		!strings.ContainsAny(value, "\x00\r\n")
}

func credentialValue(value string, maximum int) bool {
	return plain(value, maximum)
}

func headerValue(value string, maximum int) bool {
	if !plain(value, maximum) {
		return false
	}
	for _, current := range value {
		if current < 0x21 || current > 0x7e {
			return false
		}
	}
	return true
}

func stringField(values map[string]any, field string) string {
	value, _ := values[field].(string)
	return strings.TrimSpace(value)
}

func bareAddress(value string) bool {
	parsed, err := mail.ParseAddress(value)
	return err == nil && parsed.Name == "" && parsed.Address == value && len(value) <= 320
}

func clearBytes(value []byte) {
	for index := range value {
		value[index] = 0
	}
}

var _ connector_oauth.OAuthProvider = (*OAuthAdapter)(nil)
