package app

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	storeapp "github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

const (
	appNotificationRequestBodyLimit = int64(512 * 1024)
	appNotificationActionBodyLimit  = int64(64 * 1024)
)

type AppNotificationInternalHandler struct {
	store             Store
	managedWorkspaces owner.ManagedWorkspaceSet
}

func (handler *AppRuntimeHandler) NotificationAction(ctx *gin.Context) {
	notificationID, action, err := api.ParseActionSegment(ctx.Param("notificationAction"), "notificationId")
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.Params = append(ctx.Params, gin.Param{Key: "notificationId", Value: notificationID})
	switch action {
	case "resolve":
		handler.NotificationResolve(ctx)
	case "withdraw":
		handler.NotificationWithdraw(ctx)
	default:
		api.Error(ctx, http.StatusBadRequest, "Unsupported App notification action")
	}
}

// NotificationCreate handles POST /api/v1/workspaces/:workspaceId/app-runtime/notifications.
func (handler *AppRuntimeHandler) NotificationCreate(ctx *gin.Context) {
	authenticated, ok := handler.authenticateNotificationRuntime(ctx, storeapp.AppRuntimeScopeMessageWrite, storeapp.AppNotificationScopeWrite)
	if !ok {
		return
	}
	var request dto.AppNotificationCreateRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	result, err := handler.store.CreateAppNotificationForRuntime(ctx.Request.Context(), authenticated, appNotificationCommandFromDTO(request))
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

// NotificationGet handles GET /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId.
func (handler *AppRuntimeHandler) NotificationGet(ctx *gin.Context) {
	authenticated, ok := handler.authenticateNotificationRuntime(ctx, storeapp.AppNotificationScopeWrite)
	if !ok {
		return
	}
	result, err := handler.store.GetAppNotificationForRuntime(ctx.Request.Context(), authenticated, ctx.Param("notificationId"))
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

// NotificationUpdate handles PATCH /api/v1/workspaces/:workspaceId/app-runtime/notifications/:notificationId.
func (handler *AppRuntimeHandler) NotificationUpdate(ctx *gin.Context) {
	if !requireAppNotificationRuntimeBearer(ctx) {
		return
	}
	var request dto.AppNotificationMutationRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	handler.mutateNotification(ctx, request, storeapp.AppNotificationMutationUpdate)
}

// NotificationResolve handles POST .../notifications/:notificationId:resolve.
func (handler *AppRuntimeHandler) NotificationResolve(ctx *gin.Context) {
	if !requireAppNotificationRuntimeBearer(ctx) {
		return
	}
	var request dto.AppNotificationMutationRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	handler.mutateNotification(ctx, request, storeapp.AppNotificationMutationResolve)
}

// NotificationWithdraw handles POST .../notifications/:notificationId:withdraw.
func (handler *AppRuntimeHandler) NotificationWithdraw(ctx *gin.Context) {
	if !requireAppNotificationRuntimeBearer(ctx) {
		return
	}
	var request dto.AppNotificationWithdrawRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	mutation := storeapp.AppNotificationMutationCommand{
		NotificationID:         ctx.Param("notificationId"),
		Kind:                   storeapp.AppNotificationMutationWithdraw,
		ExpectedRevision:       request.ExpectedRevision,
		ClientMutationID:       request.ClientMutationID,
		Reason:                 request.Reason,
		CausedByActor:          appNotificationActorFromDTO(request.CausedByActor),
		CorrelationID:          request.CorrelationID,
		ServiceEventDeliveryID: request.ServiceEventDeliveryID,
		DeliveryAttemptID:      request.AttemptID,
	}
	handler.applyRuntimeNotificationMutation(ctx, mutation)
}

func (handler *AppRuntimeHandler) mutateNotification(ctx *gin.Context, request dto.AppNotificationMutationRequest, kind string) {
	mutation := appNotificationMutationFromDTO(request)
	mutation.NotificationID = ctx.Param("notificationId")
	mutation.Kind = kind
	handler.applyRuntimeNotificationMutation(ctx, mutation)
}

func (handler *AppRuntimeHandler) applyRuntimeNotificationMutation(ctx *gin.Context, mutation storeapp.AppNotificationMutationCommand) {
	var result storeapp.AppNotificationResult
	var err error
	deliveryID := strings.TrimSpace(mutation.ServiceEventDeliveryID)
	attemptID := strings.TrimSpace(mutation.DeliveryAttemptID)
	if (deliveryID == "") != (attemptID == "") {
		api.Error(ctx, http.StatusBadRequest, "serviceEventDeliveryId and attemptId are required together")
		return
	}
	if deliveryID != "" {
		bearer, ok := bearerTokenFromRequest(ctx)
		if !ok {
			api.Error(ctx, http.StatusUnauthorized, "Missing bearer token")
			return
		}
		deliveryAuth, authErr := handler.store.AuthenticateAppRuntimeDelivery(ctx.Request.Context(), bearer, deliveryID, attemptID)
		if authErr != nil {
			api.StoreError(ctx, authErr)
			return
		}
		if !api.EnsureWorkspaceOwner(ctx, handler.managedWorkspaces, deliveryAuth.WorkspaceID) || !ensureRuntimeRouteWorkspace(ctx, deliveryAuth.WorkspaceID) {
			return
		}
		result, err = handler.store.MutateAppNotificationForDelivery(ctx.Request.Context(), deliveryAuth, mutation)
	} else {
		authenticated, ok := handler.authenticateNotificationRuntime(ctx, storeapp.AppRuntimeScopeMessageWrite, storeapp.AppNotificationScopeWrite)
		if !ok {
			return
		}
		result, err = handler.store.MutateAppNotificationForRuntime(ctx.Request.Context(), authenticated, mutation)
	}
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

func (handler *AppRuntimeHandler) authenticateNotificationRuntime(ctx *gin.Context, requiredScopes ...string) (storeapp.AppNotificationRuntimePrincipal, bool) {
	bearerToken, ok := bearerTokenFromRequest(ctx)
	if !ok {
		api.Error(ctx, http.StatusUnauthorized, "Missing bearer token")
		return storeapp.AppNotificationRuntimePrincipal{}, false
	}
	authenticated, err := handler.store.AuthenticateAppNotificationRuntime(ctx.Request.Context(), bearerToken, requiredScopes)
	switch {
	case err == nil:
		if !api.EnsureWorkspaceOwner(ctx, handler.managedWorkspaces, authenticated.WorkspaceID) {
			return storeapp.AppNotificationRuntimePrincipal{}, false
		}
		if !ensureRuntimeRouteWorkspace(ctx, authenticated.WorkspaceID) {
			return storeapp.AppNotificationRuntimePrincipal{}, false
		}
		return authenticated, true
	case errors.Is(err, api.ErrUnauthorized):
		api.Error(ctx, http.StatusUnauthorized, "Invalid App notification runtime token")
	case errors.Is(err, api.ErrForbidden):
		api.Error(ctx, http.StatusForbidden, "Forbidden")
	default:
		api.StoreError(ctx, err)
	}
	return storeapp.AppNotificationRuntimePrincipal{}, false
}

// Internal service ingress.

func (handler *AppNotificationInternalHandler) Action(ctx *gin.Context) {
	notificationID, action, err := api.ParseActionSegment(ctx.Param("notificationAction"), "notificationId")
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.Params = append(ctx.Params, gin.Param{Key: "notificationId", Value: notificationID})
	switch action {
	case "resolve":
		handler.Resolve(ctx)
	case "withdraw":
		handler.Withdraw(ctx)
	default:
		api.Error(ctx, http.StatusBadRequest, "Unsupported App notification action")
	}
}

func (handler *AppNotificationInternalHandler) Create(ctx *gin.Context) {
	principal, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppNotificationCreateRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	result, err := handler.store.CreateAppNotificationForInternal(ctx.Request.Context(), principal, appNotificationCommandFromDTO(request))
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

func (handler *AppNotificationInternalHandler) Get(ctx *gin.Context) {
	principal, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	result, err := handler.store.GetAppNotificationForInternal(ctx.Request.Context(), principal, ctx.Param("notificationId"))
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

func (handler *AppNotificationInternalHandler) Update(ctx *gin.Context) {
	principal, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppNotificationMutationRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	handler.mutateAuthenticated(ctx, principal, appNotificationMutationFromDTO(request), storeapp.AppNotificationMutationUpdate)
}

func (handler *AppNotificationInternalHandler) Resolve(ctx *gin.Context) {
	principal, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppNotificationMutationRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	handler.mutateAuthenticated(ctx, principal, appNotificationMutationFromDTO(request), storeapp.AppNotificationMutationResolve)
}

func (handler *AppNotificationInternalHandler) Withdraw(ctx *gin.Context) {
	principal, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppNotificationWithdrawRequest
	if !bindAppNotificationJSON(ctx, appNotificationRequestBodyLimit, &request) {
		return
	}
	handler.mutateAuthenticated(ctx, principal, storeapp.AppNotificationMutationCommand{
		ExpectedRevision: request.ExpectedRevision, ClientMutationID: request.ClientMutationID,
		Reason: request.Reason, CausedByActor: appNotificationActorFromDTO(request.CausedByActor),
		CorrelationID: request.CorrelationID,
	}, storeapp.AppNotificationMutationWithdraw)
}

func (handler *AppNotificationInternalHandler) mutateAuthenticated(ctx *gin.Context, principal storeapp.AppNotificationInternalPrincipal, mutation storeapp.AppNotificationMutationCommand, kind string) {
	mutation.NotificationID = ctx.Param("notificationId")
	mutation.Kind = kind
	result, err := handler.store.MutateAppNotificationForInternal(ctx.Request.Context(), principal, mutation)
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppNotificationResponse(result))
}

func requireAppNotificationRuntimeBearer(ctx *gin.Context) bool {
	if _, ok := bearerTokenFromRequest(ctx); !ok {
		api.Error(ctx, http.StatusUnauthorized, "Missing bearer token")
		return false
	}
	return true
}

func bindAppNotificationJSON(ctx *gin.Context, limit int64, destination any) bool {
	ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, limit)
	decoder := json.NewDecoder(ctx.Request.Body)
	decoder.UseNumber()
	if err := decoder.Decode(destination); err != nil {
		status := http.StatusBadRequest
		var maxBytesError *http.MaxBytesError
		if errors.As(err, &maxBytesError) {
			status = http.StatusRequestEntityTooLarge
		}
		api.Error(ctx, status, "Invalid request body")
		return false
	}
	if err := ensureAppNotificationRequestEOF(decoder); err != nil {
		api.Error(ctx, http.StatusBadRequest, "Invalid request body")
		return false
	}
	return true
}

func ensureAppNotificationRequestEOF(decoder *json.Decoder) error {
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return errors.New("unexpected trailing JSON document")
		}
		return err
	}
	return nil
}

func (handler *AppNotificationInternalHandler) authenticate(ctx *gin.Context) (storeapp.AppNotificationInternalPrincipal, bool) {
	raw := strings.TrimSpace(ctx.GetHeader("Authorization"))
	parts := strings.Fields(raw)
	if len(parts) != 2 || !strings.EqualFold(parts[0], "Internal") || strings.TrimSpace(parts[1]) == "" {
		api.Error(ctx, http.StatusUnauthorized, "Missing internal service credential")
		return storeapp.AppNotificationInternalPrincipal{}, false
	}
	principal, err := handler.store.AuthenticateAppNotificationInternalService(ctx.Request.Context(), ctx.Param("workspaceId"), parts[1])
	if err != nil {
		api.StoreError(ctx, err)
		return storeapp.AppNotificationInternalPrincipal{}, false
	}
	if !api.EnsureWorkspaceOwner(ctx, handler.managedWorkspaces, principal.WorkspaceID) {
		return storeapp.AppNotificationInternalPrincipal{}, false
	}
	return principal, true
}

// Human action route.

func (handler *WorkspaceAppHandler) AppNotificationCardGet(request api.WorkspaceScopedRequest) (api.Response, error) {
	view, err := handler.store.GetAppNotificationCardForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		request.Gin.Param("messageId"),
	)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppNotificationCardResponse(view)), nil
}

