package agent

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	"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/core/domain"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
)

// maxSetClaimBatch bounds a single set-claim batch. It mirrors the
// Desktop task tool cap so a direct HTTP call cannot bypass it to trigger an
// unbounded claim/release mutation.
const maxSetClaimBatch = 20

func (h *TaskHandler) list(request api.AgentScopedRequest) (api.Response, error) {
	input, err := taskListRequest(request)
	if err != nil {
		return api.Response{}, err
	}
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		dto.AgentTaskLocator{Channel: input.Channel},
		"",
		agentTaskTargetBoard,
	)
	if err != nil {
		return api.Response{}, err
	}
	response, err := h.taskBoard.ListTasks(request.Context(), ListTasksRequest{
		Context:         NewAgentServiceContext(request.Principal.WorkspaceID, request.Principal.AgentID, request.Principal.MemberID),
		Board:           target.boardRef(),
		Status:          input.Status,
		IncludeSubtasks: input.IncludeSubtasks,
		Limit:           input.Limit,
		Cursor:          input.Cursor,
		SearchQuery:     input.Query,
		SortMode:        input.Sort,
		QuickFilters:    input.QuickFilters,
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.AgentTaskListResponse{
		ChannelRef: firstQueryValue(target.BoardChannelRef, response.ChannelID),
		Tasks:      converter.ToTaskViews(taskboardTaskViewsForDTO(response.TaskViews)),
		Limit:      response.Limit,
		TotalCount: response.TotalCount,
		HasMore:    response.HasMore,
		NextCursor: response.NextCursor,
	}), nil
}

func taskListRequest(request api.AgentScopedRequest) (dto.AgentTaskListRequest, error) {
	ctx := request.Gin
	status := strings.TrimSpace(ctx.DefaultQuery("status", "active"))
	filter := "active"
	if !strings.EqualFold(status, "active") {
		var err error
		filter, err = parseTaskStatusFilter(status)
		if err != nil {
			return dto.AgentTaskListRequest{}, err
		}
	}
	includeSubtasks := false
	if raw := strings.TrimSpace(ctx.Query("includeSubtasks")); raw != "" {
		var err error
		includeSubtasks, err = parseBool(raw)
		if err != nil {
			return dto.AgentTaskListRequest{}, api.NewHTTPError(http.StatusBadRequest, "includeSubtasks must be a boolean")
		}
	}
	limit, err := ParseLimit(ctx.Query("limit"), 20)
	if err != nil {
		return dto.AgentTaskListRequest{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	if limit > 100 {
		limit = 100
	}
	return dto.AgentTaskListRequest{
		Channel:         strings.TrimSpace(ctx.Query("channel")),
		Query:           strings.TrimSpace(firstQueryValue(ctx.Query("query"), ctx.Query("q"), ctx.Query("search"))),
		Status:          filter,
		IncludeSubtasks: includeSubtasks,
		Limit:           limit,
		Cursor:          strings.TrimSpace(ctx.Query("cursor")),
		Sort:            strings.TrimSpace(ctx.Query("sort")),
		QuickFilters:    normalizeCommaQueryValues(ctx.QueryArray("quickFilters"), ctx.Query("quickFilters")),
	}, nil
}

func parseBool(value string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, errors.New("invalid boolean")
	}
}

func (h *TaskHandler) get(request api.AgentScopedJSON[dto.AgentTaskGetRequest]) (api.Response, error) {
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		request.Body.AgentTaskLocator,
		"",
		agentTaskTargetTask,
	)
	if err != nil {
		return api.Response{}, err
	}
	response, err := h.taskBoard.GetTaskDetail(request.Context(), GetTaskDetailRequest{
		Context: NewAgentServiceContext(request.Principal.WorkspaceID, request.Principal.AgentID, request.Principal.MemberID),
		Board:   target.boardRef(),
		Task:    target.taskRef(),
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.AgentTaskDetailResponse{Detail: converter.ToTaskDetail(taskboardTaskDetailForDTO(response.Detail))}), nil
}

func (h *TaskHandler) del(request api.AgentScopedJSON[dto.AgentTaskDeleteRequest]) (api.Response, error) {
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		request.Body.AgentTaskLocator,
		"",
		agentTaskTargetTask,
	)
	if err != nil {
		return api.Response{}, err
	}
	mode := DeleteTaskSubtaskMode(strings.TrimSpace(request.Body.SubtaskMode))
	switch mode {
	case "", DeleteTaskSubtaskModePromote, DeleteTaskSubtaskModeDelete:
	default:
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "subtaskMode must be promote or delete")
	}
	response, err := h.taskBoard.DeleteTask(request.Context(), DeleteTaskRequest{
		Context:     agentServiceContextFromPrincipal(request.Principal),
		Board:       target.boardRef(),
		Task:        target.taskRef(),
		SubtaskMode: mode,
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.AgentTaskDeleteResponse{
		DeletedTaskNumbers:  response.DeletedTaskNumbers,
		PromotedTaskNumbers: response.PromotedTaskNumbers,
	}), nil
}

