package activity

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

	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
)

const activityDetailsDisplayKey = "display"

// ResourceActivityDisplay is the stable presentation snapshot attached to every
// resource activity event. Writers store it in details.display so AGS actors,
// repository targets, and resource labels remain readable even when the client
// cannot join the raw IDs back to AgentTeam users or product tables.
type ResourceActivityDisplay struct {
	Actor    ResourceActivityDisplayActor    `json:"actor,omitempty"`
	Resource ResourceActivityDisplayResource `json:"resource,omitempty"`
	Target   ResourceActivityDisplayTarget   `json:"target,omitempty"`
	Meta     []ResourceActivityDisplayMeta   `json:"meta,omitempty"`
}

// ResourceActivityDisplayActor is the service-owned actor presentation stored
// inside a resource activity display snapshot. It is not a AgentTeam user or AGS
// identity model and must not be used as an authorization principal.
type ResourceActivityDisplayActor struct {
	Kind  string `json:"kind,omitempty"`
	ID    string `json:"id,omitempty"`
	Name  string `json:"name,omitempty"`
	Email string `json:"email,omitempty"`
	Login string `json:"login,omitempty"`
}

// ResourceActivityDisplayResource is the service-owned resource presentation
// stored inside a resource activity display snapshot. It is not a resource
// store model or a general AgentTeam resource API DTO.
type ResourceActivityDisplayResource struct {
	Type         string `json:"type,omitempty"`
	ID           string `json:"id,omitempty"`
	Name         string `json:"name,omitempty"`
	RepoRole     string `json:"repoRole,omitempty"`
	RepoFullName string `json:"repoFullName,omitempty"`
	Href         string `json:"href,omitempty"`
}

// ResourceActivityDisplayTarget is the service-owned target presentation
// stored inside a resource activity display snapshot. It describes the target
// of a resource-domain event, not a AgentTeam task, message, or timeline object.
type ResourceActivityDisplayTarget struct {
	Kind        string `json:"kind,omitempty"`
	ID          string `json:"id,omitempty"`
	Label       string `json:"label,omitempty"`
	Href        string `json:"href,omitempty"`
	ExternalURL string `json:"externalUrl,omitempty"`
}

// ResourceActivityDisplayMeta is one stable presentation item attached to a
// resource activity display snapshot. Its links and labels are display data,
// not persisted authorization facts or a general API metadata contract.
type ResourceActivityDisplayMeta struct {
	Key         string `json:"key,omitempty"`
	Label       string `json:"label,omitempty"`
	Value       string `json:"value,omitempty"`
	Href        string `json:"href,omitempty"`
	ExternalURL string `json:"externalUrl,omitempty"`
	Title       string `json:"title,omitempty"`
}

func (display ResourceActivityDisplay) empty() bool {
	return display.Actor.empty() && display.Resource.empty() && display.Target.empty() && len(display.Meta) == 0
}

func (actor ResourceActivityDisplayActor) empty() bool {
	return strings.TrimSpace(actor.Kind) == "" && strings.TrimSpace(actor.ID) == "" && strings.TrimSpace(actor.Name) == "" && strings.TrimSpace(actor.Email) == "" && strings.TrimSpace(actor.Login) == ""
}

func (resource ResourceActivityDisplayResource) empty() bool {
	return strings.TrimSpace(resource.Type) == "" && strings.TrimSpace(resource.ID) == "" && strings.TrimSpace(resource.Name) == "" && strings.TrimSpace(resource.RepoRole) == "" && strings.TrimSpace(resource.RepoFullName) == "" && strings.TrimSpace(resource.Href) == ""
}

func (target ResourceActivityDisplayTarget) empty() bool {
	return strings.TrimSpace(target.Kind) == "" && strings.TrimSpace(target.ID) == "" && strings.TrimSpace(target.Label) == "" && strings.TrimSpace(target.Href) == "" && strings.TrimSpace(target.ExternalURL) == ""
}

func normalizeActivityDetailsMap(details any) (map[string]any, error) {
	if details == nil {
		return map[string]any{}, nil
	}
	if typed, ok := details.(map[string]any); ok {
		out := make(map[string]any, len(typed))
		for key, value := range typed {
			out[key] = value
		}
		return out, nil
	}
	bytes, err := json.Marshal(details)
	if err != nil {
		return nil, err
	}
	if len(bytes) == 0 || string(bytes) == "null" {
		return map[string]any{}, nil
	}
	var object map[string]any
	if err := json.Unmarshal(bytes, &object); err == nil && object != nil {
		return object, nil
	}
	return map[string]any{"value": string(bytes)}, nil
}

