package webagent

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/service/role/agents"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

const maxAgentLookupRequestBodyBytes int64 = 16 * 1024

func limitAgentLookupRequestBody(ctx *gin.Context) {
	ctx.Request.Body = http.MaxBytesReader(
		ctx.Writer,
		ctx.Request.Body,
		maxAgentLookupRequestBodyBytes,
	)
	ctx.Next()
}

func handleWebAgentWorkspace(handler api.WorkspaceScopedHandler) gin.HandlerFunc {
	return api.HandleWorkspaceScoped(handler, api.WithErrorClassifier(classifyWebAgentError))
}

func handleWebAgentWorkspaceJSON[T any](
	handler func(api.WorkspaceScopedJSON[T]) (api.Response, error),
) gin.HandlerFunc {
	return api.HandleWorkspaceScopedJSON[T](handler, api.WithErrorClassifier(classifyWebAgentError))
}

func handleWebAgentCreateJSON[T any](
	handler func(api.WorkspaceScopedJSON[T]) (api.Response, error),
) gin.HandlerFunc {
	return api.HandleWorkspaceScoped(func(request api.WorkspaceScopedRequest) (api.Response, error) {
		rawBody, err := io.ReadAll(request.Gin.Request.Body)
		if err != nil {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request body")
		}
		if hasTopLevelJSONField(rawBody, "permissionMode") {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "permissionMode is not supported when creating agents")
		}
		var body T
		if err := binding.JSON.BindBody(rawBody, &body); err != nil {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request body")
		}
		return handler(api.WorkspaceScopedJSON[T]{WorkspaceScopedRequest: request, Body: body})
	}, api.WithErrorClassifier(classifyWebAgentError))
}

func hasTopLevelJSONField(rawBody []byte, field string) bool {
	var body map[string]json.RawMessage
	if err := json.Unmarshal(rawBody, &body); err != nil {
		return false
	}
	_, ok := body[field]
	return ok
}

func classifyWebAgentError(err error) (api.ErrorResponse, bool) {
	switch {
	case errors.Is(err, agent.ErrNameConflict):
		return api.ErrorResponse{Status: http.StatusConflict, Code: "agent_name_conflict", Message: err.Error()}, true
	case errors.Is(err, runtimecontrol.ErrAgentStillRunning):
		return api.ErrorResponse{Status: http.StatusConflict, Message: err.Error()}, true
	case errors.Is(err, runtimecontrol.ErrDaemonWorkspaceTargetPathUpgradeRequired):
		return api.ErrorResponse{Status: http.StatusConflict, Message: err.Error()}, true
	case errors.Is(err, runtimecontrol.ErrDaemonPermissionModeUpgradeRequired):
		return api.ErrorResponse{Status: http.StatusConflict, Message: err.Error()}, true
	case errors.Is(err, runtimecontrol.ErrDaemonMaaSDirectUpgradeRequired):
		return api.ErrorResponse{Status: http.StatusConflict, Message: err.Error()}, true
	case errors.Is(err, runtimecontrol.ErrMachineOffline):
		return api.ErrorResponse{Status: http.StatusServiceUnavailable, Message: err.Error()}, true
	default:
		return api.ErrorResponse{}, false
	}
}

type Dependencies struct {
	AgentStore                AgentStore
	RoleRealizer              agents.RoleRealizer
	RoleAgentCreate           *agents.Service
	CloudMemory               CloudMemoryProvisioner
	ModelProfiles             ModelProfileStore
	SessionAuth               api.SessionAuthenticator
	WorkspaceStore            api.WorkspaceResolver
	ReviewStore               ReviewStore
	ProjectStore              ProjectStore
	MachineAccess             MachineAccessChecker
	RewindStore               RewindStore
	RuntimeCommands           RuntimeCommands
	Organizations             OrganizationProvider
	HostedAgents              HostedAgentValidator
	ResourceCopy              AgentResourceCopier
	KnowledgeBasesEnabled     bool
	Runtime                   Runtime
	Realtime                  Realtime
	PublicBaseURL             string
	EmailVerificationRequired bool
	Logger                    *slog.Logger
}