func agentServiceContextFromPrincipal(principal storeagent.AuthenticatedAgent) ServiceContext {
	return NewAgentServiceContext(principal.WorkspaceID, principal.AgentID, principal.MemberID)
}

func (h *TaskHandler) create(request api.AgentScopedJSON[dto.AgentTaskCreateRequest]) (api.Response, error) {
	parentTaskScope, target, err := h.createTarget(request)
	if err != nil {
		return api.Response{}, err
	}
	payload, err := agentTaskCreatePayload(request.Body)
	if err != nil {
		return api.Response{}, err
	}
	if payload.ParentNumber > 0 || parentTaskScope != nil {
		createRequest := CreateSubtasksRequest{
			Context:         agentServiceContextFromPrincipal(request.Principal),
			ParentTaskScope: parentTaskScope,
			SourceChannelID: target.BoardChannelID,
			Metadata:        target.Source,
			Titles:          payload.Titles,
			Input:           payload.Input,
		}
		if parentTaskScope == nil {
			createRequest.Board = target.boardRef()
			createRequest.Task = TaskRef{TaskNumber: payload.ParentNumber}
		}
		created, err := h.taskBoard.CreateSubtasks(request.Context(), createRequest)
		if err != nil {
			return api.Response{}, err
		}
		return agentCreatedTasksResponse(
			target,
			created.ChannelID,
			created.CreatedTasks,
			created.Tasks,
		), nil
	}
	createRequest := CreateTasksRequest{
		Context:  agentServiceContextFromPrincipal(request.Principal),
		Board:    target.boardRef(),
		Titles:   payload.Titles,
		Task:     payload.Input,
		Metadata: target.Source,
	}
	created, err := h.taskBoard.CreateTasks(request.Context(), createRequest)
	if err != nil {
		return api.Response{}, err
	}
	return agentCreatedTasksResponse(
		target,
		created.ChannelID,
		created.CreatedTasks,
		created.Tasks,
	), nil
}

func (h *TaskHandler) createTarget(
	request api.AgentScopedJSON[dto.AgentTaskCreateRequest],
) (*domain.MessageScopeValue, agentTaskTarget, error) {
	parentTaskScope, err := agentTaskScope(request.Body.ParentTaskScope, "parentTaskScope")
	if err != nil {
		return nil, agentTaskTarget{}, err
	}
	if parentTaskScope != nil &&
		(strings.TrimSpace(request.Body.Channel) != "" || request.Body.Parent != nil) {
		return nil, agentTaskTarget{}, api.NewHTTPError(
			http.StatusBadRequest,
			"parentTaskScope cannot be combined with channel or parent",
		)
	}

	// Target identifies source provenance; parentTaskScope remains the parent
	// locator and is resolved transactionally by the taskboard service.
	explicitTarget := strings.TrimSpace(request.Body.Target)
	if parentTaskScope != nil {
		target, err := normalizeOptionalAgentTaskCreateSource(
			request.Context(),
			h.resolver,
			request.Principal,
			explicitTarget,
		)
		return parentTaskScope, target, err
	}
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		dto.AgentTaskLocator{Channel: request.Body.Channel},
		explicitTarget,
		agentTaskTargetCreate,
	)
	return nil, target, err
}