func marshalActivityDetailsMap(details map[string]any) (string, error) {
	if details == nil || len(details) == 0 {
		return "{}", nil
	}
	bytes, err := json.Marshal(details)
	if err != nil {
		return "", err
	}
	if len(bytes) == 0 || string(bytes) == "null" {
		return "{}", nil
	}
	return string(bytes), nil
}

func displayFromDetails(details map[string]any) (ResourceActivityDisplay, bool) {
	if details == nil {
		return ResourceActivityDisplay{}, false
	}
	raw, ok := details[activityDetailsDisplayKey]
	if !ok || raw == nil {
		return ResourceActivityDisplay{}, false
	}
	bytes, err := json.Marshal(raw)
	if err != nil {
		return ResourceActivityDisplay{}, false
	}
	var display ResourceActivityDisplay
	if err := json.Unmarshal(bytes, &display); err != nil {
		return ResourceActivityDisplay{}, false
	}
	return normalizeResourceActivityDisplay(display), !display.empty()
}

func normalizeResourceActivityDisplay(display ResourceActivityDisplay) ResourceActivityDisplay {
	display.Actor.Kind = strings.TrimSpace(display.Actor.Kind)
	display.Actor.ID = strings.TrimSpace(display.Actor.ID)
	display.Actor.Name = strings.TrimSpace(display.Actor.Name)
	display.Actor.Email = strings.TrimSpace(display.Actor.Email)
	display.Actor.Login = strings.TrimSpace(display.Actor.Login)
	display.Resource.Type = strings.TrimSpace(display.Resource.Type)
	display.Resource.ID = strings.TrimSpace(display.Resource.ID)
	display.Resource.Name = strings.TrimSpace(display.Resource.Name)
	display.Resource.RepoRole = strings.TrimSpace(display.Resource.RepoRole)
	display.Resource.RepoFullName = strings.TrimSpace(display.Resource.RepoFullName)
	display.Resource.Href = strings.TrimSpace(display.Resource.Href)
	display.Target.Kind = strings.TrimSpace(display.Target.Kind)
	display.Target.ID = strings.TrimSpace(display.Target.ID)
	display.Target.Label = strings.TrimSpace(display.Target.Label)
	display.Target.Href = strings.TrimSpace(display.Target.Href)
	display.Target.ExternalURL = strings.TrimSpace(display.Target.ExternalURL)
	meta := make([]ResourceActivityDisplayMeta, 0, len(display.Meta))
	seen := map[string]struct{}{}
	for _, item := range display.Meta {
		item.Key = strings.TrimSpace(item.Key)
		item.Label = strings.TrimSpace(item.Label)
		item.Value = strings.TrimSpace(item.Value)
		item.Href = strings.TrimSpace(item.Href)
		item.ExternalURL = strings.TrimSpace(item.ExternalURL)
		item.Title = strings.TrimSpace(item.Title)
		if item.Key == "" && item.Label == "" && item.Value == "" && item.Href == "" && item.ExternalURL == "" {
			continue
		}
		fingerprint := item.Key + "\x00" + item.Label + "\x00" + item.Value + "\x00" + item.Href + "\x00" + item.ExternalURL
		if _, ok := seen[fingerprint]; ok {
			continue
		}
		seen[fingerprint] = struct{}{}
		meta = append(meta, item)
	}
	display.Meta = meta
	return display
}

