package agent

import (
	"context"
	"encoding/json"
	"errors"
	"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/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

// AgentAuthenticator validates agent credentials and returns the authenticated identity.
type AgentAuthenticator interface {
	AuthenticateAgent(ctx context.Context, agentID string, bearerToken string) (agent.AuthenticatedAgent, error)
}

func authenticate(ctx *gin.Context, auth AgentAuthenticator) (agent.AuthenticatedAgent, bool) {
	authenticatedAgent, err := authenticateAgent(ctx, auth)
	if err != nil {
		ctx.JSON(HTTPStatus(err), agentErrorFor(err))
		return agent.AuthenticatedAgent{}, false
	}
	return authenticatedAgent, true
}

func authenticateWorkspace(ctx *gin.Context, auth AgentAuthenticator) (agent.AuthenticatedAgent, bool) {
	authenticatedAgent, err := authenticateAgent(ctx, auth)
	if err != nil {
		ctx.JSON(HTTPStatus(err), dto.AgentWorkspaceSnapshotResponse{Error: err.Error(), Channels: []dto.WorkspaceChannel{}, Agents: []dto.WorkspaceActor{}, Humans: []dto.WorkspaceActor{}})
		return agent.AuthenticatedAgent{}, false
	}
	return authenticatedAgent, true
}

// RequireAgentRuntimePrincipal authenticates a canonical workspace-scoped
// Agent Runtime request once and stores the trusted principal for typed
// handlers.
func RequireAgentRuntimePrincipal(auth AgentAuthenticator) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if auth == nil {
			ctx.JSON(http.StatusUnauthorized, agentError(api.ErrUnauthorized.Error()))
			ctx.Abort()
			return
		}
		bearerToken, ok := api.BearerToken(ctx)
		if !ok {
			ctx.JSON(http.StatusUnauthorized, agentError(api.ErrUnauthorized.Error()))
			ctx.Abort()
			return
		}
		routeAgentID := strings.TrimSpace(ctx.Param("agentId"))
		authenticatedAgent, err := auth.AuthenticateAgent(ctx.Request.Context(), routeAgentID, bearerToken)
		if errors.Is(err, agent.ErrUnauthorized) {
			err = api.ErrUnauthorized
		}
		if err == nil && authenticatedAgent.AgentID != routeAgentID {
			err = api.ErrUnauthorized
		}
		if err == nil {
			err = requireMatchingRouteWorkspace(ctx, authenticatedAgent.WorkspaceID)
		}
		if err != nil {
			ctx.JSON(HTTPStatus(err), agentErrorFor(err))
			ctx.Abort()
			return
		}
		api.SetAuthenticatedAgent(ctx, authenticatedAgent)
		ctx.Next()
	}
}

func authenticateAgent(ctx *gin.Context, auth AgentAuthenticator) (agent.AuthenticatedAgent, error) {
	return authenticateAgentWithID(ctx, auth, ctx.Param("agentId"))
}

// authenticateAgentWithID authenticates an explicit route Agent identity. The
// returned principal, not the lookup string, remains authoritative.
func authenticateAgentWithID(ctx *gin.Context, auth AgentAuthenticator, agentID string) (agent.AuthenticatedAgent, error) {
	if auth == nil {
		return agent.AuthenticatedAgent{}, api.ErrUnauthorized
	}
	bearerToken := strings.TrimSpace(strings.TrimPrefix(ctx.GetHeader("Authorization"), "Bearer "))
	if bearerToken == "" || bearerToken == ctx.GetHeader("Authorization") {
		return agent.AuthenticatedAgent{}, api.ErrUnauthorized
	}
	authenticatedAgent, err := auth.AuthenticateAgent(ctx.Request.Context(), agentID, bearerToken)
	if errors.Is(err, agent.ErrUnauthorized) {
		return agent.AuthenticatedAgent{}, api.ErrUnauthorized
	}
	if err != nil {
		return agent.AuthenticatedAgent{}, err
	}
	if err := requireMatchingRouteWorkspace(ctx, authenticatedAgent.WorkspaceID); err != nil {
		return agent.AuthenticatedAgent{}, err
	}
	return authenticatedAgent, err
}

func requireMatchingRouteWorkspace(ctx *gin.Context, authenticatedWorkspaceID string) error {
	if ctx == nil {
		return badRequest("workspace route is required")
	}
	routeWorkspaceID, ok := api.CanonicalRouteWorkspaceID(ctx)
	if !ok {
		return badRequest("invalid route workspace identity")
	}
	if routeWorkspaceID != strings.TrimSpace(authenticatedWorkspaceID) {
		return owner.OwnerMismatchError{WorkspaceID: routeWorkspaceID}
	}
	return nil
}

func bindJSON(ctx *gin.Context, request any) bool {
	if err := ctx.ShouldBindJSON(request); err != nil {
		ctx.JSON(http.StatusBadRequest, agentError("invalid request body"))
		return false
	}
	return true
}

// bindJSONStrict decodes the request body with DisallowUnknownFields so removed
// canonical fields are rejected with 400 instead of silently ignored. Use it
// for canonical mutation handlers; compatibility handlers keep lenient bindJSON.
func bindJSONStrict(ctx *gin.Context, request any) bool {
	if ctx == nil || ctx.Request == nil || ctx.Request.Body == nil {
		ctx.JSON(http.StatusBadRequest, agentError("invalid request body"))
		return false
	}
	decoder := json.NewDecoder(ctx.Request.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(request); err != nil {
		ctx.JSON(http.StatusBadRequest, agentError("invalid request body"))
		return false
	}
	return true
}

func agentError(message string) dto.AgentErrorResponse {
	return dto.AgentErrorResponse{Error: message}
}

func agentErrorFor(err error) dto.AgentErrorResponse {
	if errors.Is(err, owner.ErrOwnerMismatch) {
		return dto.AgentErrorResponse{
			Error: "workspace owner mismatch",
			Code:  "workspace_owner_mismatch",
		}
	}
	var transitionErr taskboard.InvalidTaskStatusTransitionError
	if errors.As(err, &transitionErr) {
		return dto.AgentErrorResponse{
			Error:   transitionErr.Error(),
			Code:    taskboard.InvalidTaskStatusTransitionCode,
			Details: transitionErr.Details(),
		}
	}
	if errors.Is(err, taskboard.ErrInvalidChannelTarget) {
		return agentErrorWithCode(err.Error(), taskboard.InvalidChannelTargetCode)
	}
	return agentError(err.Error())
}

func agentErrorWithCode(message string, code string) dto.AgentErrorResponse {
	return dto.AgentErrorResponse{Error: message, Code: code}
}