func RegisterRoutes(router gin.IRouter, dependencies Dependencies) {
	RegisterControlRoutes(router, dependencies)
	RegisterWorkspaceRoutes(router, dependencies)
}

func RegisterControlRoutes(router gin.IRouter, dependencies Dependencies) {
	if router == nil {
		return
	}

	apiGroup := router.Group("/api")
	userScoped := apiGroup.Group(
		"/v1/me",
		api.RequireAuthenticatedUser(dependencies.SessionAuth),
		api.RequireVerifiedIdentity(dependencies.EmailVerificationRequired),
	)
	registerModelProfileRoutes(userScoped, dependencies)
}

func RegisterWorkspaceRoutes(router gin.IRouter, dependencies Dependencies) {
	if router == nil {
		return
	}

	apiGroup := router.Group("/api")
	workspaceScoped := apiGroup.Group(
		"/v1/workspaces/:workspaceId",
		api.RequireAuthenticatedUser(dependencies.SessionAuth),
		api.RequireVerifiedIdentity(dependencies.EmailVerificationRequired),
		api.RequireRouteWorkspace(dependencies.WorkspaceStore),
	)
	registerWebAgentRoutes(workspaceScoped, dependencies)
}

func registerWebAgentRoutes(workspaceScoped gin.IRoutes, dependencies Dependencies) {
	rewindStore := rewindStoreForDependencies(dependencies)
	modelProfiles := modelProfileStoreForDependencies(dependencies)
	collection := &CollectionHandler{
		agentStore:    dependencies.AgentStore,
		memory:        dependencies.CloudMemory,
		modelProfiles: modelProfiles,
		rewindStore:   rewindStore,
		orgs:          dependencies.Organizations,
		hostedAgents:  dependencies.HostedAgents,
		runtime:       dependencies.Runtime,
		realtime:      dependencies.Realtime,
		logger:        dependencies.Logger,
		publicBaseURL: dependencies.PublicBaseURL,
	}
	roleAgents := &RoleAgentsHandler{
		agentStore:      dependencies.AgentStore,
		modelProfiles:   modelProfiles,
		orgs:            dependencies.Organizations,
		hostedAgents:    dependencies.HostedAgents,
		runtime:         dependencies.Runtime,
		realtime:        dependencies.Realtime,
		logger:          dependencies.Logger,
		roleAgentCreate: roleAgentCreateService(dependencies),
	}
	duplicate := &DuplicateHandler{
		agentStore:            dependencies.AgentStore,
		machineAccess:         machineAccessCheckerForDependencies(dependencies),
		orgs:                  dependencies.Organizations,
		hostedAgents:          dependencies.HostedAgents,
		resourceCopy:          dependencies.ResourceCopy,
		knowledgeBasesEnabled: dependencies.KnowledgeBasesEnabled,
		runtime:               dependencies.Runtime,
		realtime:              dependencies.Realtime,
		logger:                dependencies.Logger,
	}
	runtime := &RuntimeHandler{
		agentStore:      dependencies.AgentStore,
		rewindStore:     rewindStore,
		runtimeCommands: dependencies.RuntimeCommands,
		orgs:            dependencies.Organizations,
		hostedAgents:    dependencies.HostedAgents,
		runtime:         dependencies.Runtime,
		realtime:        dependencies.Realtime,
		logger:          dependencies.Logger,
		publicBaseURL:   dependencies.PublicBaseURL,
	}
	runtimeSnapshots := &RuntimeSnapshotHandler{store: dependencies.AgentStore, runtime: dependencies.Runtime}
	workspaceFiles := &WorkspaceFileHandler{agentStore: dependencies.AgentStore, runtime: dependencies.Runtime}
	memorySync := &MemorySyncHandler{agentStore: memorySyncAgentStoreForDependencies(dependencies), runtime: dependencies.Runtime}
	review := &ReviewHandler{reviewStore: dependencies.ReviewStore, runtime: dependencies.Runtime}
	projects := &ProjectHandler{
		projectStore:  dependencies.ProjectStore,
		reviewStore:   dependencies.ReviewStore,
		machineAccess: machineAccessCheckerForDependencies(dependencies),
		runtime:       dependencies.Runtime,
	}
	trajectory := &TrajectoryHandler{agentStore: dependencies.AgentStore, runtime: dependencies.Runtime}
	behaviorSettings := &AgentBehaviorSettingsHandler{
		store:   agentBehaviorSettingsStoreForDependencies(dependencies),
		runtime: dependencies.Runtime,
	}

	workspaceScoped.GET("/agents", handleWebAgentWorkspace(collection.List))
	workspaceScoped.GET("/agents/directory", handleWebAgentWorkspace(collection.Directory))
	workspaceScoped.GET("/agents/search", handleWebAgentWorkspace(collection.Search))
	workspaceScoped.POST(
		"/agents/lookup",
		limitAgentLookupRequestBody,
		handleWebAgentWorkspaceJSON[dto.AgentDirectoryLookupRequest](collection.Lookup),
	)
	workspaceScoped.POST("/agents/runtime-snapshots", handleWebAgentWorkspaceJSON[dto.AgentRuntimeSnapshotsRequest](runtimeSnapshots.List))
	workspaceScoped.POST("/agents/runtime-failures", handleWebAgentWorkspaceJSON[dto.AgentRuntimeFailuresRequest](collection.RuntimeFailures))
	workspaceScoped.POST("/agents", handleWebAgentCreateJSON[dto.WebAgentCreateRequest](collection.Create))
	workspaceScoped.POST("/agents:from-role", handleWebAgentCreateJSON[dto.WebAgentCreateFromRoleRequest](roleAgents.CreateFromRole))
	workspaceScoped.GET("/agents/:agentId", handleWebAgentWorkspace(collection.Detail))
	workspaceScoped.POST("/agents/:agentId/duplicate", handleWebAgentCreateJSON[dto.WebAgentDuplicateRequest](duplicate.Duplicate))
	workspaceScoped.PATCH("/agents/:agentId", handleWebAgentWorkspaceJSON[dto.WebAgentUpdateRequest](collection.Patch))
	workspaceScoped.POST("/agents/:agentId/update", handleWebAgentWorkspaceJSON[dto.WebAgentUpdateRequest](collection.Patch))
	workspaceScoped.POST("/agents/:agentId/role:apply", handleWebAgentWorkspaceJSON[dto.WebAgentApplyRoleRequest](roleAgents.ApplyRole))
	workspaceScoped.DELETE("/agents/:agentId", handleWebAgentWorkspace(collection.Delete))
	workspaceScoped.POST("/agents/:agentId/notify-memory-access", handleWebAgentWorkspace(collection.NotifyMemoryAccess))
	workspaceScoped.GET("/agents/:agentId/runtime-failure", handleWebAgentWorkspace(collection.RuntimeFailure))
	workspaceScoped.GET("/agents/:agentId/behavior-settings", handleWebAgentWorkspace(behaviorSettings.Get))
	workspaceScoped.PATCH("/agents/:agentId/behavior-settings", handleWebAgentWorkspaceJSON[dto.AgentBehaviorSettingsUpdateRequest](behaviorSettings.Update))
	workspaceScoped.POST("/agents/:agentId/behavior-settings/update", handleWebAgentWorkspaceJSON[dto.AgentBehaviorSettingsUpdateRequest](behaviorSettings.Update))
	workspaceScoped.POST("/agents/:agentId/start", handleWebAgentWorkspace(runtime.Start))
	workspaceScoped.POST("/agents/:agentId/stop", handleWebAgentWorkspace(runtime.Stop))
	workspaceScoped.POST("/agents/:agentId/interrupt", handleWebAgentWorkspace(runtime.Interrupt))
	workspaceScoped.POST("/agents/:agentId/reset", handleWebAgentWorkspaceJSON[dto.WebAgentResetRequest](runtime.Reset))
	workspaceScoped.GET("/agents/:agentId/files", handleWebAgentWorkspace(workspaceFiles.List))
	workspaceScoped.GET("/agents/:agentId/files/content", handleWebAgentWorkspace(workspaceFiles.Read))
	workspaceScoped.GET("/agents/:agentId/memory-sync", handleWebAgentWorkspace(memorySync.Status))
	workspaceScoped.POST("/agents/:agentId/memory-sync/retry", handleWebAgentWorkspace(memorySync.Retry))
	workspaceScoped.GET("/agents/:agentId/trajectory", handleWebAgentWorkspace(trajectory.History))
	workspaceScoped.GET("/reviews", handleWebAgentWorkspace(review.List))
	workspaceScoped.GET("/reviews/:reviewId", handleWebAgentWorkspace(review.Get))
	workspaceScoped.GET("/reviews/:reviewId/snapshot", handleWebAgentWorkspace(review.Snapshot))
	workspaceScoped.GET("/reviews/:reviewId/fingerprint", handleWebAgentWorkspace(review.Fingerprint))
	workspaceScoped.GET("/reviews/:reviewId/summary", handleWebAgentWorkspace(review.Summary))
	workspaceScoped.GET("/reviews/:reviewId/files", handleWebAgentWorkspace(review.Files))
	workspaceScoped.GET("/reviews/:reviewId/diff", handleWebAgentWorkspace(review.Diff))
	workspaceScoped.POST("/reviews/:reviewId/diff/batch", handleWebAgentWorkspaceJSON[dto.ReviewDiffBatchRequest](review.DiffBatch))

	workspaceScoped.GET("/projects", handleWebAgentWorkspace(projects.List))
	workspaceScoped.POST("/projects", handleWebAgentWorkspaceJSON[dto.CreateProjectRequest](projects.Create))
	workspaceScoped.GET("/projects/:projectId", handleWebAgentWorkspace(projects.Get))
	workspaceScoped.PATCH("/projects/:projectId", handleWebAgentWorkspaceJSON[dto.UpdateProjectRequest](projects.Patch))
	workspaceScoped.DELETE("/projects/:projectId", handleWebAgentWorkspace(projects.Delete))
	workspaceScoped.GET("/projects/:projectId/settings/access", handleWebAgentWorkspace(projects.ListAccess))
	workspaceScoped.PUT("/projects/:projectId/settings/access/:memberId", handleWebAgentWorkspaceJSON[dto.UpsertProjectGrantRequest](projects.UpsertAccess))
	workspaceScoped.DELETE("/projects/:projectId/settings/access/:memberId", handleWebAgentWorkspace(projects.DeleteAccess))
	workspaceScoped.GET("/projects/:projectId/files", handleWebAgentWorkspace(projects.FilesList))
	workspaceScoped.GET("/projects/:projectId/files/content", handleWebAgentWorkspace(projects.FilesRead))
	workspaceScoped.GET("/projects/:projectId/reviews", handleWebAgentWorkspace(projects.ReviewsList))

}

