package webagent

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

	"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/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/service/maas"
	"github.com/tidbcloud/AgentTeam/internal/service/role/agents"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

func (handler *RoleAgentsHandler) CreateFromRole(r api.WorkspaceScopedJSON[dto.WebAgentCreateFromRoleRequest]) (api.Response, error) {
	if handler.roleAgentCreate == nil {
		return api.Response{}, api.ErrNotFound
	}
	request := r.Body
	trimmedName := strings.TrimSpace(request.Name)
	if trimmedName == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "name is required")
	}
	machineID := strings.TrimSpace(request.MachineID)
	if machineID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "machineId is required")
	}

	orgs, _ := handler.organizationProvider()
	requestedLLM := agentLLMFromFields(request.LLMMode, request.BaseURL, request.APIKey, request.Headers, request.WireAPI)
	if strings.TrimSpace(request.ModelProfileID) != "" {
		profile, err := handler.modelProfileLLM(r.Context(), r.UserID(), strings.TrimSpace(request.ModelProfileID))
		if err != nil {
			return api.Response{}, err
		}
		request.Model = profile.Model
		requestedLLM = agentLLMFromFields(agent.AgentLLMModeLocal, profile.BaseURL, profile.APIKey, map[string]string{}, profile.WireAPI)
	}
	if err := handler.validateHostedAgentBindingForUser(r.Context(), r.UserID(), r.WorkspaceID(), machineID, request.Model, request.Runtime, requestedLLM); err != nil {
		return api.Response{}, err
	}
	if err := validateWorkspaceAgentLLM(r.Context(), orgs, r.WorkspaceID(), requestedLLM); err != nil {
		return api.Response{}, err
	}
	storedLLM, err := buildStoredAgentLLM(r.Context(), orgs, r.WorkspaceID(), requestedLLM)
	if err != nil {
		return api.Response{}, err
	}
	request.LLMMode, request.BaseURL, request.APIKey, request.Headers, request.WireAPI = agent.AgentLLMFieldsFromStored(storedLLM)

	source := agents.Source{Kind: strings.TrimSpace(request.Source.Kind), PresetID: strings.TrimSpace(request.Source.PresetID), RoleID: strings.TrimSpace(request.Source.RoleID), VersionID: strings.TrimSpace(request.Source.VersionID), Locale: strings.TrimSpace(request.Source.Locale)}
	created, err := handler.roleAgentCreate.CreateAgentFromRole(r.Context(), agents.CreateFromRoleInput{
		UserID:          r.UserID(),
		WorkspaceID:     r.WorkspaceID(),
		MachineID:       machineID,
		Agent:           createRoleBackedAgentParamsFromRequest(request, trimmedName),
		Source:          source,
		RoleDisplayName: strings.TrimSpace(request.RoleDisplayName),
		Realization: agents.RealizationOptions{
			DryRun:         request.Realization.DryRun,
			Apply:          request.Realization.Apply || !request.Realization.DryRun,
			SkipOptional:   request.Realization.SkipOptional,
			StorageScope:   strings.TrimSpace(request.Realization.StorageScope),
			TemplateLocale: strings.TrimSpace(request.Realization.TemplateLocale),
		},
	})
	if err != nil {
		response, ok := roleAgentHTTPError(err)
		if ok {
			return api.Response{}, response
		}
		return api.Response{}, err
	}

	agent, err := handler.agentStore.AgentForWorkspace(r.Context(), r.UserID(), r.WorkspaceID(), created.Created.AgentID)
	if err != nil {
		return api.Response{}, err
	}
	api.OverlayAppAgentRuntime(&agent, handler.runtime, time.Now().UTC())
	if handler.runtime != nil {
		if err := handler.runtime.SyncMachineCatalog(r.Context(), machineID); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
			if handler.logger != nil {
				handler.logger.Warn("sync machine catalog after role-backed agent create", slog.String("machine_id", machineID), slog.String("agent_id", created.Created.AgentID), slog.Any("error", err))
			}
		}
	}
	if handler.realtime != nil {
		handler.realtime.EmitAgentCreated(r.WorkspaceID())
	}

	return api.OK(dto.WebAgentCreateFromRoleResponse{
		Agent:       converter.ToAppAgent(agent),
		Source:      roleSourceResponse(source),
		Partial:     created.Partial,
		Realization: toRoleRealizationResponse(created.Realization),
		CreatedAt:   time.Now().UTC(),
	}), nil
}