type taskCreatePayload struct {
	Titles       []string
	Input        *TaskCreateInput
	ParentNumber int64
}

func agentTaskCreatePayload(request dto.AgentTaskCreateRequest) (taskCreatePayload, error) {
	// The store accepts either a title batch or one rich task, never both.
	titles := make([]string, 0, len(request.Tasks))
	var richItem *dto.AgentTaskWrite
	richCount := 0
	for i := range request.Tasks {
		item := &request.Tasks[i]
		if strings.TrimSpace(item.Title) == "" {
			return taskCreatePayload{}, api.NewHTTPError(http.StatusBadRequest, "title is required for every task")
		}
		if isRichAgentTaskWrite(*item) {
			richCount++
			richItem = item
		} else {
			titles = append(titles, item.Title)
		}
	}
	if len(titles) == 0 {
		titles = nil
	}
	if len(titles) == 0 && richCount == 0 {
		return taskCreatePayload{}, api.NewHTTPError(http.StatusBadRequest, "at least one task is required")
	}
	if richCount > 1 || (richCount == 1 && len(titles) > 0) {
		return taskCreatePayload{}, api.NewHTTPError(
			http.StatusBadRequest,
			"rich task fields may only be set on a single task item",
		)
	}

	var input *TaskCreateInput
	if richItem != nil {
		input = taskCreateInputForPort(converter.FromAgentTaskWrite(richItem))
	}
	parentNumber := int64(0)
	if request.Parent != nil {
		parentNumber = *request.Parent
		if parentNumber <= 0 {
			return taskCreatePayload{}, api.NewHTTPError(http.StatusBadRequest, "parent must be a positive integer")
		}
	}
	return taskCreatePayload{
		Titles:       titles,
		Input:        input,
		ParentNumber: parentNumber,
	}, nil
}

func agentCreatedTasksResponse(
	target agentTaskTarget,
	channelID string,
	createdTasks []CreatedTask,
	tasks []Task,
) api.Response {
	return api.OK(dto.AgentCreatedTasksResponse{
		ChannelRef: firstQueryValue(target.BoardChannelRef, channelID),
		Tasks: converter.ToCreatedTaskSummaries(
			taskboardCreatedTasksForDTO(createdTasks),
			taskboardTasksForDTO(tasks),
		),
	})
}

func agentTaskScope(value *dto.AgentTaskScope, field string) (*domain.MessageScopeValue, error) {
	if value == nil {
		return nil, nil
	}
	scope, err := domain.NormalizeMessageScopeValue(map[string]any{
		"kind":    value.Kind,
		"scopeId": value.ScopeID,
	})
	if err != nil {
		return nil, api.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("%s is invalid: %v", field, err))
	}
	if scope.Kind != domain.MessageScopeKindTask {
		return nil, api.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("%s kind must be task", field))
	}
	return &scope, nil
}

// isRichAgentTaskWrite reports whether a create item carries any
// field beyond Title, selecting the single-rich-task create path.
func isRichAgentTaskWrite(item dto.AgentTaskWrite) bool {
	return item.Description != "" || item.Kind != "" || item.Priority != "" ||
		item.Assignee != ""
}