func roleAgentCreateService(dependencies Dependencies) *agents.Service {
	if dependencies.RoleAgentCreate != nil {
		return dependencies.RoleAgentCreate
	}
	return agents.NewService(dependencies.AgentStore, dependencies.RoleRealizer)
}

func registerModelProfileRoutes(userScoped gin.IRoutes, dependencies Dependencies) {
	profiles := &ModelProfileHandler{store: modelProfileStoreForDependencies(dependencies)}
	userScoped.GET("/model-profiles", api.HandleAuth(profiles.List))
	userScoped.POST("/model-profiles", api.HandleAuthJSON[dto.ModelProfileCreateRequest](profiles.Create))
	userScoped.POST("/model-profiles:action", handleModelProfileCollectionAction(profiles))
	userScoped.GET("/model-profiles/:profileId", api.HandleAuth(profiles.Get))
	userScoped.PATCH("/model-profiles/:profileId", api.HandleAuthJSON[dto.ModelProfileUpdateRequest](profiles.Update))
	userScoped.DELETE("/model-profiles/:profileId", api.HandleAuth(profiles.Delete))
}

func handleModelProfileCollectionAction(profiles *ModelProfileHandler) gin.HandlerFunc {
	return api.HandleAuth(func(request api.AuthenticatedRequest) (api.Response, error) {
		switch strings.TrimPrefix(request.Gin.Param("action"), ":") {
		case "detectModels":
			body, err := api.BindJSON[dto.ModelProfileModelsRequest](request.Gin)
			if err != nil {
				return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request body")
			}
			return profiles.Models(api.AuthenticatedJSON[dto.ModelProfileModelsRequest]{
				AuthenticatedRequest: request,
				Body:                 body,
			})
		case "testConnection":
			body, err := api.BindJSON[dto.ModelProfileConnectionTestRequest](request.Gin)
			if err != nil {
				return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request body")
			}
			return profiles.TestConnection(api.AuthenticatedJSON[dto.ModelProfileConnectionTestRequest]{
				AuthenticatedRequest: request,
				Body:                 body,
			})
		default:
			return api.Response{}, api.NewHTTPError(http.StatusNotFound, "Unknown model profile action")
		}
	})
}

