package taskboard

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/tasktimeline"
)

const (
	taskEventUpdated                = "updated"
	taskEventCreated                = "created"
	taskEventAssigned               = "assigned"
	taskEventUnassigned             = "unassigned"
	taskEventBlocked                = "blocked"
	taskEventUnblocked              = "unblocked"
	taskEventStatusChanged          = "status_changed"
	taskEventChecklistUpdated       = "checklist_updated"
	taskEventCheckpointAdded        = "checkpoint_added"
	taskEventDiscussionAdded        = "discussion_added"
	taskEventArtifactAdded          = "artifact_added"
	taskEventReviewerAssigned       = "reviewer_assigned"
	taskEventReviewRequested        = "review_requested"
	taskEventReviewApproved         = "review_approved"
	taskEventReviewChangesRequested = "review_changes_requested"
	taskEventReviewCancelled        = "review_cancelled"
)

func (txs TxStore) loadAccessibleAgentTaskID(ctx context.Context, tx coredb.Tx, agent AgentActor, board BoardRef, taskNumber int64) (string, string, error) {
	channelID, err := txs.resolveAccessibleAgentBoard(ctx, tx, agent, board)
	if err != nil {
		return "", "", err
	}
	task, err := txs.taskByNumber(ctx, tx, agent.WorkspaceID, channelID, taskNumber)
	if err != nil {
		return "", "", err
	}
	return channelID, task.ID, nil
}

func parseTaskNumberReference(value string) (int64, bool) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return 0, false
	}
	if strings.HasPrefix(trimmed, "#") {
		trimmed = trimmed[1:]
	}
	if strings.HasPrefix(strings.ToLower(trimmed), "t") {
		trimmed = trimmed[1:]
	}
	if trimmed == "" {
		return 0, false
	}
	parsed, err := strconv.ParseInt(trimmed, 10, 64)
	if err != nil || parsed <= 0 {
		return 0, false
	}
	return parsed, true
}