func (handler *RoleAgentsHandler) ApplyRole(r api.WorkspaceScopedJSON[dto.WebAgentApplyRoleRequest]) (api.Response, error) {
	if handler.roleAgentCreate == nil {
		return api.Response{}, api.ErrNotFound
	}
	agentID := strings.TrimSpace(r.Gin.Param("agentId"))
	if agentID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "agentId is required")
	}
	request := r.Body
	source := agents.Source{Kind: strings.TrimSpace(request.Source.Kind), PresetID: strings.TrimSpace(request.Source.PresetID), RoleID: strings.TrimSpace(request.Source.RoleID), VersionID: strings.TrimSpace(request.Source.VersionID), Locale: strings.TrimSpace(request.Source.Locale)}
	applied, err := handler.roleAgentCreate.ApplyRoleToAgent(r.Context(), agents.RealizeRoleForAgentInput{
		UserID:          r.UserID(),
		WorkspaceID:     r.WorkspaceID(),
		AgentID:         agentID,
		Source:          source,
		RoleDisplayName: strings.TrimSpace(request.RoleDisplayName),
		Realization: agents.RealizationOptions{
			DryRun:         request.Realization.DryRun,
			Apply:          request.Realization.Apply || !request.Realization.DryRun,
			SkipOptional:   request.Realization.SkipOptional,
			StorageScope:   strings.TrimSpace(request.Realization.StorageScope),
			TemplateLocale: strings.TrimSpace(request.Realization.TemplateLocale),
		},
	})
	if err != nil {
		response, ok := roleAgentHTTPError(err)
		if ok {
			return api.Response{}, response
		}
		return api.Response{}, err
	}

	agent, err := handler.agentStore.AgentForWorkspace(r.Context(), r.UserID(), r.WorkspaceID(), agentID)
	if err != nil {
		return api.Response{}, err
	}
	api.OverlayAppAgentRuntime(&agent, handler.runtime, time.Now().UTC())
	return api.OK(dto.WebAgentApplyRoleResponse{
		Agent:       converter.ToAppAgent(agent),
		Source:      roleSourceResponse(source),
		Partial:     applied.Partial,
		Realization: toRoleRealizationResponse(applied.Realization),
		AppliedAt:   time.Now().UTC(),
	}), nil
}

func roleSourceResponse(source agents.Source) dto.WebAgentRoleSource {
	return dto.WebAgentRoleSource{
		Kind:      source.Kind,
		PresetID:  source.PresetID,
		RoleID:    source.RoleID,
		VersionID: source.VersionID,
		Locale:    source.Locale,
	}
}

func createRoleBackedAgentParamsFromRequest(request dto.WebAgentCreateFromRoleRequest, trimmedName string) agent.CreateAgentParams {
	params := createAgentParamsFromRequest(dto.WebAgentCreateRequest{
		Name:            request.Name,
		Description:     request.Description,
		Runtime:         request.Runtime,
		Model:           request.Model,
		ReasoningEffort: request.ReasoningEffort,
		LLMMode:         request.LLMMode,
		BaseURL:         request.BaseURL,
		APIKey:          request.APIKey,
		Headers:         request.Headers,
		WireAPI:         request.WireAPI,
		ModelProfileID:  request.ModelProfileID,
		MachineID:       request.MachineID,
		ChannelIDs:      request.ChannelIDs,
	}, trimmedName)
	switch strings.TrimSpace(request.Source.Kind) {
	case "preset":
		params.SourceKind = "preset"
		params.SourceRef = strings.TrimSpace(request.Source.PresetID)
	case "my-role":
		params.SourceKind = "my-role"
		params.SourceRef = strings.TrimSpace(request.Source.RoleID)
	}
	return params
}

