package approvalapp

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

	"github.com/tidbcloud/AgentTeam/internal/service/collaborator"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// CollaboratorRequestConsumer is the transport-neutral collaborator-request
// service path driven by the Approval subject handler.
type CollaboratorRequestConsumer interface {
	Submit(context.Context, collaborator.SubmitCommand) (collaborator.SubmitResult, error)
	LookupReplay(context.Context, collaborator.SubmitCommand) (collaborator.ReplayResult, bool, error)
	Decide(context.Context, approval.DecideParams) (collaborator.DecideResult, error)
	Cancel(context.Context, collaborator.CancelCommand) (collaborator.DecideResult, error)
	// TerminalizeExpired settles a still-pending collaborator request row to
	// 'expired' once its Approval reached the authoritative expired terminal
	// state (idempotent; only transitions rows whose Approval is actually
	// expired). The requester status view reads the request row, so without
	// this the lazy-expiry path would leave it 'pending' forever.
	TerminalizeExpired(context.Context, string, string) error
}

// CollaboratorRequestSubmitInput is the client-supplied submit payload. The
// requester identity and execution owner are ALWAYS derived server-side; this
// carries only the target + role/expiry facts.
type CollaboratorRequestSubmitInput struct {
	ScopeType             string     `json:"scopeType"`
	ScopeID               string     `json:"scopeId"`
	Role                  string     `json:"role,omitempty"`
	CollaboratorExpiresAt *time.Time `json:"collaboratorExpiresAt,omitempty"`
}

type collaboratorRequestSubjectHandler struct {
	approvals *approval.Store
	service   CollaboratorRequestConsumer
	projector *Projector
}

// NewCollaboratorRequestSubjectHandler returns the unregistered adapter for the
// collaborator-request subject.
func NewCollaboratorRequestSubjectHandler(
	approvals *approval.Store,
	service CollaboratorRequestConsumer,
	projector *Projector,
) SubjectHandler {
	return &collaboratorRequestSubjectHandler{
		approvals: approvals, service: service, projector: projector,
	}
}

func (*collaboratorRequestSubjectHandler) SubjectType() string {
	return collaborator.SubjectType
}

func (*collaboratorRequestSubjectHandler) RequestType() string {
	return collaborator.RequestType
}

func (*collaboratorRequestSubjectHandler) CancelPolicy() SubjectCancelPolicy {
	return SubjectCancelByApplicant
}

func (*collaboratorRequestSubjectHandler) ReconcilePolicy() SubjectReconcilePolicy {
	return SubjectReconcileProjection
}

func (handler *collaboratorRequestSubjectHandler) Validate(
	_ context.Context,
	instance approval.Instance,
) error {
	if handler == nil || handler.approvals == nil || handler.service == nil || handler.projector == nil {
		return fmt.Errorf("collaborator request Approval subject handler is not configured")
	}
	if instance.SubjectType != collaborator.SubjectType {
		return fmt.Errorf("collaborator request Approval subject type mismatch")
	}
	var payload collaborator.ApprovalPayload
	if err := json.Unmarshal(instance.Payload, &payload); err != nil {
		return fmt.Errorf("collaborator request Approval payload is invalid: %w", err)
	}
	if payload.RequestID == "" || payload.RequesterMemberID == "" || payload.OwnerMemberID == "" ||
		payload.ScopeType == "" || payload.ScopeID == "" || payload.Role == "" {
		return fmt.Errorf("collaborator request Approval payload is incomplete")
	}
	return nil
}

func (*collaboratorRequestSubjectHandler) DecodeSubmitPayload(value any) (any, error) {
	switch value := value.(type) {
	case CollaboratorRequestSubmitInput:
		return value, nil
	case json.RawMessage:
		var input CollaboratorRequestSubmitInput
		decoder := json.NewDecoder(strings.NewReader(string(value)))
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(&input); err != nil {
			return nil, storeerr.BadRequest("collaborator request payload is invalid")
		}
		var trailing any
		if err := decoder.Decode(&trailing); err != io.EOF {
			return nil, storeerr.BadRequest("collaborator request payload is invalid")
		}
		input.ScopeType = strings.TrimSpace(input.ScopeType)
		input.ScopeID = strings.TrimSpace(input.ScopeID)
		input.Role = strings.TrimSpace(input.Role)
		if input.ScopeType == "" || input.ScopeID == "" {
			return nil, storeerr.BadRequest("collaborator request scopeType and scopeId are required")
		}
		return input, nil
	default:
		return nil, storeerr.BadRequest("collaborator request payload is invalid")
	}
}