func (handler *WorkspaceAppHandler) AppNotificationActionCreate(request api.WorkspaceScopedJSON[dto.AppNotificationActionRequest]) (api.Response, error) {
	result, err := handler.store.InvokeAppNotificationActionForUser(request.Context(), request.UserID(), request.WorkspaceID(), storeapp.AppNotificationActionCommand{
		MessageID: request.Gin.Param("messageId"), ActionKey: request.Gin.Param("actionKey"),
		CardRevision: request.Body.CardRevision, ClientActionID: request.Body.ClientActionID, Input: request.Body.Input,
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppNotificationActionResponse(result)), nil
}

func appNotificationCommandFromDTO(request dto.AppNotificationCreateRequest) storeapp.AppNotificationCommand {
	command := storeapp.AppNotificationCommand{
		ClientNotificationID: request.ClientNotificationID,
		Target:               storeapp.AppNotificationTarget{Kind: request.Target.Kind, MemberID: request.Target.MemberID, ScopeID: request.Target.ScopeID},
		Content:              appNotificationContentFromDTO(request.Content),
		CausedByActor:        appNotificationActorFromDTO(request.CausedByActor),
		ExpiresAt:            request.ExpiresAt, CorrelationID: request.CorrelationID,
	}
	if request.BusinessObject != nil {
		command.BusinessObject = storeapp.AppNotificationBusinessObject{
			Namespace: request.BusinessObject.Namespace, Type: request.BusinessObject.Type,
			ID: request.BusinessObject.ID, Version: request.BusinessObject.Version, URL: request.BusinessObject.URL,
		}
	}
	if request.RecipientBasis != nil {
		command.RecipientBasis = storeapp.AppNotificationRecipientBasis{Kind: request.RecipientBasis.Kind, Ref: request.RecipientBasis.Ref}
	}
	return command
}

func appNotificationMutationFromDTO(request dto.AppNotificationMutationRequest) storeapp.AppNotificationMutationCommand {
	command := storeapp.AppNotificationMutationCommand{
		ExpectedRevision: request.ExpectedRevision, ClientMutationID: request.ClientMutationID,
		BusinessObjectVersion: request.BusinessObjectVersion, CausedByActor: appNotificationActorFromDTO(request.CausedByActor),
		CorrelationID: request.CorrelationID, ServiceEventDeliveryID: request.ServiceEventDeliveryID,
		DeliveryAttemptID: request.AttemptID,
	}
	if request.Content != nil {
		content := appNotificationContentFromDTO(*request.Content)
		command.Content = &content
	}
	return command
}

func appNotificationContentFromDTO(content dto.AppNotificationContentRequest) storeapp.AppNotificationContent {
	return storeapp.AppNotificationContent{
		Kind: content.Kind, Text: content.Text, FallbackText: content.FallbackText,
		CardJSON: append([]byte(nil), content.Card...), AttachmentIDs: content.AttachmentIDs, Metadata: content.Metadata,
	}
}

func appNotificationActorFromDTO(actor *dto.AppNotificationActorRefRequest) storeapp.AppNotificationActorRef {
	if actor == nil {
		return storeapp.AppNotificationActorRef{}
	}
	return storeapp.AppNotificationActorRef{Type: actor.Type, ID: actor.ID}
}
