package approvalapp

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

	"github.com/tidbcloud/AgentTeam/internal/service/team/roleproposal"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

// teamRoleProposalPayload is the minimal immutable projection contract read by
// Approval. Keeping it local avoids coupling presentation to the domain
// service's concrete payload type.
type teamRoleProposalPayload struct {
	SchemaVersion int `json:"schemaVersion"`
	Applicant     struct {
		AgentID string `json:"agentId"`
	} `json:"applicant"`
	Base struct {
		Team struct {
			Description string `json:"description"`
		} `json:"team"`
		Members []struct {
			TeamMemberID      string `json:"teamMemberId"`
			MemberName        string `json:"memberName,omitempty"`
			RoleLabel         string `json:"roleLabel"`
			RoleOverlayPrompt string `json:"roleOverlayPrompt"`
			WakeOnTeamMention bool   `json:"wakeOnTeamMention"`
		} `json:"members"`
	} `json:"base"`
	Proposed struct {
		Description *string `json:"description,omitempty"`
		Members     []struct {
			TeamMemberID      string  `json:"teamMemberId"`
			MemberName        string  `json:"memberName,omitempty"`
			RoleLabel         *string `json:"roleLabel,omitempty"`
			RoleOverlayPrompt *string `json:"roleOverlayPrompt,omitempty"`
			WakeOnTeamMention *bool   `json:"wakeOnTeamMention,omitempty"`
		} `json:"members"`
	} `json:"proposed"`
}

type teamRoleProposalMemberDiff struct {
	TeamMemberID string
	MemberName   string
	Changes      map[string]any
}

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

func (payload teamRoleProposalPayload) diff() (*map[string]any, []teamRoleProposalMemberDiff) {
	baseMembers := make(map[string]struct {
		name, roleLabel, overlay string
		wake                     bool
	}, len(payload.Base.Members))
	for _, member := range payload.Base.Members {
		memberID := strings.TrimSpace(member.TeamMemberID)
		baseMembers[memberID] = struct {
			name, roleLabel, overlay string
			wake                     bool
		}{
			name: strings.TrimSpace(member.MemberName), roleLabel: strings.TrimSpace(member.RoleLabel),
			overlay: strings.TrimSpace(member.RoleOverlayPrompt), wake: member.WakeOnTeamMention,
		}
	}
	members := make([]teamRoleProposalMemberDiff, 0, len(payload.Proposed.Members))
	for _, member := range payload.Proposed.Members {
		memberID := strings.TrimSpace(member.TeamMemberID)
		before := baseMembers[memberID]
		changes := make(map[string]any, 3)
		if member.RoleLabel != nil {
			changes["roleLabel"] = map[string]any{
				"before": before.roleLabel, "after": strings.TrimSpace(*member.RoleLabel),
			}
		}
		if member.RoleOverlayPrompt != nil {
			changes["roleOverlayPrompt"] = map[string]any{
				"before": before.overlay, "after": strings.TrimSpace(*member.RoleOverlayPrompt),
			}
		}
		if member.WakeOnTeamMention != nil {
			changes["wakeOnTeamMention"] = map[string]any{
				"before": before.wake, "after": *member.WakeOnTeamMention,
			}
		}
		members = append(members, teamRoleProposalMemberDiff{
			TeamMemberID: memberID,
			MemberName:   firstNonEmpty(strings.TrimSpace(member.MemberName), before.name, memberID),
			Changes:      changes,
		})
	}
	var description *map[string]any
	if payload.Proposed.Description != nil {
		value := map[string]any{
			"before": strings.TrimSpace(payload.Base.Team.Description),
			"after":  strings.TrimSpace(*payload.Proposed.Description),
		}
		description = &value
	}
	return description, members
}