func (h *TaskHandler) update(request api.AgentScopedJSON[dto.AgentTaskUpdateRequest]) (api.Response, error) {
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		request.Body.AgentTaskLocator,
		"",
		agentTaskTargetTask,
	)
	if err != nil {
		return api.Response{}, err
	}
	// Validate and normalize status (trim + lower) before any write. Status may
	// not be combined with detail fields in one request: lifecycle transitions
	// can fail for reasons beyond format validation (permissions, business
	// state, concurrency), so a mixed update could leave a hard-to-recover
	// partial side effect. Each request is therefore a single operation
	// (detail-only via UpdateTask, or status-only via UpdateTaskStatus); a
	// future store-level single-transaction combined op could re-enable mixed
	// updates safely.
	status := ""
	if request.Body.Status != nil {
		status = strings.ToLower(strings.TrimSpace(*request.Body.Status))
		if status == "" {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must not be empty")
		}
		if !isValidTaskStatus(status) {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must be one of todo, in_progress, in_review, or done")
		}
	}
	hasDetail := hasUpdateDetailPatch(request.Body.AgentTaskPatch)
	if status != "" && hasDetail {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must be updated separately from detail fields")
	}
	if status == "" && !hasDetail {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "at least one patch field is required")
	}
	var task Task
	if hasDetail {
		response, err := h.taskBoard.UpdateTask(request.Context(), UpdateTaskRequest{
			Context: agentServiceContextFromPrincipal(request.Principal),
			Board:   target.boardRef(),
			Task:    target.taskRef(),
			Patch: TaskUpdatePatch{
				Title:              request.Body.Title,
				Description:        request.Body.Description,
				Kind:               request.Body.Kind,
				Priority:           request.Body.Priority,
				AssigneeRef:        request.Body.Assignee,
				AcceptanceCriteria: taskChecklistItemsForPort(converter.FromTaskChecklistItems(request.Body.AcceptanceCriteria)),
				ReviewChecklist:    taskChecklistItemsForPort(converter.FromTaskChecklistItems(request.Body.ReviewChecklist)),
				PinnedMessageRefs:  request.Body.Messages,
				RelevantURLs:       request.Body.URLs,
				WorkspaceRefs:      request.Body.Workspaces,
				BlockedReason:      request.Body.Blocker,
				ParentTaskRef:      request.Body.Parent,
				AttachmentIDs:      request.Body.Attachments,
			},
		})
		if err != nil {
			return api.Response{}, err
		}
		task = response.Task
	}
	if status != "" {
		statusResponse, err := h.taskBoard.UpdateTaskStatus(request.Context(), UpdateTaskStatusRequest{
			Context: agentServiceContextFromPrincipal(request.Principal),
			Board:   target.boardRef(),
			Task:    target.taskRef(),
			Status:  status,
		})
		if err != nil {
			return api.Response{}, err
		}
		task = statusResponse.Task
	}
	return api.OK(dto.AgentTaskResponse{Task: converter.ToTask(taskboardTaskForDTO(task))}), nil
}

// hasUpdateDetailPatch reports whether the patch carries any non-status
// detail mutation. Status is excluded so callers cannot mix a status change
// with detail fields in a single request.
func hasUpdateDetailPatch(patch dto.AgentTaskPatch) bool {
	return patch.Title != nil ||
		patch.Description != nil ||
		patch.Kind != nil ||
		patch.Priority != nil ||
		patch.Assignee != nil ||
		patch.Blocker != nil ||
		patch.Parent != nil ||
		patch.AcceptanceCriteria != nil ||
		patch.ReviewChecklist != nil ||
		patch.Messages != nil ||
		patch.URLs != nil ||
		patch.Workspaces != nil ||
		patch.Attachments != nil
}

func (h *TaskHandler) checkpoint(request api.AgentScopedJSON[dto.AgentTaskCheckpointRequest]) (api.Response, error) {
	summary := strings.TrimSpace(request.Body.Summary)
	if summary == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "summary is required")
	}
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		request.Body.AgentTaskLocator,
		"",
		agentTaskTargetTask,
	)
	if err != nil {
		return api.Response{}, err
	}
	response, err := h.taskBoard.AddTaskCheckpoint(request.Context(), AddTaskCheckpointRequest{
		Context:    agentServiceContextFromPrincipal(request.Principal),
		Board:      target.boardRef(),
		Task:       target.taskRef(),
		Checkpoint: TaskCheckpointInput{Text: summary},
	})
	if err != nil {
		return api.Response{}, err
	}
	return api.OK(dto.AgentTaskResponse{Task: converter.ToTask(taskboardTaskForDTO(response.Task))}), nil
}

