package task

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
)

// Get handles GET /api/v1/workspaces/:workspaceId/tasks/:taskId.
// It returns the full detail of a task by its ID.
func (handler *DetailHandler) Get(r api.WorkspaceScopedRequest) (api.Response, error) {
	workspaceID := r.WorkspaceID()
	includeTimeline, err := taskDetailIncludeTimeline(r.Gin.Query("includeTimeline"))
	if err != nil {
		return api.Response{}, err
	}

	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}

	response, err := handler.taskBoard.GetTaskDetail(r.Context(), GetTaskDetailRequest{
		Context:      NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Task:         TaskRef{TaskID: taskID},
		SkipTimeline: !includeTimeline,
	})
	if err != nil {
		return api.Response{}, err
	}
	overlayTaskDetailUnread(handler.unreadRuntime, workspaceID, r.Workspace.MemberID, &response.Detail)

	return api.OK(converter.ToTaskDetailResponse(taskboardGetTaskDetailResponseForDTO(response))), nil
}

// GetByNumber handles GET /api/v1/workspaces/:workspaceId/channels/:channelId/tasks/:taskNumber.
// It returns the full detail of a task by its channel-relative number.
func (handler *DetailHandler) GetByNumber(r api.WorkspaceScopedRequest) (api.Response, error) {
	workspaceID := r.WorkspaceID()
	includeTimeline, err := taskDetailIncludeTimeline(r.Gin.Query("includeTimeline"))
	if err != nil {
		return api.Response{}, err
	}

	channelID := strings.TrimSpace(r.Gin.Param("channelId"))
	if channelID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "channelId is required")
	}
	taskNumber, err := strconv.ParseInt(strings.TrimSpace(r.Gin.Param("taskNumber")), 10, 64)
	if err != nil || taskNumber <= 0 {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskNumber must be a positive integer")
	}

	response, err := handler.taskBoard.GetTaskDetail(r.Context(), GetTaskDetailRequest{
		Context:      NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Board:        BoardRef{ChannelID: channelID},
		Task:         TaskRef{TaskNumber: taskNumber},
		SkipTimeline: !includeTimeline,
	})
	if err != nil {
		return api.Response{}, err
	}
	overlayTaskDetailUnread(handler.unreadRuntime, workspaceID, r.Workspace.MemberID, &response.Detail)

	return api.OK(converter.ToTaskDetailResponse(taskboardGetTaskDetailResponseForDTO(response))), nil
}

func taskDetailIncludeTimeline(raw string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, api.NewHTTPError(http.StatusBadRequest, "includeTimeline must be true or false")
	}
}

// TimelineList handles GET /api/tasks/:taskId/timeline.
// It returns the expected task timeline entries for a task.
func (handler *DetailHandler) TimelineList(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}
	cursor, err := converter.FromTaskTimelineCursorToken(r.Gin.Query("cursor"))
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, err.Error())
	}

	response, err := handler.taskBoard.GetTaskTimeline(r.Context(), GetTaskTimelineRequest{
		Context: NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Task:    TaskRef{TaskID: taskID},
		Cursor:  taskTimelineCursorForDTO(cursor),
	})
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(converter.ToTaskTimelineListResponse(taskboardGetTaskTimelineResponseForDTO(response))), nil
}

// MarkRead handles POST /api/tasks/:taskId/read-state.
// It advances the current user's Task message and typed-Activity cursors and
// clears the shared manual-unread marker.
func (handler *DetailHandler) MarkRead(r api.WorkspaceScopedRequest) (api.Response, error) {
	workspaceID := r.WorkspaceID()

	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}
	if handler.taskBoard == nil {
		return api.Response{}, api.NewHTTPError(http.StatusNotImplemented, "Task read state is unavailable")
	}

	response, err := handler.taskBoard.MarkTaskRead(r.Context(), MarkTaskReadRequest{
		Context: NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Task:    TaskRef{TaskID: taskID},
	})
	if err != nil {
		return api.Response{}, err
	}
	applyTaskReadCursors(handler.unreadRuntime, workspaceID, r.Workspace.MemberID, response.Task, response.ReadState)
	if handler.conversationReadProjector != nil {
		if err := handler.conversationReadProjector.ProjectTaskConversationRead(r.Context(), TaskConversationReadProjection{
			WorkspaceID:         workspaceID,
			UserID:              r.UserID(),
			MemberID:            r.Workspace.MemberID,
			TaskID:              response.Task.ID,
			ScopeID:             response.Task.TaskScopeID,
			LastMessageReadSeq:  response.ReadState.LastMessageReadSeq,
			LastActivityReadSeq: response.ReadState.LastActivityReadSeq,
		}); err != nil && handler.logger != nil {
			// Durable read state has already committed. Match the generic Inbox
			// mark-read behavior: return success and let stream recovery repair a
			// transient self-projection failure.
			handler.logger.WarnContext(r.Context(), "project Task conversation read state", "task_id", response.Task.ID, "error", err)
		}
	}
	if response.Task.ReadState != nil {
		overlayTaskUnread(handler.unreadRuntime, workspaceID, r.Workspace.MemberID, &response.Task)
		response.ReadState = *response.Task.ReadState
	}

	return api.OK(markTaskReadResponseForDTO(response)), nil
}

