package taskboard

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

// TaskPrincipalProfile is the task-domain canonical identity substrate.
// Task responsibility and creator identity are exposed through profiles rather
// than duplicate actor/name aliases.
type TaskPrincipalProfile = message.AuthorProfile

type taskActorProjection struct {
	MemberType                 sql.NullString
	MemberID                   sql.NullString
	UserID                     sql.NullString
	UserDisplayName            sql.NullString
	UserAvatarURL              sql.NullString
	MemberDisplayName          sql.NullString
	UserName                   sql.NullString
	AgentID                    sql.NullString
	AgentName                  sql.NullString
	AgentAvatarURL             sql.NullString
	ServiceID                  sql.NullString
	ServiceKind                sql.NullString
	ServiceHandle              sql.NullString
	ServiceDisplayName         sql.NullString
	ServiceAvatarURL           sql.NullString
	ServiceDefinitionID        sql.NullString
	ServiceDefinitionSlug      sql.NullString
	ServiceDefinitionName      sql.NullString
	ServiceDefinitionAvatarURL sql.NullString
}

type taskDiscussionMessageAuthorProjection struct {
	taskActorProjection
}

func taskActorProjectionSelectSQL(memberAlias string, userAlias string, agentAlias string, serviceMemberAlias string, serviceDefinitionAlias string) string {
	return fmt.Sprintf(`    %s.type,
    %s.id,
    %s.id,
    %s.display_name,
    %s.avatar_url,
    %s.display_name,
    %s.name,
    %s.id,
    %s.name,
    %s.avatar_url,
    %s.service_installation_id,
    %s.service_kind,
    %s.handle,
    %s.display_name,
    %s.avatar_url,
    %s.id,
    %s.slug,
    %s.name,
    %s.avatar_url`,
		memberAlias,
		memberAlias,
		userAlias,
		userAlias,
		userAlias,
		memberAlias,
		userAlias,
		agentAlias,
		agentAlias,
		agentAlias,
		serviceMemberAlias,
		serviceMemberAlias,
		serviceMemberAlias,
		serviceMemberAlias,
		serviceMemberAlias,
		serviceDefinitionAlias,
		serviceDefinitionAlias,
		serviceDefinitionAlias,
		serviceDefinitionAlias,
	)
}

func taskActorProjectionJoinsSQL(memberAlias string, userAlias string, agentAlias string, serviceMemberAlias string, serviceDefinitionAlias string) string {
	return fmt.Sprintf(`LEFT JOIN users %s
  ON %s.type = 'human'
 AND %s.id = %s.user_id
LEFT JOIN agents %s
  ON %s.type = 'agent'
 AND %s.member_id = %s.id
LEFT JOIN service_members %s
  ON %s.type = 'service'
 AND %s.workspace_id = %s.workspace_id
 AND %s.member_id = %s.id
LEFT JOIN service_definitions %s
  ON %s.id = %s.service_definition_id
 AND %s.service_kind = %s.service_kind`,
		userAlias,
		memberAlias,
		userAlias,
		memberAlias,
		agentAlias,
		memberAlias,
		agentAlias,
		memberAlias,
		serviceMemberAlias,
		memberAlias,
		serviceMemberAlias,
		memberAlias,
		serviceMemberAlias,
		memberAlias,
		serviceDefinitionAlias,
		serviceDefinitionAlias,
		serviceMemberAlias,
		serviceDefinitionAlias,
		serviceMemberAlias,
	)
}

func appendTaskActorProjectionDestinations(destinations []any, projection *taskActorProjection) []any {
	return append(destinations,
		&projection.MemberType,
		&projection.MemberID,
		&projection.UserID,
		&projection.UserDisplayName,
		&projection.UserAvatarURL,
		&projection.MemberDisplayName,
		&projection.UserName,
		&projection.AgentID,
		&projection.AgentName,
		&projection.AgentAvatarURL,
		&projection.ServiceID,
		&projection.ServiceKind,
		&projection.ServiceHandle,
		&projection.ServiceDisplayName,
		&projection.ServiceAvatarURL,
		&projection.ServiceDefinitionID,
		&projection.ServiceDefinitionSlug,
		&projection.ServiceDefinitionName,
		&projection.ServiceDefinitionAvatarURL,
	)
}

func (projection taskDiscussionMessageAuthorProjection) authorProfile() message.AuthorProfile {
	return projection.taskActorProjection.authorProfile(trimTaskNullString(projection.MemberID))
}

