package integrationservice

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
	connectoroauth "github.com/pingcap-inc/nexuscrm-connectors/features/connector/oauth"
	mailboxauthorizationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/mailboxauthorizationv1"
)

type connectedMailboxAuthorizationRuntime interface {
	AuthorizeConnectedMailbox(
		context.Context,
		string,
		mailboxauthorizationv1.Request,
	) (mailboxauthorizationv1.Response, error)
	ReauthorizeConnectedMailbox(
		context.Context,
		mailboxauthorizationv1.ReauthorizeRequest,
	) (mailboxauthorizationv1.Response, error)
}

type connectedMailOAuth interface {
	BeginAuthorization(
		context.Context,
		connectoroauth.BeginAuthorizationRequest,
	) (connectoroauth.AuthorizationStart, error)
	CompleteAuthorization(
		context.Context,
		connectoroauth.CompleteAuthorizationRequest,
	) (connectoroauth.AuthorizationCompletion, error)
	Principal(context.Context, string, string) (*connectoroauth.ConnectedPrincipal, error)
	Disconnect(context.Context, string, string) error
}

// connectedMailHandler is the Integration-owned browser surface. Runtime
// decides which user may act; the provider adapter and token store remain
// private to Integration.
type connectedMailHandler struct {
	publicPath            string
	connectorKey          string
	callbackURI           string
	returnBaseURL         string
	authorizationEndpoint string
	requiredScopes        []string
	catalogRevision       uint64
	runtime               connectedMailboxAuthorizationRuntime
	oauth                 connectedMailOAuth
	state                 *mailboxStateCodec
	scopeDigest           string
}

func newConnectedMailHandler(
	config *ConnectedMailConfig,
	connectorKey string,
	catalogRevision uint64,
	runtime connectedMailboxAuthorizationRuntime,
	oauth connectedMailOAuth,
	state *mailboxStateCodec,
) (*connectedMailHandler, error) {
	if config == nil || !connectedMailValue(connectorKey, 255) || catalogRevision == 0 ||
		runtime == nil || oauth == nil || state == nil ||
		!validConnectedMailPublicPath(config.Definition.PublicPath) ||
		!validConnectedMailPublicURL(config.CallbackURI, false) ||
		!validConnectedMailPublicURL(config.ReturnBaseURL, true) ||
		!validConnectedMailEndpoint(config.Definition.AuthorizationEndpoint) ||
		len(config.Definition.RequiredScopes) == 0 {
		return nil, errors.New("integration service: connected-mail HTTP composition is invalid")
	}
	callback, err := url.Parse(config.CallbackURI)
	if err != nil || callback.Path != config.Definition.PublicPath+"/callback" {
		return nil, errors.New("integration service: connected-mail HTTP composition is invalid")
	}
	scopes := append([]string(nil), config.Definition.RequiredScopes...)
	return &connectedMailHandler{
		publicPath: config.Definition.PublicPath, connectorKey: connectorKey,
		callbackURI: config.CallbackURI, returnBaseURL: config.ReturnBaseURL,
		authorizationEndpoint: config.Definition.AuthorizationEndpoint,
		requiredScopes:        scopes,
		catalogRevision:       catalogRevision,
		runtime:               runtime,
		oauth:                 oauth,
		state:                 state,
		scopeDigest:           mailboxScopeDigest(scopes),
	}, nil
}

func (handler *connectedMailHandler) RegisterRoutes(engine *gin.Engine) {
	group := engine.Group(handler.publicPath)
	group.GET("", handler.status)
	group.GET("/authorize", handler.authorize)
	group.GET("/callback", handler.callback)
	group.DELETE("", handler.disconnect)
}

func (handler *connectedMailHandler) status(c *gin.Context) {
	authorized, ok := handler.authorizeDelegated(c, mailboxauthorizationv1.OperationStatus)
	if !ok {
		return
	}
	principal, err := handler.oauth.Principal(
		c.Request.Context(),
		authorized.AuthorizedSubjectID,
		handler.connectorKey,
	)
	if errors.Is(err, connectoroauth.ErrPrincipalNotFound) {
		c.JSON(http.StatusOK, gin.H{"connected": false})
		return
	}
	if err != nil || principal == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connected mailbox status is unavailable"})
		return
	}
	if principal.State != connectoroauth.PrincipalConnected {
		c.JSON(http.StatusOK, gin.H{"connected": false})
		return
	}
	if principal.UserID != authorized.AuthorizedSubjectID ||
		principal.ConnectorKey != handler.connectorKey ||
		principal.Generation <= 0 || !connectedMailValue(principal.PrincipalAddress, 4096) {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connected mailbox status is unavailable"})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"connected":        true,
		"principalAddress": principal.PrincipalAddress,
	})
}