func modelProfileStoreForDependencies(dependencies Dependencies) ModelProfileStore {
	if dependencies.ModelProfiles != nil {
		return dependencies.ModelProfiles
	}
	if store, ok := dependencies.AgentStore.(ModelProfileStore); ok {
		return store
	}
	return nil
}

func rewindStoreForDependencies(dependencies Dependencies) RewindStore {
	if dependencies.RewindStore != nil {
		return dependencies.RewindStore
	}
	return nil
}

func agentBehaviorSettingsStoreForDependencies(dependencies Dependencies) AgentBehaviorSettingsStore {
	if store, ok := dependencies.AgentStore.(AgentBehaviorSettingsStore); ok {
		return store
	}
	return nil
}

func machineAccessCheckerForDependencies(dependencies Dependencies) MachineAccessChecker {
	if dependencies.MachineAccess != nil {
		return dependencies.MachineAccess
	}
	if checker, ok := dependencies.AgentStore.(MachineAccessChecker); ok {
		return checker
	}
	return nil
}

type memorySyncAgentStoreAdapter struct {
	store AgentStore
}

func (adapter memorySyncAgentStoreAdapter) AgentMemorySyncRouteForWorkspace(
	ctx context.Context,
	userID string,
	workspaceID string,
	agentID string,
) (memorySyncAgentRoute, bool, error) {
	snapshots, err := adapter.store.AgentRuntimeSnapshotsForWorkspace(ctx, userID, workspaceID, []string{agentID})
	if err != nil {
		return memorySyncAgentRoute{}, false, err
	}
	if len(snapshots) != 1 {
		return memorySyncAgentRoute{}, false, nil
	}
	return memorySyncAgentRoute{
		agentID:   snapshots[0].AgentID,
		machineID: snapshots[0].MachineID,
		canManage: snapshots[0].CanManage,
	}, true, nil
}

func memorySyncAgentStoreForDependencies(dependencies Dependencies) memorySyncAgentStore {
	if dependencies.AgentStore == nil {
		return nil
	}
	return memorySyncAgentStoreAdapter{store: dependencies.AgentStore}
}