func (handler *collaboratorRequestSubjectHandler) Submit(
	ctx context.Context,
	command SubmitCommand,
) (SubmitResult, error) {
	input, ok := command.Payload.(CollaboratorRequestSubmitInput)
	if !ok {
		return SubmitResult{}, storeerr.BadRequest("collaborator request payload is invalid")
	}
	principal := command.Principal
	if strings.TrimSpace(principal.WorkspaceID) == "" || strings.TrimSpace(principal.MemberID) == "" {
		return SubmitResult{}, storeerr.Forbidden("collaborator request requester identity is incomplete")
	}
	result, err := handler.service.Submit(ctx, collaborator.SubmitCommand{
		WorkspaceID:           principal.WorkspaceID,
		RequesterUserID:       principal.PrincipalID,
		ScopeType:             input.ScopeType,
		ScopeID:               input.ScopeID,
		Role:                  input.Role,
		CollaboratorExpiresAt: input.CollaboratorExpiresAt,
		ClientRequestID:       command.ClientRequestID,
		Now:                   command.Now,
	})
	if err != nil {
		return SubmitResult{}, err
	}
	// A no-op submit has no Approval instance to project.
	if result.Status == "noop" {
		return SubmitResult{Value: result}, nil
	}
	if handler.approvals == nil {
		return SubmitResult{}, fmt.Errorf("collaborator request Approval store is not configured")
	}
	instance, err := handler.approvals.Load(ctx, principal.WorkspaceID, result.ApprovalID)
	if err != nil {
		return SubmitResult{}, err
	}
	assignments, err := handler.approvals.ListAssignments(ctx, instance.WorkspaceID, instance.ID)
	if err != nil {
		return SubmitResult{}, err
	}
	return SubmitResult{
		Instance: instance, Assignments: assignments,
		Created: result.Created, Value: result,
	}, nil
}

func (handler *collaboratorRequestSubjectHandler) LookupReplay(
	ctx context.Context,
	command SubmitCommand,
) (SubmitResult, bool, error) {
	input, ok := command.Payload.(CollaboratorRequestSubmitInput)
	if !ok {
		return SubmitResult{}, false, storeerr.BadRequest("collaborator request payload is invalid")
	}
	principal := command.Principal
	replay, found, err := handler.service.LookupReplay(ctx, collaborator.SubmitCommand{
		WorkspaceID:     principal.WorkspaceID,
		RequesterUserID: principal.PrincipalID,
		ScopeType:       input.ScopeType,
		ScopeID:         input.ScopeID,
		Role:            input.Role,
		ClientRequestID: command.ClientRequestID,
		Now:             command.Now,
	})
	if err != nil || !found {
		return SubmitResult{}, found, err
	}
	if handler.approvals == nil {
		return SubmitResult{}, false, fmt.Errorf("collaborator request Approval store is not configured")
	}
	instance, err := handler.approvals.Load(ctx, principal.WorkspaceID, replay.ApprovalID)
	if err != nil {
		return SubmitResult{}, false, err
	}
	assignments, err := handler.approvals.ListAssignments(ctx, instance.WorkspaceID, instance.ID)
	if err != nil {
		return SubmitResult{}, false, err
	}
	return SubmitResult{
		Instance: instance, Assignments: assignments,
		Created: false, Value: replay,
	}, true, nil
}