func toRoleRealizationResponse(realization agents.RealizationResult) dto.WebAgentRoleRealizationResponse {
	items := make([]dto.WebAgentRoleRealizationItemResponse, 0, len(realization.Items))
	for _, item := range realization.Items {
		items = append(items, dto.WebAgentRoleRealizationItemResponse{
			ManifestItemType: item.ManifestItemType,
			ManifestItemName: item.ManifestItemName,
			Action:           item.Action,
			Status:           item.Status,
			ResourceID:       item.ResourceID,
			AccessLevel:      item.AccessLevel,
			Branch:           item.Branch,
			Message:          item.Message,
			Required:         item.Required,
		})
	}
	return dto.WebAgentRoleRealizationResponse{RoleID: realization.RoleID, AgentID: realization.AgentID, DryRun: realization.DryRun, Apply: realization.Apply, Items: items}
}

func roleAgentHTTPError(err error) (error, bool) {
	var validationErr *agents.ValidationError
	if errors.As(err, &validationErr) {
		return api.NewHTTPError(http.StatusBadRequest, validationErr.Error()), true
	}
	var realizationErr *agents.RealizationError
	if errors.As(err, &realizationErr) {
		var quotaErr plan.QuotaExceededError
		if realizationErr.CleanupErr == nil && errors.As(realizationErr.Cause, &quotaErr) {
			return nil, false
		}
		if status, message, ok := roleAgentDependencyHTTPError(realizationErr.Cause); ok {
			if realizationErr.CleanupErr != nil {
				message += "; cleanup failed: " + realizationErr.CleanupErr.Error()
			}
			return api.NewHTTPError(status, message), true
		}
		message := "role realization failed"
		if realizationErr.CleanupErr != nil {
			message += "; cleanup failed: " + realizationErr.CleanupErr.Error()
		}
		return api.NewHTTPError(http.StatusConflict, message), true
	}
	var quotaErr plan.QuotaExceededError
	if errors.As(err, &quotaErr) {
		return nil, false
	}
	if status, message, ok := roleAgentDependencyHTTPError(err); ok {
		return api.NewHTTPError(status, message), true
	}
	return nil, false
}

func roleAgentDependencyHTTPError(err error) (int, string, bool) {
	if err == nil {
		return 0, "", false
	}
	if status := api.StatusForError(err); status != http.StatusInternalServerError {
		return status, err.Error(), true
	}
	status, ok := roleAgentMeshXHTTPStatus(err.Error())
	if ok {
		return status, roleAgentMeshXHTTPMessage(err.Error(), status), true
	}
	return 0, "", false
}

func roleAgentMeshXHTTPStatus(message string) (int, bool) {
	if status, ok := roleAgentMeshXHTTPStatusFromMessage(message); ok {
		return status, true
	}
	message = strings.ToLower(message)
	switch {
	case strings.Contains(message, `"code":"invalid_argument"`) || strings.Contains(message, `"code": "invalid_argument"`):
		return http.StatusBadRequest, true
	case strings.Contains(message, `"code":"forbidden"`) || strings.Contains(message, `"code": "forbidden"`):
		return http.StatusForbidden, true
	case strings.Contains(message, `"code":"not_found"`) || strings.Contains(message, `"code": "not_found"`):
		return http.StatusNotFound, true
	case strings.Contains(message, `"code":"conflict"`) || strings.Contains(message, `"code": "conflict"`):
		return http.StatusConflict, true
	default:
		return 0, false
	}
}

func roleAgentMeshXHTTPStatusFromMessage(message string) (int, bool) {
	trimmed := strings.TrimSpace(message)
	const marker = ": status "
	index := strings.Index(trimmed, marker)
	if index < 0 {
		return 0, false
	}
	remainder := trimmed[index+len(marker):]
	colon := strings.Index(remainder, ":")
	if colon < 0 {
		return 0, false
	}
	status, err := strconv.Atoi(strings.TrimSpace(remainder[:colon]))
	if err != nil || status < 100 || status > 599 {
		return 0, false
	}
	return status, true
}