func completedResourceActivityDisplay(display ResourceActivityDisplay, event activitystore.ResourceActivityEvent, details map[string]any, assetName string) ResourceActivityDisplay {
	display = normalizeResourceActivityDisplay(display)
	if display.Actor.Kind == "" {
		display.Actor.Kind = strings.TrimSpace(event.ActorType)
	}
	if display.Actor.Kind == ActivityActorTypeUser && (strings.Contains(event.ActorID, "@") || (!looksInternalActorID(event.ActorID) && event.ActorID != "")) {
		display.Actor.Kind = "git"
	}
	if display.Actor.ID == "" {
		display.Actor.ID = strings.TrimSpace(event.ActorID)
	}
	if display.Actor.Name == "" {
		display.Actor.Name = firstNonEmpty(strings.TrimSpace(event.ActorName), readableActorID(display.Actor.ID))
	}
	if display.Actor.Email == "" && strings.Contains(display.Actor.ID, "@") {
		display.Actor.Email = display.Actor.ID
	}
	if display.Actor.Login == "" && display.Actor.Email == "" && display.Actor.ID != "" && !looksInternalActorID(display.Actor.ID) {
		display.Actor.Login = display.Actor.ID
	}
	if display.Resource.Type == "" {
		display.Resource.Type = strings.TrimSpace(event.AssetType)
	}
	if display.Resource.ID == "" {
		display.Resource.ID = strings.TrimSpace(event.AssetID)
	}
	if display.Resource.Name == "" {
		display.Resource.Name = firstNonEmpty(
			strings.TrimSpace(assetName),
			detailStringValue(details, "knowledgeBaseName", "sourceName", "name"),
			strings.TrimSpace(event.RepoFullName),
			strings.TrimSpace(event.AssetID),
		)
	}
	if display.Resource.RepoFullName == "" {
		display.Resource.RepoFullName = strings.TrimSpace(event.RepoFullName)
	}
	if display.Resource.RepoRole == "" {
		display.Resource.RepoRole = detailStringValue(details, "repoRole", "repositoryRole")
	}
	if display.Target.Kind == "" {
		display.Target.Kind = strings.TrimSpace(event.TargetKind)
	}
	if display.Target.ID == "" {
		display.Target.ID = strings.TrimSpace(event.TargetID)
	}
	if display.Target.ExternalURL == "" {
		display.Target.ExternalURL = strings.TrimSpace(event.TargetURL)
	}
	if display.Target.Label == "" {
		display.Target.Label = resourceActivityTargetLabel(display.Target.Kind, display.Target.ID, details)
	}
	if len(display.Meta) == 0 {
		display.Meta = defaultResourceActivityDisplayMeta(display, event, details)
	}
	return normalizeResourceActivityDisplay(display)
}

// GitActorActivityDisplay builds a stable presentation snapshot for a Git
// actor. It does not establish a AgentTeam identity or authorization principal.
func GitActorActivityDisplay(id string, name string, email string, login string) ResourceActivityDisplay {
	id = strings.TrimSpace(id)
	name = strings.TrimSpace(name)
	email = strings.TrimSpace(email)
	login = strings.TrimSpace(login)
	if email == "" && strings.Contains(id, "@") {
		email = id
	}
	if login == "" && email == "" && id != "" && !looksInternalActorID(id) {
		login = id
	}
	if name == "" {
		name = firstNonEmpty(login, readableActorID(email), readableActorID(id))
	}
	return ResourceActivityDisplay{
		Actor: ResourceActivityDisplayActor{
			Kind:  "git",
			ID:    id,
			Name:  name,
			Email: email,
			Login: login,
		},
	}
}

// SystemActorActivityDisplay builds a presentation-only system actor snapshot.
func SystemActorActivityDisplay(name string) ResourceActivityDisplay {
	return ResourceActivityDisplay{Actor: ResourceActivityDisplayActor{Kind: ActivityActorTypeSystem, Name: firstNonEmpty(name, "System")}}
}

// DataPlaneActivityDisplay normalizes one projector event into its stable
// service-owned presentation snapshot.
func DataPlaneActivityDisplay(repo ResourceActivityDataPlaneRepository, event ResourceActivityDataPlaneEvent) ResourceActivityDisplay {
	details, _ := normalizeActivityDetailsMap(event.Details)
	return resourceActivityDisplayFor(repo, event, details)
}

