package agent

import (
	"context"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/schedule/manage"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

// ScheduleHandler handles CRUD and lifecycle operations for agent-owned schedules.
type ScheduleHandler struct {
	auth      AgentAuthenticator
	owners    TriggerOwnerStore
	schedules ScheduleControlService
}

// The camelCase page fields are authoritative. The snake_case aliases only
// preserve compatibility with released daemons and copy those canonical values.
type agentScheduleListCompatibilityResponse struct {
	Schedules        []manage.ScheduleView `json:"schedules"`
	Limit            int                   `json:"limit"`
	Cursor           string                `json:"cursor"`
	HasMore          bool                  `json:"hasMore"`
	NextCursor       string                `json:"nextCursor"`
	LegacyHasMore    bool                  `json:"has_more"`
	LegacyNextCursor string                `json:"next_cursor"`
}

func scheduleListCompatibilityResponse(schedules []manage.ScheduleView, limit int, cursor string, hasMore bool, nextCursor string) agentScheduleListCompatibilityResponse {
	return agentScheduleListCompatibilityResponse{
		Schedules:        schedules,
		Limit:            limit,
		Cursor:           cursor,
		HasMore:          hasMore,
		NextCursor:       nextCursor,
		LegacyHasMore:    hasMore,
		LegacyNextCursor: nextCursor,
	}
}

// ScheduleControlService provides the schedule lifecycle API scoped to an owner.
type ScheduleControlService interface {
	ListSchedulesForOwnerWithFilter(ctx context.Context, ownerUserID string, workspaceID string, filter manage.ListFilter) ([]manage.ScheduleView, error)
	ValidateScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, input manage.UpsertInput, existingScheduleID string) (manage.ScheduleValidateResult, error)
	CreateScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, input manage.UpsertInput) (manage.ScheduleView, error)
	GetScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string) (manage.ScheduleView, error)
	UpdateScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string, input manage.UpsertInput) (manage.ScheduleView, error)
	PauseScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string) (manage.ScheduleView, error)
	ResumeScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string) (manage.ScheduleView, error)
	DeleteScheduleForOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string) error
	ListAutomationRunsForScheduleOwnerPage(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string, options manage.RunListOptions) (manage.RunPage, error)
	ListAutomationRunsForScheduleOwner(ctx context.Context, ownerUserID string, workspaceID string, scheduleID string, limit int) ([]manage.AutomationRunView, error)
}

func (h *ScheduleHandler) requireScheduleService(c *gin.Context) ScheduleControlService {
	if h.schedules == nil {
		c.JSON(http.StatusNotImplemented, agentError("Schedule service is not configured"))
		return nil
	}
	return h.schedules
}