func (handler *collaboratorRequestSubjectHandler) Decide(
	ctx context.Context,
	command DecisionCommand,
) (DecisionResult, error) {
	result, err := handler.service.Decide(ctx, approval.DecideParams{
		WorkspaceID:      command.WorkspaceID,
		ApprovalID:       command.ApprovalID,
		AssignmentID:     command.AssignmentID,
		ActorMemberID:    command.ActorMemberID,
		ExpectedVersion:  command.ExpectedVersion,
		ClientDecisionID: command.ClientDecisionID,
		Decision:         command.Decision,
		Reason:           command.Reason,
		Now:              command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	if handler.approvals == nil {
		return DecisionResult{}, fmt.Errorf("collaborator request Approval store is not configured")
	}
	instance, err := handler.approvals.Load(ctx, command.WorkspaceID, command.ApprovalID)
	if err != nil {
		return DecisionResult{}, err
	}
	return DecisionResult{
		Instance: instance,
		Value:    result,
		Result: map[string]any{
			"approvalId": result.ApprovalID,
			"requestId":  result.RequestID,
			"status":     result.Status,
		},
	}, nil
}

func (handler *collaboratorRequestSubjectHandler) Cancel(
	ctx context.Context,
	command CancelCommand,
) (DecisionResult, error) {
	result, err := handler.service.Cancel(ctx, collaborator.CancelCommand{
		WorkspaceID:          command.Principal.WorkspaceID,
		RequesterUserID:      command.Principal.PrincipalID,
		ApprovalID:           command.ApprovalID,
		ExpectedVersion:      command.ExpectedVersion,
		ClientCancellationID: command.ClientRequestID,
		Reason:               "applicant cancelled",
		Now:                  command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	if handler.approvals == nil {
		return DecisionResult{}, fmt.Errorf("collaborator request Approval store is not configured")
	}
	instance, err := handler.approvals.Load(ctx, command.Principal.WorkspaceID, command.ApprovalID)
	if err != nil {
		return DecisionResult{}, err
	}
	return DecisionResult{
		Instance: instance,
		Value:    result,
		Result: map[string]any{
			"approvalId": result.ApprovalID,
			"requestId":  result.RequestID,
			"status":     result.Status,
		},
	}, nil
}

func (handler *collaboratorRequestSubjectHandler) PresentPending(
	ctx context.Context,
	result SubmitResult,
) (app.AppNotificationResult, error) {
	if handler == nil || handler.projector == nil ||
		handler.projector.notifications == nil || handler.projector.provisioner == nil {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request Approval projector is not configured")
	}
	instance := result.Instance
	if instance.SubjectType != collaborator.SubjectType ||
		instance.State != approval.StatePending || instance.Version <= 0 ||
		len(result.Assignments) != 1 {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request Approval is not pending")
	}
	assignment := result.Assignments[0]
	if assignment.WorkspaceID != instance.WorkspaceID ||
		assignment.ApprovalInstanceID != instance.ID ||
		assignment.ApproverMemberID == "" ||
		assignment.Status != approval.AssignmentPending {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request Approval assignment is invalid")
	}
	payload, err := decodeCollaboratorRequestPayload(instance)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if _, err := handler.projector.provisioner.EnsureWorkspace(
		ctx, instance.WorkspaceID, time.Now().UTC(),
	); err != nil {
		return app.AppNotificationResult{}, err
	}
	content, err := collaboratorRequestCardContent(
		instance, assignment.ID, assignment.ApproverMemberID, payload, approval.StatePending,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return handler.projector.notifications.CreateAppNotificationForBuiltin(
		ctx, IdentityID, instance.WorkspaceID, app.AppNotificationCommand{
			ClientNotificationID: collaboratorRequestReviewClientNotificationID(instance.ID),
			Target: app.AppNotificationTarget{
				Kind: app.AppNotificationTargetHumanDM, MemberID: assignment.ApproverMemberID,
			},
			Content: content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeInstance,
				ID: instance.ID, Version: instance.Version,
			},
			RecipientBasis: app.AppNotificationRecipientBasis{
				Kind: RecipientBasisAssignment, Ref: assignment.ID,
			},
			CausedByActor: app.AppNotificationActorRef{
				Type: "human", ID: payload.RequesterMemberID,
			},
			CorrelationID: instance.CorrelationID,
		},
	)
}

func (handler *collaboratorRequestSubjectHandler) PresentResolved(
	ctx context.Context,
	result DecisionResult,
	presentation PresentationContext,
) (app.AppNotificationResult, error) {
	if handler == nil || handler.projector == nil ||
		handler.projector.notifications == nil || handler.projector.provisioner == nil ||
		handler.approvals == nil {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request Approval projector is not configured")
	}
	instance := result.Instance
	if instance.SubjectType != collaborator.SubjectType {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request resolved Approval is invalid")
	}
	payload, err := decodeCollaboratorRequestPayload(instance)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	// Durable requester-status settlement: once the Approval reached the
	// authoritative expired terminal state, the request row follows so the
	// requester status view stops showing a stale pending. Idempotent; the
	// settlement only transitions a row whose Approval is actually expired.
	if instance.State == approval.StateExpired && handler.service != nil {
		if err := handler.service.TerminalizeExpired(ctx, instance.WorkspaceID, payload.RequestID); err != nil {
			return app.AppNotificationResult{}, err
		}
	}
	// Find the stable review notification to resolve.
	if _, err := handler.projector.provisioner.EnsureWorkspace(
		ctx, instance.WorkspaceID, time.Now().UTC(),
	); err != nil {
		return app.AppNotificationResult{}, err
	}
	notificationID := strings.TrimSpace(presentation.NotificationID)
	if notificationID != "" {
		notification, err := handler.projector.notifications.LoadAppNotification(ctx, instance.WorkspaceID, notificationID)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		if notification.Status == app.AppNotificationStatusResolved {
			return handler.projector.notifications.GetAppNotificationForBuiltin(ctx, IdentityID, instance.WorkspaceID, notification.ID)
		}
		content, err := collaboratorRequestCardContent(instance, "", "", payload, instance.State)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		version := instance.Version
		command := app.AppNotificationMutationCommand{
			NotificationID:        notification.ID,
			Kind:                  app.AppNotificationMutationResolve,
			ExpectedRevision:      notification.Revision,
			ClientMutationID:      fmt.Sprintf("collaborator-request:%s:%d", instance.ID, instance.Version),
			Content:               &content,
			BusinessObjectVersion: &version,
			CausedByActor: app.AppNotificationActorRef{
				Type: "human", ID: instance.DecidedByMemberID,
			},
			CorrelationID: instance.CorrelationID,
		}
		return handler.projector.notifications.MutateAppNotificationForBuiltin(
			ctx, IdentityID, instance.WorkspaceID, command,
		)
	}
	// No action notification: create an applicant-facing result card.
	if handler.projector.notifications == nil {
		return app.AppNotificationResult{}, fmt.Errorf("collaborator request Approval projector is not configured")
	}
	content, err := collaboratorRequestCardContent(instance, "", "", payload, instance.State)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return handler.projector.notifications.CreateAppNotificationForBuiltin(
		ctx, IdentityID, instance.WorkspaceID, app.AppNotificationCommand{
			ClientNotificationID: collaboratorRequestResultClientNotificationID(instance.ID, instance.Version),
			Target: app.AppNotificationTarget{
				Kind: app.AppNotificationTargetHumanDM, MemberID: payload.RequesterMemberID,
			},
			Content: content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeInstance,
				ID: instance.ID, Version: instance.Version,
			},
			RecipientBasis: app.AppNotificationRecipientBasis{
				Kind: RecipientBasisApplicant, Ref: instance.ID,
			},
			CausedByActor: app.AppNotificationActorRef{
				Type: "human", ID: instance.DecidedByMemberID,
			},
			CorrelationID: instance.CorrelationID,
		},
	)
}

func decodeCollaboratorRequestPayload(instance approval.Instance) (collaborator.ApprovalPayload, error) {
	var payload collaborator.ApprovalPayload
	if err := json.Unmarshal(instance.Payload, &payload); err != nil {
		return collaborator.ApprovalPayload{}, err
	}
	return payload, nil
}

func collaboratorRequestCardContent(
	instance approval.Instance,
	assignmentID, approverMemberID string,
	payload collaborator.ApprovalPayload,
	status string,
) (app.AppNotificationContent, error) {
	card := map[string]any{
		"schemaVersion": "1",
		"template":      "approval_action",
		"titleKey":      "collaborator_request_review",
		"status":        status,
		"requester": map[string]any{
			"memberId": payload.RequesterMemberID,
		},
		"proposal": map[string]any{
			"scopeType": payload.ScopeType,
			"scopeId":   payload.ScopeID,
			"label":     payload.ScopeDisplayLabel,
			"role":      payload.Role,
			"effect":    payload.Effect,
		},
		"collaboratorExpiry": collaboratorExpiryValue(payload.CollaboratorExpiresAt),
		"approvalExpiry":     collaboratorExpiryValue(payload.ApprovalExpiresAt),
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version,
			"assignmentId": assignmentID,
		},
		"actions": []map[string]any{},
	}
	if status == approval.StatePending {
		actorPolicy := map[string]any{
			"kind": "members", "memberIds": []string{approverMemberID},
		}
		card["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},
					},
				},
			},
		}
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	return app.AppNotificationContent{
		Kind: app.AppNotificationContentInteractiveCard,
		FallbackText: fmt.Sprintf(
			"协作申请：%s %s (%s)", payload.ScopeType, payload.ScopeID, status,
		),
		CardJSON: encoded,
	}, nil
}

func collaboratorExpiryValue(value *time.Time) any {
	if value == nil {
		return nil
	}
	return value.UTC().Format(time.RFC3339)
}

func collaboratorRequestReviewClientNotificationID(approvalID string) string {
	return "collaborator-request-review:" + approvalID
}

func collaboratorRequestResultClientNotificationID(approvalID string, version int64) string {
	return fmt.Sprintf("collaborator-request-result:%s:%d", approvalID, version)
}
