package converter

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/im"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

func FromThreadInboxCursorToken(token string) (message.ThreadInboxCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return message.ThreadInboxCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return message.ThreadInboxCursor{}, fmt.Errorf("invalid thread inbox cursor")
	}
	var cursor dto.ThreadInboxCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return message.ThreadInboxCursor{}, fmt.Errorf("invalid thread inbox cursor")
	}
	threadChannelID := strings.TrimSpace(cursor.ThreadChannelID)
	if cursor.SortAt.IsZero() || threadChannelID == "" {
		return message.ThreadInboxCursor{}, fmt.Errorf("invalid thread inbox cursor")
	}
	return message.ThreadInboxCursor{SortAt: cursor.SortAt.UTC(), ThreadChannelID: threadChannelID}, nil
}

func ToThreadInboxCursorToken(cursor message.ThreadInboxCursor) string {
	if cursor.IsZero() {
		return ""
	}
	payload, err := json.Marshal(dto.ThreadInboxCursor{SortAt: cursor.SortAt.UTC(), ThreadChannelID: strings.TrimSpace(cursor.ThreadChannelID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func FromConversationCursorToken(token string) (message.ConversationCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return message.ConversationCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return message.ConversationCursor{}, fmt.Errorf("invalid conversation cursor")
	}
	var cursor dto.ConversationCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return message.ConversationCursor{}, fmt.Errorf("invalid conversation cursor")
	}
	scopeID := strings.TrimSpace(cursor.ScopeID)
	if cursor.SortAt.IsZero() || scopeID == "" {
		return message.ConversationCursor{}, fmt.Errorf("invalid conversation cursor")
	}
	var pinnedAt *time.Time
	if cursor.PinnedAt != nil {
		normalizedPinnedAt := cursor.PinnedAt.UTC()
		if normalizedPinnedAt.IsZero() {
			return message.ConversationCursor{}, fmt.Errorf("invalid conversation cursor")
		}
		pinnedAt = &normalizedPinnedAt
	}
	return message.ConversationCursor{PinnedAt: pinnedAt, SortAt: cursor.SortAt.UTC(), ScopeID: scopeID}, nil
}

func ToConversationCursorToken(cursor message.ConversationCursor) string {
	if cursor.IsZero() {
		return ""
	}
	var pinnedAt *time.Time
	if cursor.PinnedAt != nil {
		normalizedPinnedAt := cursor.PinnedAt.UTC()
		pinnedAt = &normalizedPinnedAt
	}
	payload, err := json.Marshal(dto.ConversationCursor{PinnedAt: pinnedAt, SortAt: cursor.SortAt.UTC(), ScopeID: strings.TrimSpace(cursor.ScopeID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

// FromIMConversationCursorToken decodes a browser IM conversation-index cursor token.
func FromIMConversationCursorToken(token string) (im.ConversationCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return im.ConversationCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return im.ConversationCursor{}, fmt.Errorf("invalid conversation index cursor")
	}
	var cursor dto.IMConversationCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return im.ConversationCursor{}, fmt.Errorf("invalid conversation index cursor")
	}
	conversationID := strings.TrimSpace(cursor.ConversationID)
	if cursor.SortAt.IsZero() || conversationID == "" {
		return im.ConversationCursor{}, fmt.Errorf("invalid conversation index cursor")
	}
	var pinnedAt *time.Time
	if cursor.PinnedAt != nil {
		normalizedPinnedAt := cursor.PinnedAt.UTC()
		if normalizedPinnedAt.IsZero() {
			return im.ConversationCursor{}, fmt.Errorf("invalid conversation index cursor")
		}
		pinnedAt = &normalizedPinnedAt
	}
	return im.ConversationCursor{PinnedAt: pinnedAt, SortAt: cursor.SortAt.UTC(), ConversationID: conversationID}, nil
}

// ToIMConversationCursorToken encodes a browser IM conversation-index cursor token.
func ToIMConversationCursorToken(cursor im.ConversationCursor) string {
	conversationID := strings.TrimSpace(cursor.ConversationID)
	if cursor.SortAt.IsZero() || conversationID == "" {
		return ""
	}
	var pinnedAt *time.Time
	if cursor.PinnedAt != nil {
		normalizedPinnedAt := cursor.PinnedAt.UTC()
		pinnedAt = &normalizedPinnedAt
	}
	payload, err := json.Marshal(dto.IMConversationCursor{PinnedAt: pinnedAt, SortAt: cursor.SortAt.UTC(), ConversationID: conversationID})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

// FromMessageSearchCursorToken decodes a browser message-search cursor token.
func FromMessageSearchCursorToken(token string) (message.MessageSearchCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return message.MessageSearchCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return message.MessageSearchCursor{}, fmt.Errorf("invalid search cursor")
	}
	var cursor dto.MessageSearchCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return message.MessageSearchCursor{}, fmt.Errorf("invalid search cursor")
	}
	result := message.MessageSearchCursor{
		Rank:      cursor.Rank,
		GlobalSeq: cursor.GlobalSeq,
		ScopeID:   strings.TrimSpace(cursor.ScopeID),
		Seq:       cursor.Seq,
		MessageID: strings.TrimSpace(cursor.MessageID),
	}
	if result.Rank < 0 || result.GlobalSeq < 0 || result.Seq < 0 {
		return message.MessageSearchCursor{}, fmt.Errorf("invalid search cursor")
	}
	if result.IsZero() {
		return message.MessageSearchCursor{}, nil
	}
	return result, nil
}

// ToMessageSearchCursorToken encodes a browser message-search cursor token.
func ToMessageSearchCursorToken(cursor message.MessageSearchCursor) string {
	if cursor.IsZero() {
		return ""
	}
	payload, err := json.Marshal(dto.MessageSearchCursor{
		Rank:      cursor.Rank,
		GlobalSeq: cursor.GlobalSeq,
		ScopeID:   strings.TrimSpace(cursor.ScopeID),
		Seq:       cursor.Seq,
		MessageID: strings.TrimSpace(cursor.MessageID),
	})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func FromTaskBrowseCursorToken(token string, sortMode string) (taskboard.TaskBrowseCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return taskboard.TaskBrowseCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
	}
	var wire dto.TaskBrowseCursor
	if err := json.Unmarshal(payload, &wire); err != nil {
		return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
	}
	wire.SortMode = normalizeTaskSortMode(wire.SortMode)
	if wire.SortMode != normalizeTaskSortMode(sortMode) || strings.TrimSpace(wire.TaskID) == "" || strings.TrimSpace(wire.Value) == "" {
		return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
	}
	cursor := taskboard.TaskBrowseCursor{SortMode: wire.SortMode, TaskID: strings.TrimSpace(wire.TaskID)}
	switch wire.SortMode {
	case "recently_updated":
		var value dto.TaskBrowseRecentCursorValue
		if err := decodeTaskBrowseCursorValue(wire.Value, &value); err != nil {
			return taskboard.TaskBrowseCursor{}, err
		}
		activityAt, err := parseTaskBrowseCursorTime(value.ActivityAt)
		if err != nil || value.TaskNumber <= 0 {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		cursor.ActivityAt = activityAt
		cursor.TaskNumber = value.TaskNumber
	case "created_time":
		var value dto.TaskBrowseCreatedTimeCursorValue
		if err := decodeTaskBrowseCursorValue(wire.Value, &value); err != nil {
			return taskboard.TaskBrowseCursor{}, err
		}
		createdAt, err := parseTaskBrowseCursorTime(value.CreatedAt)
		if err != nil {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		activityAt, err := parseTaskBrowseCursorTime(value.ActivityAt)
		if err != nil || value.TaskNumber <= 0 {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		cursor.CreatedAt = createdAt
		cursor.ActivityAt = activityAt
		cursor.TaskNumber = value.TaskNumber
	case "title":
		var value dto.TaskBrowseTitleCursorValue
		if err := decodeTaskBrowseCursorValue(wire.Value, &value); err != nil {
			return taskboard.TaskBrowseCursor{}, err
		}
		createdAt, err := parseTaskBrowseCursorTime(value.CreatedAt)
		if err != nil || value.TaskNumber <= 0 || strings.TrimSpace(value.Title) == "" {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		cursor.Title = strings.TrimSpace(value.Title)
		cursor.CreatedAt = createdAt
		cursor.TaskNumber = value.TaskNumber
	case "priority":
		var value dto.TaskBrowsePriorityCursorValue
		if err := decodeTaskBrowseCursorValue(wire.Value, &value); err != nil {
			return taskboard.TaskBrowseCursor{}, err
		}
		createdAt, err := parseTaskBrowseCursorTime(value.CreatedAt)
		if err != nil || value.TaskNumber <= 0 {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		cursor.PriorityMissing = value.Missing
		cursor.PriorityLabel = strings.TrimSpace(value.Label)
		cursor.CreatedAt = createdAt
		cursor.TaskNumber = value.TaskNumber
	case "manual":
		fallthrough
	default:
		var value dto.TaskBrowseManualCursorValue
		if err := decodeTaskBrowseCursorValue(wire.Value, &value); err != nil {
			return taskboard.TaskBrowseCursor{}, err
		}
		createdAt, err := parseTaskBrowseCursorTime(value.CreatedAt)
		if err != nil || value.TaskNumber <= 0 {
			return taskboard.TaskBrowseCursor{}, fmt.Errorf("invalid browse cursor")
		}
		cursor.CreatedAt = createdAt
		cursor.TaskNumber = value.TaskNumber
	}
	return cursor, nil
}

func ToTaskBrowseCursorToken(cursor taskboard.TaskBrowseCursor) string {
	if cursor.IsZero() {
		return ""
	}
	value := ""
	switch normalizeTaskSortMode(cursor.SortMode) {
	case "recently_updated":
		value = encodeTaskBrowseCursorValue(dto.TaskBrowseRecentCursorValue{ActivityAt: formatTaskBrowseCursorTime(cursor.ActivityAt), TaskNumber: cursor.TaskNumber})
	case "created_time":
		value = encodeTaskBrowseCursorValue(dto.TaskBrowseCreatedTimeCursorValue{CreatedAt: formatTaskBrowseCursorTime(cursor.CreatedAt), ActivityAt: formatTaskBrowseCursorTime(cursor.ActivityAt), TaskNumber: cursor.TaskNumber})
	case "title":
		value = encodeTaskBrowseCursorValue(dto.TaskBrowseTitleCursorValue{Title: strings.TrimSpace(cursor.Title), CreatedAt: formatTaskBrowseCursorTime(cursor.CreatedAt), TaskNumber: cursor.TaskNumber})
	case "priority":
		value = encodeTaskBrowseCursorValue(dto.TaskBrowsePriorityCursorValue{Missing: cursor.PriorityMissing, Label: strings.TrimSpace(cursor.PriorityLabel), CreatedAt: formatTaskBrowseCursorTime(cursor.CreatedAt), TaskNumber: cursor.TaskNumber})
	case "manual":
		fallthrough
	default:
		value = encodeTaskBrowseCursorValue(dto.TaskBrowseManualCursorValue{CreatedAt: formatTaskBrowseCursorTime(cursor.CreatedAt), TaskNumber: cursor.TaskNumber})
	}
	if value == "" {
		return ""
	}
	payload, err := json.Marshal(dto.TaskBrowseCursor{SortMode: normalizeTaskSortMode(cursor.SortMode), Value: value, TaskID: strings.TrimSpace(cursor.TaskID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func FromTaskBrowseColumnCursorTokens(tokens map[string]string, sortMode string) (map[string]taskboard.TaskBrowseCursor, error) {
	if len(tokens) == 0 {
		return nil, nil
	}
	items := make(map[string]taskboard.TaskBrowseCursor, len(tokens))
	for key, token := range tokens {
		trimmedKey := strings.TrimSpace(key)
		cursor, err := FromTaskBrowseCursorToken(token, sortMode)
		if err != nil {
			return nil, err
		}
		if trimmedKey == "" || cursor.IsZero() {
			continue
		}
		items[trimmedKey] = cursor
	}
	if len(items) == 0 {
		return nil, nil
	}
	return items, nil
}

func ToTaskBrowseColumnCursorTokens(cursors map[string]taskboard.TaskBrowseCursor) map[string]string {
	if len(cursors) == 0 {
		return nil
	}
	items := make(map[string]string, len(cursors))
	for key, cursor := range cursors {
		if token := ToTaskBrowseCursorToken(cursor); token != "" {
			items[key] = token
		}
	}
	if len(items) == 0 {
		return nil
	}
	return items
}

func FromTaskTimelineCursorToken(token string) (taskboard.TaskTimelineCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return taskboard.TaskTimelineCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return taskboard.TaskTimelineCursor{}, fmt.Errorf("invalid timeline cursor")
	}
	var cursor dto.TaskTimelineCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return taskboard.TaskTimelineCursor{}, fmt.Errorf("invalid timeline cursor")
	}
	id := strings.TrimSpace(cursor.ID)
	if cursor.Seq < 0 || cursor.CreatedAt.IsZero() || id == "" {
		return taskboard.TaskTimelineCursor{}, fmt.Errorf("invalid timeline cursor")
	}
	return taskboard.TaskTimelineCursor{Seq: cursor.Seq, CreatedAt: cursor.CreatedAt.UTC(), ID: id}, nil
}

func ToTaskTimelineCursorToken(cursor taskboard.TaskTimelineCursor) string {
	if cursor.IsZero() {
		return ""
	}
	payload, err := json.Marshal(dto.TaskTimelineCursor{Seq: cursor.Seq, CreatedAt: cursor.CreatedAt.UTC(), ID: strings.TrimSpace(cursor.ID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

// FromTaskSubtasksCursorToken decodes an opaque direct-child page continuation.
// A blank token means the first page. Any non-blank token must decode to a UTC
// createdAt and a nonblank id, otherwise the caller must reject it with 400.
func FromTaskSubtasksCursorToken(token string) (taskboard.SubtaskPageCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return taskboard.SubtaskPageCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return taskboard.SubtaskPageCursor{}, fmt.Errorf("invalid subtask cursor")
	}
	var cursor dto.TaskSubtaskCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return taskboard.SubtaskPageCursor{}, fmt.Errorf("invalid subtask cursor")
	}
	id := strings.TrimSpace(cursor.ID)
	if cursor.CreatedAt.IsZero() || id == "" {
		return taskboard.SubtaskPageCursor{}, fmt.Errorf("invalid subtask cursor")
	}
	return taskboard.SubtaskPageCursor{CreatedAt: cursor.CreatedAt.UTC(), ID: id}, nil
}

func ToTaskSubtasksCursorToken(cursor taskboard.SubtaskPageCursor) string {
	if cursor.IsZero() {
		return ""
	}
	payload, err := json.Marshal(dto.TaskSubtaskCursor{CreatedAt: cursor.CreatedAt.UTC(), ID: strings.TrimSpace(cursor.ID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func FromRelatedTaskCursorToken(token string) (taskboard.RelatedTaskCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return taskboard.RelatedTaskCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return taskboard.RelatedTaskCursor{}, fmt.Errorf("invalid related task cursor")
	}
	var cursor dto.RelatedTaskCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return taskboard.RelatedTaskCursor{}, fmt.Errorf("invalid related task cursor")
	}
	taskID := strings.TrimSpace(cursor.TaskID)
	if cursor.TaskNumber <= 0 || taskID == "" {
		return taskboard.RelatedTaskCursor{}, fmt.Errorf("invalid related task cursor")
	}
	return taskboard.RelatedTaskCursor{TaskNumber: cursor.TaskNumber, TaskID: taskID}, nil
}

func ToRelatedTaskCursorToken(cursor taskboard.RelatedTaskCursor) string {
	if cursor.IsZero() {
		return ""
	}
	payload, err := json.Marshal(dto.RelatedTaskCursor{TaskNumber: cursor.TaskNumber, TaskID: strings.TrimSpace(cursor.TaskID)})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(payload)
}

func FromDaemonWorkspaceFile(file dto.DaemonWorkspaceFile) (agent.WorkspaceFile, error) {
	if file.IsDirectory == nil {
		return agent.WorkspaceFile{}, fmt.Errorf("workspace file is missing isDirectory")
	}
	return agent.WorkspaceFile{
		Name:           file.Name,
		Path:           file.Path,
		IsDirectory:    *file.IsDirectory,
		Size:           file.Size,
		ModifiedAt:     file.ModifiedAt,
		ChildDirCount:  file.ChildDirCount,
		ChildFileCount: file.ChildFileCount,
	}, nil
}

func FromDaemonWorkspaceFiles(files []dto.DaemonWorkspaceFile) ([]agent.WorkspaceFile, error) {
	if files == nil {
		return nil, nil
	}
	items := make([]agent.WorkspaceFile, 0, len(files))
	for _, file := range files {
		converted, err := FromDaemonWorkspaceFile(file)
		if err != nil {
			return nil, err
		}
		items = append(items, converted)
	}
	return items, nil
}

func decodeTaskBrowseCursorValue(raw string, target any) error {
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), target); err != nil {
		return fmt.Errorf("invalid browse cursor")
	}
	return nil
}

func encodeTaskBrowseCursorValue(value any) string {
	payload, err := json.Marshal(value)
	if err != nil {
		return ""
	}
	return string(payload)
}

func parseTaskBrowseCursorTime(raw string) (time.Time, error) {
	parsed, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(raw))
	if err != nil {
		return time.Time{}, err
	}
	return parsed.UTC(), nil
}

func formatTaskBrowseCursorTime(value time.Time) string {
	return value.UTC().Format(time.RFC3339Nano)
}

func normalizeTaskSortMode(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "recently_updated", "created_time", "title", "priority", "manual":
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return "manual"
	}
}