func (h *ScheduleHandler) List(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	ownerUserID, err := h.owners.AgentManagedByUser(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	limit, err := ParseLimit(c.Query("limit"), manage.DefaultListLimit)
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	if limit > manage.MaxListLimit {
		limit = manage.MaxListLimit
	}
	cursor := strings.TrimSpace(c.Query("cursor"))
	pageMode := cursor != "" || parseBoolishQuery(c.Query("page"))
	offset, err := parseOffsetCursor(cursor)
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	queryLimit := limit
	if pageMode {
		queryLimit = limit + 1
	}
	listFilter := manage.ListFilter{
		SourceKind: manage.SourceKind(strings.TrimSpace(strings.ToLower(c.Query("source")))),
		Status:     strings.TrimSpace(strings.ToLower(c.Query("status"))),
		Limit:      queryLimit,
		Offset:     offset,
		Page:       pageMode,
	}
	// The canonical exec input is a single self-describing target (e.g.
	// "#channel" / reply thread / DM). The server resolves it to the stored
	// scope id and derives the target kind from the author target syntax; the
	// Desktop transport cannot obtain the resolved scope id itself.
	if targetQuery := strings.TrimSpace(c.Query("target")); targetQuery != "" {
		resolved, err := h.owners.ResolveWritableAgentTargetScopeID(c.Request.Context(), authenticatedAgent, targetQuery)
		if err != nil {
			c.JSON(HTTPStatus(err), agentError(err.Error()))
			return
		}
		targetKind, err := scheduleListTargetKind(targetQuery)
		if err != nil {
			c.JSON(http.StatusBadRequest, agentError(err.Error()))
			return
		}
		listFilter.TargetKind = targetKind
		listFilter.TargetRef = resolved
	}
	result, err := schedules.ListSchedulesForOwnerWithFilter(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, listFilter)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	if pageMode {
		hasMore := len(result) > limit
		if hasMore {
			result = result[:limit]
		}
		nextCursor := ""
		if hasMore {
			nextCursor = strconv.Itoa(offset + limit)
		}
		c.JSON(http.StatusOK, scheduleListCompatibilityResponse(result, limit, cursor, hasMore, nextCursor))
		return
	}
	if len(result) > limit {
		result = result[:limit]
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) Validate(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	var request dto.AgentScheduleValidateRequest
	if !bindJSONStrict(c, &request) {
		return
	}
	ownerUserID, input, err := h.scheduleInputForAgent(c.Request.Context(), authenticatedAgent, request.AgentScheduleMutationRequest)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	result, err := schedules.ValidateScheduleForOwner(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, input, strings.TrimSpace(request.ExistingID()))
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) Create(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	var request dto.AgentScheduleMutationRequest
	if !bindJSONStrict(c, &request) {
		return
	}
	ownerUserID, input, err := h.scheduleInputForAgent(c.Request.Context(), authenticatedAgent, request)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	result, err := schedules.CreateScheduleForOwner(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, input)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) Get(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	ownerUserID, err := h.owners.AgentManagedByUser(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	result, err := schedules.GetScheduleForOwner(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, scheduleOrTriggerID(c))
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) Update(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	var request dto.AgentScheduleMutationRequest
	if !bindJSONStrict(c, &request) {
		return
	}
	ownerUserID, input, err := h.scheduleInputForAgent(c.Request.Context(), authenticatedAgent, request)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	result, err := schedules.UpdateScheduleForOwner(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, scheduleOrTriggerID(c), input)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) Pause(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	h.handleLifecycleMutation(c, authenticatedAgent, func(ctx context.Context, service ScheduleControlService, ownerUserID string, workspaceID string, scheduleID string) (manage.ScheduleView, error) {
		return service.PauseScheduleForOwner(ctx, ownerUserID, workspaceID, scheduleID)
	})
}

func (h *ScheduleHandler) Resume(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	h.handleLifecycleMutation(c, authenticatedAgent, func(ctx context.Context, service ScheduleControlService, ownerUserID string, workspaceID string, scheduleID string) (manage.ScheduleView, error) {
		return service.ResumeScheduleForOwner(ctx, ownerUserID, workspaceID, scheduleID)
	})
}

func (h *ScheduleHandler) Delete(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	ownerUserID, err := h.owners.AgentManagedByUser(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	if err := schedules.DeleteScheduleForOwner(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, scheduleOrTriggerID(c)); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentScheduleDeleteResponse{Deleted: true})
}

type agentScheduleRunCursor struct {
	Version     int       `json:"v"`
	ScheduleID  string    `json:"scheduleId"`
	AttemptedAt time.Time `json:"attemptedAt"`
	ID          string    `json:"id"`
}

func (h *ScheduleHandler) ListRuns(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	ownerUserID, err := h.owners.AgentManagedByUser(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	limit, err := ParseLimit(c.Query("limit"), manage.DefaultListLimit)
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	if limit > manage.MaxListLimit {
		limit = manage.MaxListLimit
	}
	scheduleID := scheduleOrTriggerID(c)
	options := manage.RunListOptions{Limit: limit}
	rawCursor := strings.TrimSpace(c.Query("cursor"))
	if rawCursor != "" {
		var cursor agentScheduleRunCursor
		if err := decodeAgentCollectionCursor(rawCursor, "scheduled-message run", &cursor); err != nil || cursor.Version != 2 || cursor.ScheduleID != scheduleID || cursor.AttemptedAt.IsZero() || strings.TrimSpace(cursor.ID) == "" {
			c.JSON(http.StatusBadRequest, agentError("invalid scheduled-message run cursor"))
			return
		}
		attemptedAt := cursor.AttemptedAt.UTC()
		options.CursorAttempted = &attemptedAt
		options.CursorID = strings.TrimSpace(cursor.ID)
	}
	page, err := schedules.ListAutomationRunsForScheduleOwnerPage(c.Request.Context(), ownerUserID, authenticatedAgent.WorkspaceID, scheduleID, options)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	nextCursor := ""
	if page.HasMore && len(page.Runs) > 0 {
		last := page.Runs[len(page.Runs)-1]
		nextCursor, err = encodeAgentCollectionCursor(agentScheduleRunCursor{Version: 2, ScheduleID: scheduleID, AttemptedAt: last.AttemptedAt.UTC(), ID: last.ID})
		if err != nil {
			c.JSON(http.StatusInternalServerError, agentError(err.Error()))
			return
		}
	}
	c.JSON(http.StatusOK, gin.H{"runs": page.Runs, "hasMore": page.HasMore, "nextCursor": nextCursor})
}

func (h *ScheduleHandler) handleLifecycleMutation(c *gin.Context, authenticatedAgent agent.AuthenticatedAgent, fn func(context.Context, ScheduleControlService, string, string, string) (manage.ScheduleView, error)) {
	schedules := h.requireScheduleService(c)
	if schedules == nil {
		return
	}
	ownerUserID, err := h.owners.AgentManagedByUser(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	result, err := fn(c.Request.Context(), schedules, ownerUserID, authenticatedAgent.WorkspaceID, scheduleOrTriggerID(c))
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *ScheduleHandler) scheduleInputForAgent(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, request dto.AgentScheduleMutationRequest) (string, manage.UpsertInput, error) {
	ownerUserID, err := h.owners.AgentManagedByUser(ctx, authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID)
	if err != nil {
		return "", manage.UpsertInput{}, err
	}
	targetRef, err := h.owners.ResolveWritableAgentTargetScopeID(ctx, authenticatedAgent, strings.TrimSpace(request.Target))
	if err != nil {
		return "", manage.UpsertInput{}, err
	}
	source, err := request.Schedule.ToSourceInput()
	if err != nil {
		return "", manage.UpsertInput{}, err
	}
	return ownerUserID, manage.UpsertInput{
		Name:                  request.Name,
		Source:                source,
		Target:                manage.TargetInput{Ref: targetRef},
		MessageTemplate:       request.Template,
		MentionHints:          converter.FromScheduleMentionHints(request.Mentions),
		MentionHintsSet:       request.Mentions != nil,
		AlsoStartThread:       request.StartThread,
		HumanNotificationMode: request.HumanNotificationMode,
		IdempotencyKey:        request.IdempotencyKey,
		ExpectedUpdatedAt:     request.ExpectedUpdatedAt,
	}, nil
}

// scheduleListTargetKind derives the schedule target kind from the author
// target syntax. The resolved scope id is obtained separately via
// ResolveWritableAgentTargetScopeID; this mirrors the kind classification used
// by schedule target resolution (DM, reply thread, channel; a team reference
// with a short id is a reply thread).
func scheduleListTargetKind(target string) (manage.TargetKind, error) {
	parsed, err := domain.ParseMessageTargetReference(target)
	if err != nil {
		return "", err
	}
	switch parsed.Kind {
	case domain.MessageTargetKindDM:
		return manage.TargetKindDM, nil
	case domain.MessageTargetKindReplyThread:
		return manage.TargetKindReplyThread, nil
	case domain.MessageTargetKindTask:
		return manage.TargetKindTask, nil
	case domain.MessageTargetKindTeam:
		if strings.TrimSpace(parsed.Team.ShortID) != "" {
			return manage.TargetKindReplyThread, nil
		}
		return manage.TargetKindChannel, nil
	case domain.MessageTargetKindChannel:
		return manage.TargetKindChannel, nil
	default:
		return "", badRequest("target does not support schedules")
	}
}