func (txs TxStore) resolveParentTaskSummary(
	ctx context.Context,
	queryer coredb.QueryExecer,
	workspaceID string,
	channelID string,
	parentTaskRef string,
) (*Task, error) {
	trimmedParentTaskRef := strings.TrimSpace(parentTaskRef)
	if trimmedParentTaskRef == "" {
		return nil, nil
	}

	if taskNumber, ok := parseTaskNumberReference(trimmedParentTaskRef); ok {
		task, err := txs.taskByNumber(ctx, queryer, workspaceID, channelID, taskNumber)
		if err != nil {
			if coredb.IsNotFound(err) {
				return nil, nil
			}
			return nil, fmt.Errorf("load parent task summary by number: %w", err)
		}
		return &task, nil
	}

	task, err := txs.taskByID(ctx, queryer, workspaceID, trimmedParentTaskRef)
	if err != nil {
		if coredb.IsNotFound(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("load parent task summary by id: %w", err)
	}
	if strings.TrimSpace(task.ChannelID) != strings.TrimSpace(channelID) {
		return nil, nil
	}
	return &task, nil
}

func taskTypedEventIntent(taskID string, actorRef string, eventType string, eventPayload string, createdAt time.Time) tasktimeline.TypedActivityIntent {
	intent := tasktimeline.NewTypedActivityIntent(taskID, actorRef, eventType, eventPayload, createdAt)
	if eventType == taskEventDiscussionAdded {
		intent.SourceMessageID = eventPayload
	}
	return intent
}

func applyTaskTypedEventPlan(ctx context.Context, tx coredb.Tx, intents ...tasktimeline.TypedActivityIntent) error {
	_, err := applyTaskTypedEventPlanWithResult(ctx, tx, intents...)
	return err
}

func applyTaskTypedEventPlanWithResult(ctx context.Context, tx coredb.Tx, intents ...tasktimeline.TypedActivityIntent) (tasktimeline.TaskTimelineWriteResult, error) {
	result, err := tasktimeline.WritePlanWithResult(ctx, tx, tasktimeline.TaskTimelinePlan{
		TypedActivityIntents: intents,
	})
	if err != nil {
		if errors.Is(err, storeerr.ErrNotFound) {
			return tasktimeline.TaskTimelineWriteResult{}, ErrNotFound
		}
		return tasktimeline.TaskTimelineWriteResult{}, err
	}
	return result, nil
}

func ensureTaskChannelID(task *Task, channelID string) {
	if task == nil || strings.TrimSpace(task.ChannelID) != "" {
		return
	}
	task.ChannelID = strings.TrimSpace(channelID)
}

func replaceTaskChecklist(ctx context.Context, execer interface {
	Exec(context.Context, string, ...any) (coredb.Result, error)
}, taskID string, listKind string, items []TaskChecklistItem, now time.Time) error {
	if _, err := execer.Exec(ctx, `
DELETE FROM task_checklist_items
WHERE task_id = $1
  AND list_kind = $2
`, taskID, listKind); err != nil {
		return fmt.Errorf("clear task checklist items: %w", err)
	}
	if len(items) == 0 {
		return nil
	}

	query, args, err := buildTaskChecklistInsertQuery(taskID, listKind, items, now)
	if err != nil {
		return err
	}
	if _, err := execer.Exec(ctx, query, args...); err != nil {
		return fmt.Errorf("insert task checklist items: %w", err)
	}
	return nil
}

func buildTaskChecklistInsertQuery(taskID string, listKind string, items []TaskChecklistItem, now time.Time) (string, []any, error) {
	var rows strings.Builder
	args := make([]any, 0, len(items)*7)
	param := 1
	for index, item := range items {
		itemID, err := newTaskChecklistItemID()
		if err != nil {
			return "", nil, fmt.Errorf("generate task checklist item id: %w", err)
		}
		if index > 0 {
			rows.WriteString("\nUNION ALL\n")
		}
		rows.WriteString("SELECT $")
		rows.WriteString(strconv.Itoa(param))
		rows.WriteString(" AS id, $")
		rows.WriteString(strconv.Itoa(param + 1))
		rows.WriteString(" AS task_id, $")
		rows.WriteString(strconv.Itoa(param + 2))
		rows.WriteString(" AS list_kind, $")
		rows.WriteString(strconv.Itoa(param + 3))
		rows.WriteString(" AS position, $")
		rows.WriteString(strconv.Itoa(param + 4))
		rows.WriteString(" AS item_text, $")
		rows.WriteString(strconv.Itoa(param + 5))
		rows.WriteString(" AS is_checked, $")
		rows.WriteString(strconv.Itoa(param + 6))
		rows.WriteString(" AS created_at, $")
		rows.WriteString(strconv.Itoa(param + 6))
		rows.WriteString(" AS updated_at")
		args = append(args, itemID, taskID, listKind, index, item.Text, item.Checked, now)
		param += 7
	}

	query := fmt.Sprintf(`
INSERT INTO task_checklist_items (
    id,
    task_id,
    list_kind,
    position,
    item_text,
    is_checked,
    created_at,
    updated_at
) %s
`, rows.String())
	return query, args, nil
}

func loadTaskChecklist(ctx context.Context, queryer interface {
	Query(context.Context, string, ...any) (coredb.Rows, error)
}, taskID string, listKind string) ([]TaskChecklistItem, error) {
	return loadTaskChecklistLimit(ctx, queryer, taskID, listKind, 0)
}

func loadTaskChecklistLimit(ctx context.Context, queryer interface {
	Query(context.Context, string, ...any) (coredb.Rows, error)
}, taskID string, listKind string, limit int) ([]TaskChecklistItem, error) {
	limitClause := ""
	args := []any{taskID, listKind}
	if limit > 0 {
		limitClause = "LIMIT $3"
		args = append(args, limit+1)
	}
	rows, err := queryer.Query(ctx, `
SELECT id, item_text, is_checked
FROM task_checklist_items
WHERE task_id = $1
  AND list_kind = $2
ORDER BY position ASC, id ASC
`+limitClause, args...)
	if err != nil {
		return nil, fmt.Errorf("query task checklist items: %w", err)
	}
	defer rows.Close()

	items := make([]TaskChecklistItem, 0)
	for rows.Next() {
		var item TaskChecklistItem
		if err := rows.Scan(&item.ID, &item.Text, &item.Checked); err != nil {
			return nil, fmt.Errorf("scan task checklist item: %w", err)
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate task checklist items: %w", err)
	}
	if limit > 0 && len(items) > limit {
		items = items[:limit]
	}
	return items, nil
}

func loadTaskContextRefsLimit(ctx context.Context, queryer coredb.QueryRower, taskID string, limit int) ([]string, bool, []string, bool, []string, bool, error) {
	if limit <= 0 {
		limit = taskDetailContextRefLimit
	}
	extractCount := limit + 1
	columns := []string{
		"pinned_message_refs_json",
		"relevant_urls_json",
		"workspace_refs_json",
	}
	selects := make([]string, 0, len(columns)*extractCount)
	for _, column := range columns {
		for index := 0; index < extractCount; index++ {
			selects = append(selects, fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(%s, '$[%d]'))", column, index))
		}
	}
	row := queryer.QueryRow(ctx, fmt.Sprintf(`
SELECT %s
FROM tasks
WHERE id = $1
`, strings.Join(selects, ",\n       ")), taskID)

	values := make([]sql.NullString, len(selects))
	destinations := make([]any, len(values))
	for index := range values {
		destinations[index] = &values[index]
	}
	if err := row.Scan(destinations...); err != nil {
		if coredb.IsNotFound(err) {
			return nil, false, nil, false, nil, false, ErrNotFound
		}
		return nil, false, nil, false, nil, false, fmt.Errorf("load task context refs: %w", err)
	}

	pinnedMessageRefs, pinnedMessageRefsHasMore := taskContextRefValuesFromExtracts(values[0:extractCount], limit)
	relevantURLs, relevantURLsHasMore := taskContextRefValuesFromExtracts(values[extractCount:2*extractCount], limit)
	workspaceRefs, workspaceRefsHasMore := taskContextRefValuesFromExtracts(values[2*extractCount:3*extractCount], limit)
	return pinnedMessageRefs, pinnedMessageRefsHasMore, relevantURLs, relevantURLsHasMore, workspaceRefs, workspaceRefsHasMore, nil
}

func taskContextRefValuesFromExtracts(values []sql.NullString, limit int) ([]string, bool) {
	itemCount := len(values)
	if itemCount > limit {
		itemCount = limit
	}
	refs := make([]string, 0, itemCount)
	for _, value := range values[:itemCount] {
		if !value.Valid {
			continue
		}
		if trimmed := strings.TrimSpace(value.String); trimmed != "" {
			refs = append(refs, trimmed)
		}
	}
	hasMore := len(values) > limit && values[limit].Valid
	return refs, hasMore
}

func ensureParentTaskRef(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelID string, taskID string, parentTaskRef string) error {
	trimmedTaskID := strings.TrimSpace(taskID)
	trimmedParentTaskRef := strings.TrimSpace(parentTaskRef)
	if trimmedTaskID == trimmedParentTaskRef {
		return badRequest("parentTaskRef must reference a different task")
	}

	row := queryer.QueryRow(ctx, `
SELECT t.board_id, COALESCE(t.parent_task_ref, '')
FROM tasks t
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $2
JOIN scopes task_scope
  ON task_scope.id = t.task_scope_id
 AND task_scope.workspace_id = $2
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
JOIN scopes board_channel
  ON board_channel.id = tb.channel_id
 AND board_channel.workspace_id = $2
 AND board_channel.type = 'channel'
 AND board_channel.status = 'active'
 AND board_channel.archived_at IS NULL
WHERE t.id = $1
  AND t.channel_id = tb.channel_id
  AND tb.channel_id = $3
FOR UPDATE
`, trimmedParentTaskRef, workspaceID, channelID)
	var parentBoardID string
	var parentParentTaskRef string
	if err := row.Scan(&parentBoardID, &parentParentTaskRef); err != nil {
		if coredb.IsNotFound(err) {
			return badRequest("parentTaskRef must reference a task on the same board")
		}
		return fmt.Errorf("load parent task reference: %w", err)
	}

	childRow := queryer.QueryRow(ctx, `
SELECT t.board_id
FROM tasks t
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $2
JOIN scopes task_scope
  ON task_scope.id = t.task_scope_id
 AND task_scope.workspace_id = $2
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
WHERE t.id = $1
  AND t.channel_id = tb.channel_id
  AND tb.channel_id = $3
FOR UPDATE
`, trimmedTaskID, workspaceID, channelID)
	var childBoardID string
	if err := childRow.Scan(&childBoardID); err != nil {
		if coredb.IsNotFound(err) {
			return badRequest("task must reference a task on the same board")
		}
		return fmt.Errorf("load child task board: %w", err)
	}
	if strings.TrimSpace(childBoardID) != strings.TrimSpace(parentBoardID) {
		return badRequest("parentTaskRef must reference a task on the same board")
	}

	// Lock and walk the complete ancestor chain. This permits arbitrary-depth
	// nesting while rejecting self/cyclic or malformed topology before the
	// caller's update transaction can commit. The bounded walk is deliberately
	// fail-closed for pathological chains.
	seen := map[string]struct{}{trimmedParentTaskRef: {}}
	ancestorID := strings.TrimSpace(parentParentTaskRef)
	for depth := 0; ancestorID != ""; depth++ {
		if depth >= 64 {
			return badRequest("parentTaskRef hierarchy exceeds the supported depth")
		}
		if ancestorID == trimmedTaskID {
			return badRequest("parentTaskRef cannot be a descendant of the task")
		}
		if _, exists := seen[ancestorID]; exists {
			return badRequest("parentTaskRef hierarchy contains a cycle")
		}
		seen[ancestorID] = struct{}{}

		ancestorRow := queryer.QueryRow(ctx, `
SELECT t.board_id, COALESCE(t.parent_task_ref, '')
FROM tasks t
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $2
JOIN scopes task_scope
  ON task_scope.id = t.task_scope_id
 AND task_scope.workspace_id = $2
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
WHERE t.id = $1
  AND t.channel_id = tb.channel_id
FOR UPDATE
`, ancestorID, workspaceID)
		var ancestorBoardID string
		var nextAncestorID string
		if err := ancestorRow.Scan(&ancestorBoardID, &nextAncestorID); err != nil {
			if coredb.IsNotFound(err) {
				return badRequest("parentTaskRef hierarchy is malformed")
			}
			return fmt.Errorf("load parent task ancestor: %w", err)
		}
		if strings.TrimSpace(ancestorBoardID) != strings.TrimSpace(childBoardID) {
			return badRequest("parentTaskRef hierarchy crosses task boards")
		}
		ancestorID = strings.TrimSpace(nextAncestorID)
	}
	return nil
}