func resourceActivityDisplayFor(repo ResourceActivityDataPlaneRepository, event ResourceActivityDataPlaneEvent, details map[string]any) ResourceActivityDisplay {
	display := normalizeResourceActivityDisplay(event.Display)
	if display.empty() {
		if detailDisplay, ok := displayFromDetails(details); ok {
			display = detailDisplay
		}
	}
	display.Actor.Kind = firstNonEmpty(display.Actor.Kind, strings.TrimSpace(event.ActorType))
	if display.Actor.Kind == ActivityActorTypeUser && (strings.Contains(event.ActorID, "@") || (!looksInternalActorID(event.ActorID) && event.ActorID != "")) {
		display.Actor.Kind = "git"
	}
	display.Actor.ID = firstNonEmpty(display.Actor.ID, strings.TrimSpace(event.ActorID))
	display.Actor.Name = firstNonEmpty(display.Actor.Name, strings.TrimSpace(event.ActorName), readableActorID(display.Actor.ID))
	if display.Actor.Email == "" && strings.Contains(display.Actor.ID, "@") {
		display.Actor.Email = display.Actor.ID
	}
	if display.Actor.Login == "" && display.Actor.Email == "" && display.Actor.ID != "" && !looksInternalActorID(display.Actor.ID) {
		display.Actor.Login = display.Actor.ID
	}
	display.Resource.Type = firstNonEmpty(display.Resource.Type, strings.TrimSpace(repo.AssetType))
	display.Resource.ID = firstNonEmpty(display.Resource.ID, strings.TrimSpace(repo.AssetID), strings.TrimSpace(repo.ResourceID))
	display.Resource.Name = firstNonEmpty(display.Resource.Name, detailStringValue(details, "knowledgeBaseName", "sourceName", "name"))
	display.Resource.RepoRole = firstNonEmpty(display.Resource.RepoRole, strings.TrimSpace(repo.RepoRole))
	display.Resource.RepoFullName = firstNonEmpty(display.Resource.RepoFullName, strings.TrimSpace(repo.RepoFullName))
	display.Target.Kind = firstNonEmpty(display.Target.Kind, strings.TrimSpace(event.TargetKind))
	display.Target.ID = firstNonEmpty(display.Target.ID, strings.TrimSpace(event.TargetID))
	display.Target.ExternalURL = firstNonEmpty(display.Target.ExternalURL, strings.TrimSpace(event.TargetURL))
	display.Target.Label = firstNonEmpty(display.Target.Label, resourceActivityTargetLabel(display.Target.Kind, display.Target.ID, details))
	if len(display.Meta) == 0 {
		display.Meta = defaultResourceActivityDisplayMeta(display, activitystore.ResourceActivityEvent{
			AssetType:    repo.AssetType,
			RepoFullName: repo.RepoFullName,
			TargetKind:   event.TargetKind,
			TargetID:     event.TargetID,
			TargetURL:    event.TargetURL,
		}, details)
	}
	return normalizeResourceActivityDisplay(display)
}

// WithDataPlaneActivityDisplay attaches a normalized presentation snapshot to
// a data-plane event without changing its identity or authorization semantics.
func WithDataPlaneActivityDisplay(repo ResourceActivityDataPlaneRepository, event ResourceActivityDataPlaneEvent, display ResourceActivityDisplay) ResourceActivityDataPlaneEvent {
	display = DataPlaneActivityDisplay(repo, ResourceActivityDataPlaneEvent{
		ActorType:  event.ActorType,
		ActorID:    event.ActorID,
		ActorName:  event.ActorName,
		Details:    event.Details,
		TargetKind: event.TargetKind,
		TargetID:   event.TargetID,
		TargetURL:  event.TargetURL,
		Display:    display,
	})
	if event.ActorID != "" && display.Actor.ID == "" {
		display.Actor.ID = strings.TrimSpace(event.ActorID)
	}
	if event.ActorName != "" && display.Actor.Name == "" {
		display.Actor.Name = strings.TrimSpace(event.ActorName)
	}
	if event.ActorType != "" && display.Actor.Kind == "" {
		display.Actor.Kind = strings.TrimSpace(event.ActorType)
	}
	event.Display = normalizeResourceActivityDisplay(display)
	return event
}

// ResourceActivityDisplayForItem completes a service item display from its
// decoded event fields. The result remains presentation data, not a AgentTeam DTO.
func ResourceActivityDisplayForItem(item ResourceActivityItem) ResourceActivityDisplay {
	repo := ResourceActivityDataPlaneRepository{
		WorkspaceID:  item.WorkspaceID,
		ResourceType: item.AssetType,
		ResourceID:   item.AssetID,
		AssetType:    item.AssetType,
		AssetID:      item.AssetID,
		RepoFullName: item.RepoFullName,
	}
	event := ResourceActivityDataPlaneEvent{
		ActorType:  item.Actor.Type,
		ActorID:    item.Actor.ID,
		ActorName:  item.Actor.Name,
		Details:    item.Details,
		Display:    item.Display,
		TargetKind: item.Target.Kind,
		TargetID:   item.Target.ID,
		TargetURL:  item.Target.URL,
	}
	return resourceActivityDisplayFor(repo, event, item.Details)
}