// projectPending creates one stable interactive diff card in the approver's
// Approval DM. Team approval reuses the generic assignment recipient fact;
// the Team channel is not an approval action path.
func (handler *teamRoleProposalSubjectHandler) projectPending(
	ctx context.Context,
	result roleproposal.Result,
) (app.AppNotificationResult, error) {
	projector := handler.projector
	proposal := result.Proposal
	instance := result.Approval
	if strings.TrimSpace(proposal.ID) == "" ||
		strings.TrimSpace(proposal.WorkspaceID) == "" ||
		strings.TrimSpace(proposal.TeamID) == "" {
		return app.AppNotificationResult{}, fmt.Errorf("Team role proposal identity is required")
	}
	if instance.SubjectType != SubjectTypeTeamRoleProposal ||
		instance.SubjectRef != proposal.TeamID ||
		instance.ID != proposal.ID ||
		instance.State != approval.StatePending ||
		instance.Version <= 0 {
		return app.AppNotificationResult{}, fmt.Errorf("Team role proposal Approval is not pending or does not match")
	}
	if len(result.Assignments) != 1 ||
		result.Assignments[0].ApprovalInstanceID != instance.ID ||
		result.Assignments[0].Status != approval.AssignmentPending {
		return app.AppNotificationResult{}, fmt.Errorf("Team role proposal must have one pending creator assignment")
	}
	if _, err := projector.provisioner.EnsureWorkspace(
		ctx, proposal.WorkspaceID, time.Now().UTC(),
	); err != nil {
		return app.AppNotificationResult{}, err
	}
	assignment := result.Assignments[0]
	currentProcessorName, err := projector.memberDisplayName(
		ctx, proposal.WorkspaceID, assignment.ApproverMemberID, "",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	var payload teamRoleProposalPayload
	if err := json.Unmarshal(proposal.Payload, &payload); err != nil {
		return app.AppNotificationResult{}, fmt.Errorf("decode Team role proposal payload: %w", err)
	}
	content, err := pendingTeamRoleProposalCardContent(
		proposal, instance, assignment, payload, currentProcessorName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return projector.notifications.CreateAppNotificationForBuiltin(
		ctx,
		IdentityID,
		proposal.WorkspaceID,
		app.AppNotificationCommand{
			ClientNotificationID: teamRoleProposalClientNotificationID(proposal.ID),
			Target: app.AppNotificationTarget{
				Kind: app.AppNotificationTargetHumanDM, MemberID: assignment.ApproverMemberID,
			},
			RecipientBasis: app.AppNotificationRecipientBasis{Kind: RecipientBasisAssignment, Ref: assignment.ID},
			Content:        content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeInstance,
				ID: instance.ID, Version: instance.Version,
			},
			CausedByActor: app.AppNotificationActorRef{
				Type: "agent", ID: payload.Applicant.AgentID,
			},
			CorrelationID: instance.CorrelationID,
		},
	)
}

func (handler *teamRoleProposalSubjectHandler) resolveCard(
	ctx context.Context,
	proposal roleproposal.Proposal,
	instance approval.Instance,
	currentNotificationID, deliveryID, attemptID, actionID string,
) (app.AppNotificationResult, error) {
	projector := handler.projector
	var payload teamRoleProposalPayload
	if err := json.Unmarshal(proposal.Payload, &payload); err != nil {
		return app.AppNotificationResult{}, fmt.Errorf("decode Team role proposal payload: %w", err)
	}
	processorName, err := projector.memberDisplayName(
		ctx, proposal.WorkspaceID, instance.DecidedByMemberID, "",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	content, err := resolvedTeamRoleProposalCardContent(
		proposal, instance, payload, processorName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	notification, err := projector.notifications.LoadAppNotification(
		ctx, proposal.WorkspaceID, currentNotificationID,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if notification.TargetKind != app.AppNotificationTargetHumanDM ||
		notification.RecipientMemberID == nil ||
		notification.RecipientBasisKind == nil ||
		notification.RecipientBasisRef == nil ||
		strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAssignment ||
		notification.BusinessObjectID == nil ||
		strings.TrimSpace(*notification.BusinessObjectID) != instance.ID {
		return app.AppNotificationResult{}, fmt.Errorf("Team role proposal action notification does not match")
	}
	version := instance.Version
	return projector.notifications.MutateAppNotificationForBuiltinDelivery(
		ctx,
		IdentityID,
		proposal.WorkspaceID,
		deliveryID,
		attemptID,
		app.AppNotificationMutationCommand{
			NotificationID:   currentNotificationID,
			Kind:             app.AppNotificationMutationResolve,
			ExpectedRevision: notification.Revision,
			ClientMutationID: "team-role-proposal-action:" +
				strings.TrimSpace(actionID) + ":" + currentNotificationID,
			Content:               &content,
			BusinessObjectVersion: &version,
			CausedByActor: app.AppNotificationActorRef{
				Type: "human", ID: instance.DecidedByMemberID,
			},
			CorrelationID: instance.CorrelationID,
		},
	)
}

func (handler *teamRoleProposalSubjectHandler) projectResolved(
	ctx context.Context,
	proposal roleproposal.Proposal,
	instance approval.Instance,
) (app.AppNotificationResult, error) {
	projector := handler.projector
	provisioned, err := projector.provisioner.EnsureWorkspace(
		ctx, proposal.WorkspaceID, time.Now().UTC(),
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	notification, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx, proposal.WorkspaceID, provisioned.Installation.ID,
		teamRoleProposalClientNotificationID(instance.ID),
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if !found {
		// A terminal Approval has no active assignment with which to create a new
		// actionable DM. If its pending projection never existed, converge the
		// projection watermark without manufacturing a historical Channel card or
		// a second recipient contract. Normal decisions resolve the existing DM.
		return app.AppNotificationResult{}, nil
	}
	if notification.Status == app.AppNotificationStatusResolved {
		result, err := projector.notifications.GetAppNotificationForBuiltin(
			ctx, IdentityID, proposal.WorkspaceID, notification.ID,
		)
		if err == nil {
			result.AlreadyCreated = true
		}
		return result, err
	}
	if notification.Status != app.AppNotificationStatusActive {
		return app.AppNotificationResult{}, fmt.Errorf(
			"Team role proposal card cannot converge from status %q",
			notification.Status,
		)
	}
	var payload teamRoleProposalPayload
	if err := json.Unmarshal(proposal.Payload, &payload); err != nil {
		return app.AppNotificationResult{}, fmt.Errorf(
			"decode Team role proposal payload: %w", err,
		)
	}
	processorName, err := projector.memberDisplayName(
		ctx, proposal.WorkspaceID, instance.DecidedByMemberID, "",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	content, err := resolvedTeamRoleProposalCardContent(
		proposal, instance, payload, processorName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	version := instance.Version
	return projector.notifications.MutateAppNotificationForBuiltin(
		ctx, IdentityID, proposal.WorkspaceID,
		app.AppNotificationMutationCommand{
			NotificationID:   notification.ID,
			Kind:             app.AppNotificationMutationResolve,
			ExpectedRevision: notification.Revision,
			ClientMutationID: "team-role-proposal-terminal:" +
				instance.ID + ":" + fmt.Sprint(instance.Version),
			Content:               &content,
			BusinessObjectVersion: &version,
			CausedByActor:         terminalTeamRoleProposalActor(instance, payload),
			CorrelationID:         instance.CorrelationID,
		},
	)
}

func terminalTeamRoleProposalActor(
	instance approval.Instance,
	payload teamRoleProposalPayload,
) app.AppNotificationActorRef {
	if instance.State == approval.StateCancelled &&
		instance.DecisionReason == "withdrawn_by_leader" {
		return app.AppNotificationActorRef{
			Type: "agent", ID: strings.TrimSpace(payload.Applicant.AgentID),
		}
	}
	return app.AppNotificationActorRef{
		Type: "human", ID: strings.TrimSpace(instance.DecidedByMemberID),
	}
}

func pendingTeamRoleProposalCardContent(
	proposal roleproposal.Proposal,
	instance approval.Instance,
	assignment approval.Assignment,
	payload teamRoleProposalPayload,
	currentProcessorName string,
) (app.AppNotificationContent, error) {
	description, members := payload.diff()
	memberDiffs := make([]map[string]any, 0, len(members))
	for _, member := range members {
		item := map[string]any{
			"teamMemberId": member.TeamMemberID,
			"memberName":   member.MemberName,
		}
		for key, value := range member.Changes {
			item[key] = value
		}
		memberDiffs = append(memberDiffs, item)
	}
	actorPolicy := map[string]any{
		"kind":      "members",
		"memberIds": []string{strings.TrimSpace(assignment.ApproverMemberID)},
	}
	card := map[string]any{
		"schemaVersion": "1",
		"template":      "approval_diff",
		"title":         "审批 Team 角色方案",
		"titleKey":      "team_role_proposal_review",
		"status":        approval.StatePending,
		"proposal":      map[string]any{"teamId": proposal.TeamID},
		"diff":          map[string]any{"members": memberDiffs},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version,
			"assignmentId":          assignment.ID,
			"currentProcessorNames": []string{strings.TrimSpace(currentProcessorName)},
		},
		"actions": []map[string]any{
			{
				"actionKey": ActionApprove, "type": "callback", "label": "通过",
				"semantics": ActionSemanticsCommit, "actorPolicy": actorPolicy,
			},
			{
				"actionKey": ActionReject, "type": "callback", "label": "暂不通过",
				"semantics": ActionSemanticsRevise, "actorPolicy": actorPolicy,
				"inputSchema": map[string]any{
					"type": "object",
					"properties": map[string]any{
						"reason": map[string]any{"type": "string", "maxLength": 1000},
						"locale": map[string]any{
							"type": "string", "enum": []string{"en", "zh-CN", "ja"},
						},
					},
					"additionalProperties": false,
				},
			},
		},
	}
	if description != nil {
		card["diff"].(map[string]any)["description"] = *description
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal pending Team role proposal card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: "待审批：Team 角色方案",
		CardJSON:     encoded,
	}, nil
}

func resolvedTeamRoleProposalCardContent(
	proposal roleproposal.Proposal,
	instance approval.Instance,
	payload teamRoleProposalPayload,
	processorName string,
) (app.AppNotificationContent, error) {
	title := "Team 角色方案已通过"
	titleKey := "team_role_proposal_approved"
	if instance.State == approval.StateRejected {
		title = "Team 角色方案已退回"
		titleKey = "team_role_proposal_rejected"
	} else if instance.State == approval.StateCancelled {
		if instance.DecisionReason == "stale_subject" {
			title = "Team 已变化，请 Leader 重新提交"
			titleKey = "team_role_proposal_stale"
		} else {
			title = "Team 角色方案已撤回"
			titleKey = "team_role_proposal_withdrawn"
		}
	}
	description, members := payload.diff()
	memberDiffs := make([]map[string]any, 0, len(members))
	for _, member := range members {
		item := map[string]any{
			"teamMemberId": member.TeamMemberID,
			"memberName":   member.MemberName,
		}
		for key, value := range member.Changes {
			item[key] = value
		}
		memberDiffs = append(memberDiffs, item)
	}
	card := map[string]any{
		"schemaVersion": "1",
		"template":      "approval_diff",
		"title":         title,
		"titleKey":      titleKey,
		"status":        instance.State,
		"proposal":      map[string]any{"teamId": proposal.TeamID},
		"diff":          map[string]any{"members": memberDiffs},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version, "state": instance.State,
			"processorName": strings.TrimSpace(processorName),
			"reason":        instance.DecisionReason,
		},
	}
	if description != nil {
		card["diff"].(map[string]any)["description"] = *description
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal resolved Team role proposal card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: title,
		CardJSON:     encoded,
	}, nil
}