func (projection taskDiscussionMessageAuthorProjection) compatibilityActor() (TaskActor, error) {
	fallbackActor, err := projection.actor()
	if err != nil {
		return TaskActor{}, err
	}
	fallback := TaskActor{}
	if fallbackActor != nil {
		fallback = *fallbackActor
	}
	return taskActorFromAuthorProfile(projection.authorProfile(), fallback), nil
}

func (projection taskActorProjection) actor() (*TaskActor, error) {
	memberType := trimTaskNullString(projection.MemberType)
	switch memberType {
	case "":
		return nil, nil
	case memberTypeHuman:
		userID := trimTaskNullString(projection.UserID)
		if userID == "" {
			return nil, nil
		}
		return &TaskActor{
			Type: "human",
			ID:   userID,
			Name: projection.displayName(),
		}, nil
	case memberTypeAgent:
		agentID := trimTaskNullString(projection.AgentID)
		if agentID == "" {
			return nil, nil
		}
		return &TaskActor{
			Type: "agent",
			ID:   agentID,
			Name: projection.displayName(),
		}, nil
	case memberTypeService:
		memberID := trimTaskNullString(projection.MemberID)
		if memberID == "" {
			return nil, nil
		}
		return &TaskActor{
			Type: "app",
			ID:   memberID,
			Name: projection.displayName(),
		}, nil
	default:
		return nil, fmt.Errorf("invalid actor member type %q", memberType)
	}
}

func (projection taskActorProjection) displayName() string {
	return projection.displayNameWithUnknownFallback(true)
}

func (projection taskActorProjection) displayNameOrEmpty() string {
	return projection.displayNameWithUnknownFallback(false)
}

func (projection taskActorProjection) displayNameWithUnknownFallback(includeUnknown bool) string {
	switch trimTaskNullString(projection.MemberType) {
	case memberTypeHuman:
		values := []string{
			trimTaskNullString(projection.UserDisplayName),
			trimTaskNullString(projection.MemberDisplayName),
			trimTaskNullString(projection.UserName),
		}
		if includeUnknown {
			values = append(values, "Unknown user")
		}
		return firstTaskString(values...)
	case memberTypeAgent:
		values := []string{
			trimTaskNullString(projection.MemberDisplayName),
			trimTaskNullString(projection.AgentName),
		}
		if includeUnknown {
			values = append(values, "Unknown agent")
		}
		return firstTaskString(values...)
	case memberTypeService:
		values := []string{
			trimTaskNullString(projection.MemberDisplayName),
			trimTaskNullString(projection.ServiceDisplayName),
			trimTaskNullString(projection.ServiceDefinitionName),
			trimTaskNullString(projection.ServiceHandle),
		}
		if includeUnknown {
			values = append(values, "Unknown app")
		}
		return firstTaskString(values...)
	default:
		return ""
	}
}

func trimTaskNullString(value sql.NullString) string {
	if !value.Valid {
		return ""
	}
	return strings.TrimSpace(value.String)
}

