package agent

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

type agentTaskTargetKind uint8

const (
	agentTaskTargetBoard agentTaskTargetKind = iota
	agentTaskTargetCreate
	agentTaskTargetTask
)

type agentTaskTarget struct {
	BoardChannelID  string
	BoardChannelRef string
	TaskID          string
	TaskScopeID     string
	TaskNumber      int64
	Source          *CreateTaskMetadata
}

func (target agentTaskTarget) boardRef() BoardRef {
	return BoardRef{
		ChannelID: strings.TrimSpace(target.BoardChannelID),
		Channel:   strings.TrimSpace(target.BoardChannelRef),
	}
}

func (target agentTaskTarget) taskRef() TaskRef {
	return TaskRef{
		TaskID:     strings.TrimSpace(target.TaskID),
		TaskNumber: target.TaskNumber,
	}
}

// normalizeAgentTaskTarget resolves only the explicit locator supplied by the
// caller. Task targets are resource locators, not ambient delivery authority;
// omitting one is a validation error instead of a fallback to a prior wake.
func normalizeAgentTaskTarget(
	ctx context.Context,
	resolver TaskTargetResolver,
	principal storeagent.AuthenticatedAgent,
	locator dto.AgentTaskLocator,
	explicitTarget string,
	kind agentTaskTargetKind,
) (agentTaskTarget, error) {
	if resolver == nil {
		return agentTaskTarget{}, badRequest("agent task target resolver is unavailable")
	}

	explicitChannel := strings.TrimSpace(locator.Channel)
	explicitTaskNumber := locator.Number
	targetRef := explicitChannel
	if kind == agentTaskTargetCreate && strings.TrimSpace(explicitTarget) != "" {
		targetRef = strings.TrimSpace(explicitTarget)
	}
	if targetRef == "" {
		return agentTaskTarget{}, badRequest("channel or target is required")
	}
	if err := validateAgentTaskChannelTarget(targetRef); err != nil {
		return agentTaskTarget{}, err
	}
	resolved, err := resolver.ResolveTaskTargetForAgent(ctx, principal, targetRef)
	if err != nil {
		return agentTaskTarget{}, err
	}

	target := agentTaskTarget{
		BoardChannelID:  strings.TrimSpace(resolved.TaskChannelID),
		BoardChannelRef: strings.TrimSpace(resolved.TaskChannelRef),
		TaskID:          strings.TrimSpace(resolved.TaskID),
		TaskScopeID:     strings.TrimSpace(resolved.TaskScopeID),
		TaskNumber:      resolved.TaskNumber,
	}
	if target.BoardChannelID == "" && target.BoardChannelRef == "" {
		return agentTaskTarget{}, badRequest("resolved task board is required")
	}
	if explicitChannel != "" &&
		target.BoardChannelRef != "" &&
		!sameTaskBoardRef(explicitChannel, target.BoardChannelRef) &&
		!sameTeamTaskBoardAlias(explicitChannel, targetRef) {
		return agentTaskTarget{}, badRequest("resolved target conflicts with task channel")
	}

	taskNumber := explicitTaskNumber
	if target.TaskNumber > 0 {
		if taskNumber > 0 && target.TaskNumber != taskNumber {
			return agentTaskTarget{}, badRequest("resolved target conflicts with task number")
		}
		taskNumber = target.TaskNumber
	}
	target.TaskNumber = taskNumber
	if kind == agentTaskTargetTask && target.TaskID == "" && target.TaskNumber <= 0 {
		return agentTaskTarget{}, badRequest("task_number or task scope target is required")
	}
	if kind == agentTaskTargetCreate {
		target.Source = createTaskMetadataFromTarget(resolved)
	}
	return target, nil
}

// normalizeOptionalAgentTaskCreateSource resolves only an explicit source
// target for a canonical parentTaskScope create. The parent scope itself is a
// complete locator and does not need a hidden conversation fallback.
func normalizeOptionalAgentTaskCreateSource(
	ctx context.Context,
	resolver TaskTargetResolver,
	principal storeagent.AuthenticatedAgent,
	explicitTarget string,
) (agentTaskTarget, error) {
	if strings.TrimSpace(explicitTarget) == "" {
		return agentTaskTarget{}, nil
	}
	return normalizeAgentTaskTarget(
		ctx,
		resolver,
		principal,
		dto.AgentTaskLocator{},
		explicitTarget,
		agentTaskTargetCreate,
	)
}

// validateAgentTaskChannelTarget rejects Task target references that are
// syntactically not a usable #channel board — a bare name, a raw channel id, a
// DM, or a DM reply thread — with the stable taskboard.ErrInvalidChannelTarget
// code, so the exec client can attach one bounded Hint without guessing from
// HTTP status. Valid #channel and channel reply-thread targets, raw task-scope
// targets, and team targets pass through to the shared resolver, where an
// unknown or inaccessible (but syntactically valid) channel stays a no-leak
// not-found/forbidden and is NOT mapped to this code.
func validateAgentTaskChannelTarget(targetRef string) error {
	parsed, err := domain.ParseMessageTargetReference(strings.TrimSpace(targetRef))
	if err != nil {
		return fmt.Errorf("%v: %w", err, taskboard.ErrInvalidChannelTarget)
	}
	switch parsed.Kind {
	case domain.MessageTargetKindWorkflow:
		return fmt.Errorf("Workflow Builder edit scopes do not support tasks: %w", taskboard.ErrInvalidChannelTarget)
	case domain.MessageTargetKindDM:
		return fmt.Errorf("dm does not support tasks: %w", taskboard.ErrInvalidChannelTarget)
	case domain.MessageTargetKindReplyThread:
		if parsed.Thread.Type == domain.ThreadTypeDM {
			return fmt.Errorf("dm does not support tasks: %w", taskboard.ErrInvalidChannelTarget)
		}
	}
	return nil
}

func createTaskMetadataFromTarget(target message.TaskCommandTarget) *CreateTaskMetadata {
	if strings.TrimSpace(target.SourceThreadParentChannelID) == "" ||
		strings.TrimSpace(target.SourceThreadParentMessageID) == "" {
		return nil
	}
	return &CreateTaskMetadata{
		SourceThreadChannelID:       strings.TrimSpace(target.SourceThreadChannelID),
		SourceThreadParentChannelID: strings.TrimSpace(target.SourceThreadParentChannelID),
		SourceThreadParentMessageID: strings.TrimSpace(target.SourceThreadParentMessageID),
		SourceThreadRef:             strings.TrimSpace(target.SourceThreadRef),
	}
}

func sameTaskBoardRef(left string, right string) bool {
	left = strings.TrimSpace(left)
	right = strings.TrimSpace(right)
	if left == right {
		return true
	}
	return taskBoardPart(left) == taskBoardPart(right)
}

func sameTeamTaskBoardAlias(left string, right string) bool {
	leftTarget, leftErr := domain.ParseMessageTargetReference(strings.TrimSpace(left))
	rightTarget, rightErr := domain.ParseMessageTargetReference(strings.TrimSpace(right))
	return leftErr == nil &&
		rightErr == nil &&
		leftTarget.Kind == domain.MessageTargetKindTeam &&
		rightTarget.Kind == domain.MessageTargetKindTeam &&
		leftTarget.Team.TeamID == rightTarget.Team.TeamID
}

func taskBoardPart(target string) string {
	target = strings.TrimSpace(target)
	if strings.HasPrefix(target, "#") {
		if index := strings.IndexByte(target, ':'); index >= 0 {
			return target[:index]
		}
	}
	return target
}