func (handler *connectedMailHandler) authorize(c *gin.Context) {
	authorized, ok := handler.authorizeDelegated(c, mailboxauthorizationv1.OperationBegin)
	if !ok {
		return
	}
	start, err := handler.oauth.BeginAuthorization(
		c.Request.Context(),
		connectoroauth.BeginAuthorizationRequest{
			UserID: authorized.AuthorizedSubjectID, ConnectorKey: handler.connectorKey,
			RedirectURI: handler.callbackURI,
			Scopes:      append([]string(nil), handler.requiredScopes...),
		},
	)
	if err != nil || start.Outcome != connectoroauth.AuthorizationOutcomeRedirect ||
		start.URL == "" || authorized.ReauthorizationHandle == "" {
		c.JSON(http.StatusConflict, gin.H{"error": "connected mailbox authorization could not start"})
		return
	}
	providerURL, err := url.Parse(start.URL)
	if err != nil || !handler.matchesAuthorizationEndpoint(providerURL) {
		c.JSON(http.StatusConflict, gin.H{"error": "connected mailbox authorization could not start"})
		return
	}
	query := providerURL.Query()
	internalState, ok := singleMailboxQueryValue(query, "state", true)
	if !ok || !boundedMailboxStateValue(internalState, 2048) {
		c.JSON(http.StatusConflict, gin.H{"error": "connected mailbox authorization could not start"})
		return
	}
	returnTarget, returnTargetOK := singleMailboxQueryValue(c.Request.URL.Query(), "returnTo", false)
	if !returnTargetOK {
		returnTarget = ""
	}
	wrapper, err := handler.state.seal(mailboxState{
		OAuthState: internalState, AuthorizedSubjectID: authorized.AuthorizedSubjectID,
		ReauthorizationHandle: authorized.ReauthorizationHandle,
		ConnectorKey:          handler.connectorKey, CallbackURI: handler.callbackURI,
		ScopeDigest: handler.scopeDigest, ReturnTarget: safeMailboxReturnTarget(returnTarget),
		ExpiresAt: start.ExpiresAt,
	})
	if err != nil {
		c.JSON(http.StatusConflict, gin.H{"error": "connected mailbox authorization could not start"})
		return
	}
	query.Set("state", wrapper)
	providerURL.RawQuery = query.Encode()
	c.JSON(http.StatusOK, gin.H{"authorizationUrl": providerURL.String()})
}

func (handler *connectedMailHandler) callback(c *gin.Context) {
	query := c.Request.URL.Query()
	encodedState, ok := singleMailboxQueryValue(query, "state", true)
	if !ok {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "connected mailbox callback authorization is invalid"})
		return
	}
	state, err := handler.state.open(encodedState)
	if err != nil || state.ConnectorKey != handler.connectorKey ||
		state.CallbackURI != handler.callbackURI || state.ScopeDigest != handler.scopeDigest {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "connected mailbox callback authorization is invalid"})
		return
	}
	providerError, errorOK := singleMailboxQueryValue(query, "error", false)
	code, codeOK := singleMailboxQueryValue(query, "code", true)
	if !errorOK || providerError != "" || !codeOK || !connectedMailSecretValue(code, 8*1024) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "connected mailbox callback is invalid"})
		return
	}
	minimum := handler.catalogRevision
	reauthorized, err := handler.runtime.ReauthorizeConnectedMailbox(
		c.Request.Context(),
		mailboxauthorizationv1.ReauthorizeRequest{
			ReauthorizationHandle: state.ReauthorizationHandle,
			MinCatalogRevision:    &minimum,
		},
	)
	if err != nil || reauthorized.AuthorizedSubjectID != state.AuthorizedSubjectID ||
		reauthorized.Receipts.CatalogRevision != handler.catalogRevision {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connected mailbox callback authorization is unavailable"})
		return
	}
	completion, err := handler.oauth.CompleteAuthorization(
		c.Request.Context(),
		connectoroauth.CompleteAuthorizationRequest{
			UserID: state.AuthorizedSubjectID, State: state.OAuthState, Code: code,
		},
	)
	if err != nil || completion.Outcome != connectoroauth.AuthorizationCompletionResultStaged {
		c.JSON(http.StatusBadGateway, gin.H{"error": "connected mailbox authorization failed"})
		return
	}
	resumed, err := handler.oauth.BeginAuthorization(
		c.Request.Context(),
		connectoroauth.BeginAuthorizationRequest{
			UserID: state.AuthorizedSubjectID, ConnectorKey: handler.connectorKey,
			RedirectURI: handler.callbackURI,
			Scopes:      append([]string(nil), handler.requiredScopes...),
		},
	)
	if err != nil || resumed.Outcome != connectoroauth.AuthorizationOutcomeConnected ||
		resumed.Principal == nil || resumed.Principal.UserID != state.AuthorizedSubjectID ||
		resumed.Principal.ConnectorKey != handler.connectorKey ||
		resumed.Principal.Generation <= 0 ||
		resumed.Principal.State != connectoroauth.PrincipalConnected ||
		!connectedMailValue(resumed.Principal.PrincipalAddress, 4096) {
		c.JSON(http.StatusBadGateway, gin.H{"error": "connected mailbox authorization requires recovery"})
		return
	}
	returnURL, err := handler.returnURL(state.ReturnTarget)
	if err != nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connected mailbox return is unavailable"})
		return
	}
	c.Redirect(http.StatusFound, returnURL)
}