// Patch handles PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId.
// It applies a partial update to a task's fields, or a status-only lifecycle
// transition when the request carries status.
func (handler *DetailHandler) Patch(r api.WorkspaceScopedJSON[dto.TaskUpdateRequest]) (api.Response, error) {
	request := r.Body
	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}

	if request.Status != nil {
		status := strings.ToLower(strings.TrimSpace(*request.Status))
		if status == "" {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must not be empty")
		}
		if !isKnownTaskLifecycleStatus(status) {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must be one of todo, in_progress, in_review, or done")
		}
		if hasTaskDetailUpdatePatch(request) {
			return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "status must be updated separately from detail fields")
		}
		response, err := handler.taskBoard.UpdateTaskStatus(r.Context(), UpdateTaskStatusRequest{
			Context: NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
			Task:    TaskRef{TaskID: taskID},
			Status:  status,
		})
		if err != nil {
			return api.Response{}, err
		}
		return api.OK(converter.ToTaskResponse(taskboardTaskForDTO(response.Task))), nil
	}

	response, err := handler.taskBoard.UpdateTask(r.Context(), UpdateTaskRequest{
		Context: NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Task:    TaskRef{TaskID: taskID},
		Patch: TaskUpdatePatch{
			Title:              request.Title,
			Description:        request.Description,
			Kind:               request.Kind,
			Priority:           request.Priority,
			KindOptionID:       request.KindOptionID,
			PriorityOptionID:   request.PriorityOptionID,
			AssigneeRef:        request.AssigneeRef,
			AcceptanceCriteria: taskChecklistItemsForPort(converter.FromTaskChecklistItems(request.AcceptanceCriteria)),
			ReviewChecklist:    taskChecklistItemsForPort(converter.FromTaskChecklistItems(request.ReviewChecklist)),
			PinnedMessageRefs:  request.PinnedMessageRefs,
			RelevantURLs:       request.RelevantURLs,
			WorkspaceRefs:      request.WorkspaceRefs,
			BlockedReason:      request.BlockedReason,
			ParentTaskRef:      request.ParentTaskRef,
			AttachmentIDs:      request.AttachmentIDs,
		},
	})
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(converter.ToTaskResponse(taskboardTaskForDTO(response.Task))), nil
}

func isKnownTaskLifecycleStatus(status string) bool {
	switch status {
	case "todo", "in_progress", "in_review", "done":
		return true
	default:
		return false
	}
}

func hasTaskDetailUpdatePatch(request dto.TaskUpdateRequest) bool {
	return request.Title != nil ||
		request.Description != nil ||
		request.Kind != nil ||
		request.Priority != nil ||
		request.KindOptionID != nil ||
		request.PriorityOptionID != nil ||
		request.AssigneeRef != nil ||
		request.AcceptanceCriteria != nil ||
		request.ReviewChecklist != nil ||
		request.PinnedMessageRefs != nil ||
		request.RelevantURLs != nil ||
		request.WorkspaceRefs != nil ||
		request.BlockedReason != nil ||
		request.ParentTaskRef != nil ||
		request.AttachmentIDs != nil
}

// Delete handles DELETE /api/v1/workspaces/:workspaceId/tasks/:taskId.
// It permanently deletes a task and applies the requested subtask handling mode
// when deleting a parent task.
func (handler *DetailHandler) Delete(r api.WorkspaceScopedRequest) (api.Response, error) {
	taskID := strings.TrimSpace(r.Gin.Param("taskId"))
	if taskID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "taskId is required")
	}
	body, _, err := api.BindEmptyOrJSON[dto.TaskDeleteRequest](r.Gin)
	if err != nil {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "Invalid request body")
	}

	_, err = handler.taskBoard.DeleteTask(r.Context(), DeleteTaskRequest{
		Context:     NewUserServiceContext(r.WorkspaceID(), r.User.User.ID, r.User.User.Name),
		Task:        TaskRef{TaskID: taskID},
		SubtaskMode: DeleteTaskSubtaskMode(strings.TrimSpace(body.SubtaskMode)),
	})
	if err != nil {
		return api.Response{}, err
	}

	return api.OK(dto.EmptyResponse{}), nil
}