func (h *TaskHandler) setClaim(request api.AgentScopedJSON[dto.AgentTaskSetClaimRequest]) (api.Response, error) {
	if len(request.Body.Numbers) == 0 {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "numbers must contain at least one task")
	}
	if len(request.Body.Numbers) > maxSetClaimBatch {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("numbers must contain at most %d items", maxSetClaimBatch))
	}
	// Reject non-positive numbers and de-duplicate so a direct HTTP call cannot
	// bypass Desktop validation to trigger unbounded or repeated mutations.
	taskNumbers := make([]int64, 0, len(request.Body.Numbers))
	seen := make(map[int64]struct{}, len(request.Body.Numbers))
	for _, number := range request.Body.Numbers {
		if number <= 0 {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "numbers must contain only positive integers")
		}
		if _, ok := seen[number]; ok {
			continue
		}
		seen[number] = struct{}{}
		taskNumbers = append(taskNumbers, number)
	}
	target, err := normalizeAgentTaskTarget(
		request.Context(),
		h.resolver,
		request.Principal,
		dto.AgentTaskLocator{Channel: request.Body.Channel},
		"",
		agentTaskTargetBoard,
	)
	if err != nil {
		return api.Response{}, err
	}
	context := agentServiceContextFromPrincipal(request.Principal)
	board := target.boardRef()
	if request.Body.Claimed {
		taskRefs := make([]TaskRef, 0, len(taskNumbers))
		for _, number := range taskNumbers {
			taskRefs = append(taskRefs, TaskRef{TaskNumber: number})
		}
		response, err := h.taskBoard.ClaimTasks(request.Context(), ClaimTasksRequest{
			Context: context,
			Board:   board,
			Tasks:   taskRefs,
		})
		if err != nil {
			return api.Response{}, err
		}
		claimed, failed := summarizeClaimResults(response.Results)
		return api.OK(dto.AgentTaskSetClaimResponse{Claimed: claimed, Failed: failed}), nil
	}
	claimed := 0
	failed := make([]dto.AgentTaskClaimFailure, 0)
	for _, number := range taskNumbers {
		if _, err := h.taskBoard.UnclaimTask(request.Context(), UnclaimTaskRequest{
			Context: context,
			Board:   board,
			Task:    TaskRef{TaskNumber: number},
		}); err != nil {
			failed = append(failed, dto.AgentTaskClaimFailure{TaskNumber: number, Reason: agentErrorFor(err).Error})
			continue
		}
		claimed++
	}
	return api.OK(dto.AgentTaskSetClaimResponse{Claimed: claimed, Failed: failed}), nil
}

func summarizeClaimResults(results []ClaimResult) (int, []dto.AgentTaskClaimFailure) {
	claimed := 0
	failed := make([]dto.AgentTaskClaimFailure, 0)
	for _, result := range results {
		if result.Success {
			claimed++
			continue
		}
		failed = append(failed, dto.AgentTaskClaimFailure{TaskNumber: result.TaskNumber, Reason: result.Reason})
	}
	return claimed, failed
}

func isValidTaskStatus(status string) bool {
	switch strings.ToLower(status) {
	case "todo", "in_progress", "in_review", "done":
		return true
	}
	return false
}

func agentTaskErrorClassifier(err error) (api.ErrorResponse, bool) {
	status := HTTPStatus(err)
	response := agentErrorFor(err)
	return api.ErrorResponse{
		Status:  status,
		Message: response.Error,
		Code:    response.Code,
		Details: response.Details,
	}, true
}