func firstTaskString(values ...string) string {
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func taskDiscussionReactionAuthorProfile(memberID string, projection taskActorProjection) message.AuthorProfile {
	memberID = strings.TrimSpace(memberID)
	return projection.authorProfile(memberID)
}

func taskActivityAuthorProfile(memberID string, projection taskActorProjection) message.AuthorProfile {
	memberID = strings.TrimSpace(memberID)
	profile := projection.authorProfile(memberID)
	if projection.displayNameOrEmpty() == "" {
		switch profile.ActorType {
		case message.AuthorActorTypeHuman, message.AuthorActorTypeAgent:
			profile.DisplayName = ""
		}
	}
	return profile.Normalize()
}

func (projection taskActorProjection) authorProfile(memberID string) message.AuthorProfile {
	memberID = strings.TrimSpace(memberID)
	memberType := trimTaskNullString(projection.MemberType)
	displayName := projection.displayName()
	switch memberType {
	case memberTypeHuman:
		return message.AuthorProfile{
			ActorType:   message.AuthorActorTypeHuman,
			PrincipalID: trimTaskNullString(projection.UserID),
			MemberID:    memberID,
			TargetID:    memberID,
			DisplayName: displayName,
			AvatarURL:   trimTaskNullString(projection.UserAvatarURL),
		}.Normalize()
	case memberTypeAgent:
		return message.AuthorProfile{
			ActorType:   message.AuthorActorTypeAgent,
			PrincipalID: trimTaskNullString(projection.AgentID),
			MemberID:    memberID,
			TargetID:    trimTaskNullString(projection.AgentName),
			DisplayName: displayName,
			AvatarURL:   trimTaskNullString(projection.AgentAvatarURL),
		}.Normalize()
	case memberTypeService:
		serviceHandle := trimTaskNullString(projection.ServiceHandle)
		serviceSlug := trimTaskNullString(projection.ServiceDefinitionSlug)
		serviceKind := trimTaskNullString(projection.ServiceKind)
		if serviceKind == "" {
			serviceKind = message.AuthorServiceKindApp
		}
		return message.AuthorProfile{
			ActorType:   message.AuthorActorTypeService,
			PrincipalID: firstTaskString(trimTaskNullString(projection.ServiceID), memberID),
			MemberID:    memberID,
			TargetID:    firstTaskString(serviceHandle, serviceSlug, memberID),
			DisplayName: firstTaskString(trimTaskNullString(projection.ServiceDisplayName), displayName, trimTaskNullString(projection.ServiceDefinitionName), serviceHandle, memberID),
			AvatarURL:   firstTaskString(trimTaskNullString(projection.ServiceAvatarURL), trimTaskNullString(projection.ServiceDefinitionAvatarURL)),
			ServiceKind: serviceKind,
			ServiceSlug: serviceSlug,
		}.Normalize()
	default:
		return message.AuthorProfile{}
	}
}

func taskActorFromAuthorProfile(profile message.AuthorProfile, fallback TaskActor) TaskActor {
	profile = profile.Normalize()
	if profile.Valid() {
		actorType := profile.ActorType
		if actorType == message.AuthorActorTypeService {
			actorType = "app"
		}
		return TaskActor{
			Type: actorType,
			ID:   firstTaskString(profile.PrincipalID, profile.TargetID, profile.MemberID, fallback.ID),
			Name: firstTaskString(profile.DisplayName, fallback.Name),
		}
	}
	return fallback
}

func taskPrincipalProfileFromProjection(label string, projection taskActorProjection, required bool) (*TaskPrincipalProfile, error) {
	memberID := trimTaskNullString(projection.MemberID)
	memberType := trimTaskNullString(projection.MemberType)
	if memberType == "" {
		if required {
			return nil, fmt.Errorf("%s principal is required", label)
		}
		return nil, nil
	}
	if _, err := projection.actor(); err != nil {
		return nil, err
	}
	profile := projection.authorProfile(memberID).Normalize()
	if !profile.Valid() {
		if required {
			return nil, fmt.Errorf("%s principal profile is invalid", label)
		}
		return nil, nil
	}
	return &profile, nil
}

func taskActorCompatFromPrincipalProfile(profile *TaskPrincipalProfile, fallback *TaskActor) *TaskActor {
	if profile == nil {
		return fallback
	}
	fallbackValue := TaskActor{}
	if fallback != nil {
		fallbackValue = *fallback
	}
	actor := taskActorFromAuthorProfile(*profile, fallbackValue)
	if strings.TrimSpace(actor.ID) == "" && strings.TrimSpace(actor.Name) == "" {
		return nil
	}
	return &actor
}

func taskLegacyCreatedByFromPrincipalProfile(profile *TaskPrincipalProfile, fallbackType string, fallbackID string, fallbackName string) (string, string, string) {
	if profile == nil {
		return fallbackType, fallbackID, fallbackName
	}
	normalized := profile.Normalize()
	if !normalized.Valid() {
		return fallbackType, fallbackID, fallbackName
	}
	legacyType := normalized.ActorType
	switch legacyType {
	case message.AuthorActorTypeHuman:
		legacyType = "user"
	case message.AuthorActorTypeService:
		legacyType = "app"
	}
	return legacyType, firstTaskString(normalized.PrincipalID, normalized.TargetID, normalized.MemberID, fallbackID), firstTaskString(normalized.DisplayName, fallbackName)
}

func taskHumanPrincipalProfile(userID string, displayName string) *TaskPrincipalProfile {
	profile := message.AuthorProfile{
		ActorType:   message.AuthorActorTypeHuman,
		PrincipalID: strings.TrimSpace(userID),
		DisplayName: strings.TrimSpace(displayName),
	}.Normalize()
	if !profile.Valid() {
		return nil
	}
	return &profile
}