func defaultResourceActivityDisplayMeta(display ResourceActivityDisplay, event activitystore.ResourceActivityEvent, details map[string]any) []ResourceActivityDisplayMeta {
	meta := make([]ResourceActivityDisplayMeta, 0, 4)
	if !display.Target.empty() {
		meta = append(meta, ResourceActivityDisplayMeta{
			Key:         firstNonEmpty(display.Target.Kind, "target"),
			Value:       firstNonEmpty(display.Target.Label, display.Target.ID),
			Href:        display.Target.Href,
			ExternalURL: display.Target.ExternalURL,
			Title:       firstNonEmpty(display.Target.ID, display.Target.Label),
		})
	}
	if detail := primaryActivityDetailMeta(details, event.AssetType); detail != "" && !metaHasValue(meta, detail) {
		meta = append(meta, ResourceActivityDisplayMeta{Key: "detail", Value: detail})
	}
	if repoRole := detailStringValue(details, "repoRole", "repositoryRole"); repoRole != "" && !metaHasValue(meta, repoRole) {
		meta = append(meta, ResourceActivityDisplayMeta{Key: "repoRole", Value: repoRole})
	}
	if repo := strings.TrimSpace(event.RepoFullName); repo != "" && display.Resource.Name == "" && !metaHasValue(meta, repo) {
		meta = append(meta, ResourceActivityDisplayMeta{Key: "repo", Value: repo})
	}
	return meta
}

func metaHasValue(meta []ResourceActivityDisplayMeta, value string) bool {
	value = strings.TrimSpace(value)
	if value == "" {
		return false
	}
	for _, item := range meta {
		if strings.TrimSpace(item.Value) == value || strings.TrimSpace(item.Label) == value {
			return true
		}
	}
	return false
}

func resourceActivityTargetLabel(kind string, id string, details map[string]any) string {
	kind = strings.TrimSpace(kind)
	id = strings.TrimSpace(id)
	switch kind {
	case "commit":
		return shortDisplayIdentifier(firstNonEmpty(detailStringValue(details, "sha", "commitSHA", "commitSha", "commit", "commitId"), id))
	case "issue", "pull":
		number := firstNonEmpty(detailStringValue(details, "number"), id)
		if number == "" {
			return ""
		}
		if strings.HasPrefix(number, "#") {
			return number
		}
		return "#" + number
	case "tag":
		return firstNonEmpty(detailStringValue(details, "tagName", "tag", "name"), id)
	case "release":
		return firstNonEmpty(detailStringValue(details, "releaseName", "name"), id)
	case "wiki_page":
		return firstNonEmpty(detailStringValue(details, "pageTitle", "title", "slug"), id)
	case "source":
		return firstNonEmpty(detailStringValue(details, "sourceName", "name"), id)
	case "automation":
		return firstNonEmpty(detailStringValue(details, "jobId", "stage", "status"), id)
	case "branch":
		return firstNonEmpty(detailStringValue(details, "branchName", "branch"), id)
	default:
		return id
	}
}

func primaryActivityDetailMeta(details map[string]any, assetType string) string {
	if value := detailStringValue(details, "sourceName", "source_name", "pageTitle", "title", "path", "filePath", "subjectName", "agentName", "role"); value != "" {
		return value
	}
	if strings.TrimSpace(assetType) == activitystore.AssetTypeMemory {
		return ""
	}
	return detailStringValue(details, "branch", "ref")
}

func detailStringValue(details map[string]any, keys ...string) string {
	for _, key := range keys {
		value := details[key]
		switch typed := value.(type) {
		case string:
			if trimmed := strings.TrimSpace(typed); trimmed != "" {
				return trimmed
			}
		case fmt.Stringer:
			if trimmed := strings.TrimSpace(typed.String()); trimmed != "" {
				return trimmed
			}
		case int:
			return fmt.Sprintf("%d", typed)
		case int64:
			return fmt.Sprintf("%d", typed)
		case float64:
			if typed == float64(int64(typed)) {
				return fmt.Sprintf("%d", int64(typed))
			}
		}
	}
	return ""
}

func readableActorID(id string) string {
	id = strings.TrimSpace(id)
	if id == "" {
		return ""
	}
	if strings.Contains(id, "@") {
		local, _, ok := strings.Cut(id, "@")
		if ok && strings.TrimSpace(local) != "" {
			return strings.TrimSpace(local)
		}
	}
	return id
}

func looksInternalActorID(id string) bool {
	id = strings.TrimSpace(id)
	return strings.HasPrefix(id, "usr_") || strings.HasPrefix(id, "agt_") || strings.HasPrefix(id, "mem_") || strings.HasPrefix(id, "mbr_")
}

func shortDisplayIdentifier(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= 12 {
		return value
	}
	return value[:12]
}