func (handler *connectedMailHandler) disconnect(c *gin.Context) {
	authorized, ok := handler.authorizeDelegated(c, mailboxauthorizationv1.OperationDisconnect)
	if !ok {
		return
	}
	err := handler.oauth.Disconnect(
		c.Request.Context(),
		authorized.AuthorizedSubjectID,
		handler.connectorKey,
	)
	if err != nil && !errors.Is(err, connectoroauth.ErrPrincipalNotFound) &&
		!errors.Is(err, connectoroauth.ErrManualActionRequired) {
		c.JSON(http.StatusConflict, gin.H{"error": "connected mailbox could not be disconnected"})
		return
	}
	c.Status(http.StatusNoContent)
}

func (handler *connectedMailHandler) authorizeDelegated(
	c *gin.Context,
	operation mailboxauthorizationv1.Operation,
) (mailboxauthorizationv1.Response, bool) {
	bearer, ok := delegatedBearer(c.GetHeader("Authorization"))
	if !ok {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
		return mailboxauthorizationv1.Response{}, false
	}
	minimum := handler.catalogRevision
	response, err := handler.runtime.AuthorizeConnectedMailbox(
		c.Request.Context(),
		bearer,
		mailboxauthorizationv1.Request{
			Operation: operation, MinCatalogRevision: &minimum,
		},
	)
	if err != nil {
		c.JSON(http.StatusForbidden, gin.H{"error": "connected mailbox operation is not authorized"})
		return mailboxauthorizationv1.Response{}, false
	}
	if response.Receipts.CatalogRevision != handler.catalogRevision ||
		response.AuthorizedSubjectID == "" ||
		(operation == mailboxauthorizationv1.OperationBegin &&
			response.ReauthorizationHandle == "") {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "connected mailbox authority is unavailable"})
		return mailboxauthorizationv1.Response{}, false
	}
	return response, true
}

func delegatedBearer(header string) (string, bool) {
	if !strings.HasPrefix(header, "Bearer ") {
		return "", false
	}
	value := strings.TrimPrefix(header, "Bearer ")
	return value, value != "" && len(value) <= 16*1024 && value == strings.TrimSpace(value) &&
		!strings.ContainsAny(value, "\x00\r\n ")
}

func singleMailboxQueryValue(values url.Values, name string, required bool) (string, bool) {
	selected, present := values[name]
	if !present {
		return "", !required
	}
	if len(selected) != 1 {
		return "", false
	}
	return selected[0], !required || selected[0] != ""
}

func mailboxScopeDigest(scopes []string) string {
	sum := sha256.Sum256([]byte(strings.Join(scopes, "\n")))
	return "sha256:" + hex.EncodeToString(sum[:])
}

func safeMailboxReturnTarget(value string) string {
	value = strings.TrimSpace(value)
	if !validMailboxReturnTarget(value) {
		return "/"
	}
	return value
}

func (handler *connectedMailHandler) matchesAuthorizationEndpoint(candidate *url.URL) bool {
	expected, err := url.Parse(handler.authorizationEndpoint)
	return err == nil && candidate != nil && candidate.User == nil && candidate.Fragment == "" &&
		candidate.Opaque == "" && candidate.Scheme == expected.Scheme &&
		candidate.Host == expected.Host && candidate.EscapedPath() == expected.EscapedPath()
}

func (handler *connectedMailHandler) returnURL(target string) (string, error) {
	base, err := url.Parse(handler.returnBaseURL)
	if err != nil || !validMailboxReturnTarget(target) {
		return "", errMailboxStateInvalid
	}
	relative, err := url.Parse(target)
	if err != nil {
		return "", errMailboxStateInvalid
	}
	resolved := base.ResolveReference(relative)
	if resolved.Scheme != base.Scheme || resolved.Host != base.Host || resolved.User != nil {
		return "", errMailboxStateInvalid
	}
	return resolved.String(), nil
}
