package app

import (
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"time"

	"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"
	"github.com/tidbcloud/AgentTeam/internal/service/appruntime"
	storeapp "github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

type AppRuntimeHandler struct {
	store             AppRuntimeStore
	runner            AppRunnerService
	managedWorkspaces owner.ManagedWorkspaceSet
}

// AppRunnerCredentialsList handles GET /api/v1/workspaces/:workspaceId/app-installations/:installationId/runner-credentials.
func (handler *WorkspaceAppHandler) AppRunnerCredentialsList(request api.WorkspaceScopedRequest) (api.Response, error) {
	installationID := request.Gin.Param("installationId")
	credentials, err := handler.store.ListAppRunnerCredentialsForUser(request.Context(), request.UserID(), request.WorkspaceID(), installationID)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppRunnerCredentialResponses(credentials)), nil
}

// AppRunnerCredentialCreate handles POST /api/v1/workspaces/:workspaceId/app-installations/:installationId/runner-credentials.
func (handler *WorkspaceAppHandler) AppRunnerCredentialCreate(request api.WorkspaceScopedJSON[dto.AppRunnerCredentialCreateRequest]) (api.Response, error) {
	installationID := request.Gin.Param("installationId")
	result, err := handler.store.CreateAppRunnerCredentialForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		installationID,
		storeapp.AppRunnerCredentialCreateParams{Name: request.Body.Name, ExpiresAt: request.Body.ExpiresAt, Metadata: request.Body.Metadata},
	)
	if err != nil {
		return api.Response{}, err
	}
	return noStoreOK(converter.ToAppRunnerCredentialTokenResponse(result)), nil
}

// AppRunnerCredentialDelete handles DELETE /api/v1/workspaces/:workspaceId/runner-credentials/:credentialId.
func (handler *WorkspaceAppHandler) AppRunnerCredentialDelete(request api.WorkspaceScopedRequest) (api.Response, error) {
	credentialID := request.Gin.Param("credentialId")
	credential, err := handler.store.DisableAppRunnerCredentialForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		credentialID,
	)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppRunnerCredentialResponse(credential)), nil
}

// AppCallbackSecretRotate handles POST
// /api/v1/workspaces/:workspaceId/app-installations/:installationId/callback-secret:rotate.
// The plaintext is returned by this mutation exactly once and is not exposed
// by any ordinary App read surface.
func (handler *WorkspaceAppHandler) AppCallbackSecretRotate(request api.WorkspaceScopedRequest) (api.Response, error) {
	result, err := handler.store.RotateAppCallbackSecretForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		request.Gin.Param("installationId"),
	)
	if err != nil {
		return api.Response{}, err
	}
	return noStoreOK(converter.ToAppCallbackSecretResponse(result)), nil
}

// AppInitialCredentialsStatus handles the secret-free setup status read for
// one installed App.
func (handler *WorkspaceAppHandler) AppInitialCredentialsStatus(request api.WorkspaceScopedRequest) (api.Response, error) {
	result, err := handler.store.GetAppInitialCredentialsStatusForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		request.Gin.Param("installationId"),
	)
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(converter.ToAppInitialCredentialsStatusResponse(result)), nil
}

// AppInitialCredentialsClaim issues the initial credential set. The plaintext
// response must never be cached.
func (handler *WorkspaceAppHandler) AppInitialCredentialsClaim(request api.WorkspaceScopedRequest) (api.Response, error) {
	result, err := handler.store.ClaimAppInitialCredentialsForUser(
		request.Context(),
		request.UserID(),
		request.WorkspaceID(),
		request.Gin.Param("installationId"),
	)
	if err != nil {
		return api.Response{}, err
	}
	return noStoreOK(converter.ToAppInitialCredentialsClaimResponse(result)), nil
}

// Heartbeat handles POST /api/v1/workspaces/:workspaceId/app-runtime/heartbeat.
func (handler *AppRuntimeHandler) Heartbeat(ctx *gin.Context) {
	authenticated, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppRunnerHeartbeatRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		appRuntimeJSONError(ctx, err)
		return
	}
	session, err := handler.runner.Heartbeat(ctx.Request.Context(), authenticated, appruntime.RunnerHeartbeatParams{
		RunnerID:      request.RunnerID,
		RunnerVersion: request.RunnerVersion,
		Capabilities:  request.Capabilities,
		Metadata:      request.Metadata,
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRunnerSessionResponse(session))
}