func roleAgentMeshXHTTPMessage(message string, status int) string {
	body := roleAgentMeshXHTTPBody(message)
	if parsed := roleAgentMeshXJSONMessage(body); parsed != "" {
		return parsed
	}
	if roleAgentJSONBody(body) {
		return roleAgentMeshXFallbackMessage(status)
	}
	if trimmed := strings.TrimSpace(body); trimmed != "" && !roleAgentInternalCode(trimmed) {
		return trimmed
	}
	return roleAgentMeshXFallbackMessage(status)
}

func roleAgentMeshXHTTPBody(message string) string {
	trimmed := strings.TrimSpace(message)
	const marker = ": status "
	index := strings.Index(trimmed, marker)
	if index < 0 {
		return trimmed
	}
	remainder := trimmed[index+len(marker):]
	colon := strings.Index(remainder, ":")
	if colon < 0 {
		return trimmed
	}
	return strings.TrimSpace(remainder[colon+1:])
}

func roleAgentMeshXJSONMessage(body string) string {
	var payload map[string]any
	if err := json.Unmarshal([]byte(strings.TrimSpace(body)), &payload); err != nil {
		return ""
	}
	code := jsonStringField(payload, "code")
	for _, key := range []string{"message", "detail", "reason", "error"} {
		value := jsonStringField(payload, key)
		if value == "" || value == code || roleAgentInternalCode(value) {
			continue
		}
		return value
	}
	return ""
}

func roleAgentJSONBody(body string) bool {
	trimmed := strings.TrimSpace(body)
	return strings.HasPrefix(trimmed, "{") && json.Valid([]byte(trimmed))
}

func jsonStringField(payload map[string]any, key string) string {
	value, ok := payload[key]
	if !ok {
		return ""
	}
	text, ok := value.(string)
	if !ok {
		return ""
	}
	return strings.TrimSpace(text)
}

func roleAgentInternalCode(value string) bool {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "invalid_argument", "forbidden", "not_found", "conflict", "internal":
		return true
	default:
		return false
	}
}

func roleAgentMeshXFallbackMessage(status int) string {
	switch status {
	case http.StatusBadRequest:
		return "role apply request is invalid"
	case http.StatusForbidden:
		return "you do not have permission to apply this role"
	case http.StatusNotFound:
		return "role or target agent was not found"
	case http.StatusConflict:
		return "role cannot be applied in the current state"
	default:
		if status >= 500 {
			return "role service is temporarily unavailable"
		}
		return "role apply failed"
	}
}

func (handler *RoleAgentsHandler) organizationProvider() (OrganizationProvider, bool) {
	if handler.orgs == nil {
		return nil, false
	}
	return handler.orgs, true
}

func (handler *RoleAgentsHandler) modelProfileLLM(ctx context.Context, userID string, profileID string) (agent.ModelProfile, error) {
	if handler.modelProfiles == nil {
		return agent.ModelProfile{}, api.ErrNotFound
	}
	return handler.modelProfiles.GetModelProfileForUser(ctx, userID, profileID)
}

func (handler *RoleAgentsHandler) validateHostedAgentBindingForUser(ctx context.Context, userID string, workspaceID string, machineID string, modelRef string, runtime string, llm *agent.AgentLLM) error {
	if !agentLLMUsesAgentTeamHosted(llm) {
		return nil
	}
	if handler.hostedAgents == nil {
		return storeerr.Conflict(maas.ReasonRuntimeUnavailable)
	}
	return handler.hostedAgents.ValidateAgentBindingForUser(ctx, maas.AgentBindingValidation{
		UserID:                    userID,
		WorkspaceID:               workspaceID,
		ModelReference:            modelRef,
		LLMMode:                   agent.AgentLLMModeAgentTeamHosted,
		Runtime:                   runtime,
		MachineSupportsDirectMaaS: handler.machineMaaSDirectSupport(machineID),
	})
}

func (handler *RoleAgentsHandler) machineMaaSDirectSupport(machineID string) *bool {
	if handler.runtime == nil {
		return nil
	}
	supported, known := handler.runtime.AgentMaaSDirectSupport(machineID)
	if !known {
		return nil
	}
	return &supported
}