// Claim handles POST /api/v1/workspaces/:workspaceId/app-runtime/events:claim.
func (handler *AppRuntimeHandler) Claim(ctx *gin.Context) {
	authenticated, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	var request dto.AppRunnerClaimRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		appRuntimeJSONError(ctx, err)
		return
	}
	result, err := handler.runner.ClaimDeliveries(ctx.Request.Context(), authenticated, appruntime.RunnerClaimParams{
		RunnerID:      request.RunnerID,
		RunnerVersion: request.RunnerVersion,
		Capabilities:  request.Capabilities,
		Metadata:      request.Metadata,
		Limit:         request.Limit,
		LeaseDuration: time.Duration(request.LeaseSeconds) * time.Second,
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRunnerClaimResponse(result))
}

// Stream handles GET /api/v1/workspaces/:workspaceId/app-runtime/stream.
func (handler *AppRuntimeHandler) Stream(ctx *gin.Context) {
	authenticated, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	params := appruntime.RunnerClaimParams{
		RunnerID:      ctx.Query("runnerId"),
		RunnerVersion: ctx.Query("runnerVersion"),
		Limit:         intQuery(ctx, "limit"),
		LeaseDuration: time.Duration(intQuery(ctx, "leaseSeconds")) * time.Second,
	}
	once := boolQuery(ctx, "once")
	wait := time.Duration(intQuery(ctx, "waitSeconds")) * time.Second
	if wait <= 0 {
		wait = 5 * time.Second
	}
	if wait > 30*time.Second {
		wait = 30 * time.Second
	}
	ctx.Header("Content-Type", "application/x-ndjson")
	ctx.Header("Cache-Control", "no-cache")
	ctx.Header("X-Accel-Buffering", "no")
	encoder := json.NewEncoder(ctx.Writer)
	flusher, _ := ctx.Writer.(http.Flusher)
	for {
		result, err := handler.runner.ClaimDeliveries(ctx.Request.Context(), authenticated, params)
		if err != nil {
			if ctx.Writer.Written() {
				_ = encoder.Encode(dto.AppRunnerStreamEventResponse{
					Type:  "error",
					Error: &dto.AppRunnerStreamErrorResponse{Code: "claim_failed", Message: err.Error()},
				})
				if flusher != nil {
					flusher.Flush()
				}
				return
			}
			api.StoreError(ctx, err)
			return
		}
		session := converter.ToAppRunnerSessionResponse(result.Session)
		if err := encoder.Encode(dto.AppRunnerStreamEventResponse{Type: "session", Session: &session}); err != nil {
			return
		}
		for _, envelope := range result.Envelopes {
			converted := converter.ToAppRunnerDeliveryEnvelopeResponse(envelope)
			if err := encoder.Encode(dto.AppRunnerStreamEventResponse{Type: "delivery", Envelope: &converted}); err != nil {
				return
			}
		}
		if flusher != nil {
			flusher.Flush()
		}
		if once {
			return
		}
		timer := time.NewTimer(wait)
		select {
		case <-ctx.Request.Context().Done():
			timer.Stop()
			return
		case <-timer.C:
		}
	}
}

// SessionOffline handles POST /api/v1/workspaces/:workspaceId/app-runtime/sessions/:sessionId/offline.
func (handler *AppRuntimeHandler) SessionOffline(ctx *gin.Context) {
	authenticated, ok := handler.authenticate(ctx)
	if !ok {
		return
	}
	session, err := handler.runner.MarkSessionOffline(ctx.Request.Context(), authenticated, ctx.Param("sessionId"))
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRunnerSessionResponse(session))
}

// Context handles GET /api/v1/workspaces/:workspaceId/app-runtime/deliveries/:deliveryId/context.
func (handler *AppRuntimeHandler) Context(ctx *gin.Context) {
	attemptID := ctx.Query("attemptId")
	authenticated, ok := handler.authenticateDeliveryToken(ctx, attemptID)
	if !ok {
		return
	}
	page, err := handler.store.ReadAppRuntimeDeliveryContext(ctx.Request.Context(), authenticated, storeapp.AppRuntimeContextParams{
		AttemptID: attemptID,
		BeforeSeq: int64Query(ctx, "beforeSeq"),
		Limit:     intQuery(ctx, "limit"),
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, page)
}

// Reply handles POST /api/v1/workspaces/:workspaceId/app-runtime/deliveries/:deliveryId/reply.
func (handler *AppRuntimeHandler) Reply(ctx *gin.Context) {
	var request dto.AppRuntimeReplyRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		appRuntimeJSONError(ctx, err)
		return
	}
	authenticated, ok := handler.authenticateDeliveryToken(ctx, request.AttemptID)
	if !ok {
		return
	}
	result, err := handler.store.ReplyAppRuntimeDelivery(ctx.Request.Context(), authenticated, storeapp.AppRuntimeReplyParams{
		AttemptID:      request.AttemptID,
		Content:        request.Content,
		Metadata:       request.Metadata,
		IdempotencyKey: request.IdempotencyKey,
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRuntimeReplyResponse(result))
}

// Ack handles POST /api/v1/workspaces/:workspaceId/app-runtime/deliveries/:deliveryId/ack.
func (handler *AppRuntimeHandler) Ack(ctx *gin.Context) {
	var request dto.AppRuntimeAckRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		appRuntimeJSONError(ctx, err)
		return
	}
	authenticated, ok := handler.authenticateDeliveryToken(ctx, request.AttemptID)
	if !ok {
		return
	}
	result, err := handler.store.AckAppRuntimeDelivery(ctx.Request.Context(), authenticated, storeapp.AppRuntimeAckParams{
		AttemptID: request.AttemptID,
		Result:    request.Result,
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRuntimeDeliveryActionResponse(result))
}

// Fail handles POST /api/v1/workspaces/:workspaceId/app-runtime/deliveries/:deliveryId/fail.
func (handler *AppRuntimeHandler) Fail(ctx *gin.Context) {
	var request dto.AppRuntimeFailRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		appRuntimeJSONError(ctx, err)
		return
	}
	authenticated, ok := handler.authenticateDeliveryToken(ctx, request.AttemptID)
	if !ok {
		return
	}
	result, err := handler.store.FailAppRuntimeDelivery(ctx.Request.Context(), authenticated, storeapp.AppRuntimeFailParams{
		AttemptID: request.AttemptID,
		Code:      request.Code,
		Message:   request.Message,
		Result:    request.Result,
	})
	if err != nil {
		api.StoreError(ctx, err)
		return
	}
	ctx.JSON(http.StatusOK, converter.ToAppRuntimeDeliveryActionResponse(result))
}

func appRuntimeJSONError(ctx *gin.Context, err error) {
	status := http.StatusBadRequest
	var maxBytesError *http.MaxBytesError
	if errors.As(err, &maxBytesError) {
		status = http.StatusRequestEntityTooLarge
	}
	api.Error(ctx, status, "Invalid request body")
}

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

func (handler *AppRuntimeHandler) authenticate(ctx *gin.Context) (appruntime.RunnerAuthentication, bool) {
	bearerToken, ok := bearerTokenFromRequest(ctx)
	if !ok {
		api.Error(ctx, http.StatusUnauthorized, "Missing bearer token")
		return appruntime.RunnerAuthentication{}, false
	}
	authenticated, err := handler.runner.AuthenticateRunner(ctx.Request.Context(), bearerToken)
	switch {
	case err == nil:
		if !api.EnsureWorkspaceOwner(ctx, handler.managedWorkspaces, authenticated.WorkspaceID) {
			return appruntime.RunnerAuthentication{}, false
		}
		if !ensureRuntimeRouteWorkspace(ctx, authenticated.WorkspaceID) {
			return appruntime.RunnerAuthentication{}, false
		}
		return authenticated, true
	case errors.Is(err, api.ErrUnauthorized):
		api.Error(ctx, http.StatusUnauthorized, "Invalid App runner token")
	case errors.Is(err, api.ErrForbidden):
		api.Error(ctx, http.StatusForbidden, "Forbidden")
	default:
		api.StoreError(ctx, err)
	}
	return appruntime.RunnerAuthentication{}, false
}

func intQuery(ctx *gin.Context, key string) int {
	raw := ctx.Query(key)
	if raw == "" {
		return 0
	}
	value, err := strconv.Atoi(raw)
	if err != nil {
		return 0
	}
	return value
}

func int64Query(ctx *gin.Context, key string) int64 {
	raw := ctx.Query(key)
	if raw == "" {
		return 0
	}
	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return 0
	}
	return value
}

func boolQuery(ctx *gin.Context, key string) bool {
	value, err := strconv.ParseBool(ctx.Query(key))
	return err == nil && value
}
