package approvalapp

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

	"github.com/tidbcloud/AgentTeam/internal/service/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/service/team/roleproposal"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type serviceAccountCreateSubjectHandler struct {
	approvals *approval.Store
	service   *serviceaccount.Service
	projector *Projector
}

func (handler *serviceAccountCreateSubjectHandler) PresentResolved(
	ctx context.Context,
	result DecisionResult,
	presentation PresentationContext,
) (app.AppNotificationResult, error) {
	consumed, ok := result.Value.(serviceaccount.WorkspaceCreateRequestResult)
	if !ok {
		return app.AppNotificationResult{}, fmt.Errorf("service account Approval result is invalid")
	}
	resolved, err := handler.projector.resolveApprovalCards(
		ctx, consumed.Request, result.Instance.WorkspaceID, result.Instance.ID,
		presentation.NotificationID, presentation.DeliveryID,
		presentation.AttemptID, presentation.MutationID, result.Instance,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if _, err := handler.projector.projectApplicantResult(
		ctx, consumed.Request, result.Instance, consumed.Account,
	); err != nil {
		return app.AppNotificationResult{}, err
	}
	return resolved, nil
}

func NewServiceAccountCreateSubjectHandler(
	approvals *approval.Store,
	service *serviceaccount.Service,
	projector *Projector,
) SubjectHandler {
	return &serviceAccountCreateSubjectHandler{
		approvals: approvals, service: service, projector: projector,
	}
}

func (*serviceAccountCreateSubjectHandler) SubjectType() string {
	return SubjectTypeServiceAccountCreate
}

func (*serviceAccountCreateSubjectHandler) CancelPolicy() SubjectCancelPolicy {
	return SubjectCancelNotSupported
}

func (*serviceAccountCreateSubjectHandler) ReconcilePolicy() SubjectReconcilePolicy {
	return SubjectReconcileNone
}

func (handler *serviceAccountCreateSubjectHandler) Validate(
	_ context.Context,
	instance approval.Instance,
) error {
	if handler == nil || handler.approvals == nil || handler.service == nil || handler.projector == nil {
		return fmt.Errorf("service account Approval subject handler is not configured")
	}
	if instance.SubjectType != SubjectTypeServiceAccountCreate ||
		strings.TrimSpace(instance.SubjectRef) == "" {
		return fmt.Errorf("service account Approval subject is invalid")
	}
	return nil
}

func (handler *serviceAccountCreateSubjectHandler) Decide(
	ctx context.Context,
	command DecisionCommand,
) (DecisionResult, error) {
	settled, wasSettled, err := handler.service.SettlePrivilegedWorkspaceCreateApproval(
		ctx, command.WorkspaceID, command.ApprovalID, command.Now,
	)
	if err != nil {
		return DecisionResult{}, err
	}
	if wasSettled {
		if settled.Approval == nil {
			return DecisionResult{}, fmt.Errorf("settled service account Approval is missing")
		}
		return DecisionResult{
			Instance: *settled.Approval,
			Value:    settled,
			Result: map[string]any{
				"requestId": settled.Request.ID, "requestState": settled.Request.BusinessState,
			},
		}, nil
	}
	decided, err := handler.approvals.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
	}
	consumed, err := handler.service.ConsumeWorkspaceCreateApproval(ctx, serviceaccount.ConsumeApprovalDecision{
		WorkspaceID: command.WorkspaceID, RequestID: decided.Instance.SubjectRef,
		ApprovalID: decided.Instance.ID, DecisionVersion: decided.Instance.Version,
		CorrelationID: command.CorrelationID, Now: command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	if consumed.Account != nil && consumed.Account.AudienceState != serviceaccountstore.AudienceStateReady {
		ready, reconcileErr := handler.service.ReconcileAllWorkspaceHumans(ctx, consumed.Account.WorkspaceID, consumed.Account.ID,
			consumed.Account.Version, consumed.Account.AudiencePolicyVersion, command.Now)
		if reconcileErr != nil {
			return DecisionResult{}, reconcileErr
		}
		consumed.Account = &ready
	}
	return DecisionResult{Instance: decided.Instance, Value: consumed, Replay: decided.AlreadyApplied,
		Result: map[string]any{"requestId": consumed.Request.ID, "requestState": consumed.Request.BusinessState}}, nil
}

type TeamRoleProposalConsumer interface {
	Submit(context.Context, agent.AuthenticatedAgent, roleproposal.SubmitInput) (roleproposal.Result, error)
	Decide(context.Context, approval.DecideParams) (roleproposal.DecisionResult, error)
	Withdraw(context.Context, agent.AuthenticatedAgent, roleproposal.WithdrawInput) (roleproposal.WithdrawResult, error)
	Load(context.Context, string, string) (roleproposal.Proposal, error)
}

type teamRoleProposalSubjectHandler struct {
	approvals *approval.Store
	service   TeamRoleProposalConsumer
	projector *Projector
}

type teamRoleProposalRequest struct {
	Description *string                         `json:"description,omitempty"`
	Members     []teamRoleProposalMemberRequest `json:"members"`
}

type teamRoleProposalMemberRequest struct {
	TeamMemberID      string  `json:"teamMemberId"`
	RoleLabel         *string `json:"roleLabel,omitempty"`
	RoleOverlayPrompt *string `json:"roleOverlayPrompt,omitempty"`
	WakeOnTeamMention *bool   `json:"wakeOnTeamMention,omitempty"`
}

func NewTeamRoleProposalSubjectHandler(
	approvals *approval.Store,
	service TeamRoleProposalConsumer,
	projector *Projector,
) SubjectHandler {
	return &teamRoleProposalSubjectHandler{
		approvals: approvals, service: service, projector: projector,
	}
}

func (*teamRoleProposalSubjectHandler) SubjectType() string {
	return SubjectTypeTeamRoleProposal
}

func (*teamRoleProposalSubjectHandler) RequestType() string {
	return "team_role"
}

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

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

func (handler *teamRoleProposalSubjectHandler) Validate(
	_ context.Context,
	instance approval.Instance,
) error {
	if handler == nil || handler.approvals == nil || handler.service == nil || handler.projector == nil {
		return fmt.Errorf("Team role proposal Approval subject handler is not configured")
	}
	if instance.SubjectType != SubjectTypeTeamRoleProposal ||
		strings.TrimSpace(instance.SubjectRef) == "" ||
		len(instance.Payload) == 0 {
		return fmt.Errorf("Team role proposal Approval subject is invalid")
	}
	var payload teamRoleProposalPayload
	if err := json.Unmarshal(instance.Payload, &payload); err != nil {
		return fmt.Errorf("decode Team role proposal payload: %w", err)
	}
	if payload.SchemaVersion != roleproposal.PayloadSchemaVersion {
		return fmt.Errorf("Team role proposal payload schema version is not supported")
	}
	return nil
}

func (handler *teamRoleProposalSubjectHandler) Submit(ctx context.Context, command SubmitCommand) (SubmitResult, error) {
	payload, ok := command.Payload.(roleproposal.SubmitInput)
	if !ok {
		return SubmitResult{}, storeerr.BadRequest("Team role proposal payload is invalid")
	}
	payload.TeamID = command.SubjectRef
	payload.ClientRequestID = command.ClientRequestID
	payload.CorrelationID = command.CorrelationID
	payload.Now = command.Now
	result, err := handler.service.Submit(ctx, authenticatedAgent(command.Principal), payload)
	if err != nil {
		return SubmitResult{}, err
	}
	return SubmitResult{Instance: result.Approval, Assignments: result.Assignments, Created: result.Created, Value: result}, nil
}

func (*teamRoleProposalSubjectHandler) DecodeSubmitPayload(value any) (any, error) {
	switch value := value.(type) {
	case json.RawMessage:
		if err := requireStrictJSONKeysWithOptional(
			value, []string{"members"}, []string{"description"},
		); err != nil {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		var object map[string]json.RawMessage
		if err := json.Unmarshal(value, &object); err != nil {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		if description, ok := object["description"]; ok && requireJSONString(description) != nil {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		if string(object["members"]) == "null" {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		var members []json.RawMessage
		if err := json.Unmarshal(object["members"], &members); err != nil {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		for _, member := range members {
			if err := requireStrictJSONKeysWithOptional(
				member, []string{"teamMemberId"}, []string{"roleLabel", "roleOverlayPrompt", "wakeOnTeamMention"},
			); err != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			var memberObject map[string]json.RawMessage
			if err := json.Unmarshal(member, &memberObject); err != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			if err := requireJSONString(memberObject["teamMemberId"]); err != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			if roleLabel, ok := memberObject["roleLabel"]; ok && requireJSONString(roleLabel) != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			if overlay, ok := memberObject["roleOverlayPrompt"]; ok && requireJSONString(overlay) != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			if wake, ok := memberObject["wakeOnTeamMention"]; ok && requireJSONBool(wake) != nil {
				return nil, storeerr.BadRequest("Team role proposal payload is invalid")
			}
			if _, hasLabel := memberObject["roleLabel"]; !hasLabel {
				if _, hasOverlay := memberObject["roleOverlayPrompt"]; !hasOverlay {
					if _, hasWake := memberObject["wakeOnTeamMention"]; !hasWake {
						return nil, storeerr.BadRequest("Team role proposal payload is invalid")
					}
				}
			}
		}
		if _, hasDescription := object["description"]; !hasDescription && len(members) == 0 {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		var request teamRoleProposalRequest
		decoder := json.NewDecoder(strings.NewReader(string(value)))
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(&request); err != nil {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		var trailing any
		if err := decoder.Decode(&trailing); err != io.EOF {
			return nil, storeerr.BadRequest("Team role proposal payload is invalid")
		}
		payload := roleproposal.SubmitInput{
			Description: request.Description,
			Members:     make([]roleproposal.MemberInput, 0, len(request.Members)),
		}
		for _, member := range request.Members {
			payload.Members = append(payload.Members, roleproposal.MemberInput{
				TeamMemberID:      member.TeamMemberID,
				RoleLabel:         member.RoleLabel,
				RoleOverlayPrompt: member.RoleOverlayPrompt,
				WakeOnTeamMention: member.WakeOnTeamMention,
			})
		}
		return payload, nil
	case roleproposal.SubmitInput:
		return value, nil
	default:
		return nil, storeerr.BadRequest("Team role proposal payload is invalid")
	}
}

func requireStrictJSONKeysWithOptional(value json.RawMessage, required, optional []string) error {
	var object map[string]json.RawMessage
	if err := json.Unmarshal(value, &object); err != nil || object == nil {
		return fmt.Errorf("JSON object is required")
	}
	allowedKeys := make(map[string]struct{}, len(required)+len(optional))
	for _, key := range required {
		allowedKeys[key] = struct{}{}
	}
	for _, key := range optional {
		allowedKeys[key] = struct{}{}
	}
	for key := range object {
		if _, ok := allowedKeys[key]; !ok {
			return fmt.Errorf("JSON key %q is not allowed", key)
		}
	}
	for _, key := range required {
		if _, ok := object[key]; !ok {
			return fmt.Errorf("JSON key %q is required", key)
		}
	}
	return nil
}

func requireJSONBool(value json.RawMessage) error {
	var decoded bool
	if len(value) == 0 || string(value) == "null" {
		return fmt.Errorf("JSON bool is required")
	}
	if err := json.Unmarshal(value, &decoded); err != nil {
		return fmt.Errorf("JSON bool is required")
	}
	return nil
}

func requireJSONString(value json.RawMessage) error {
	var decoded string
	if len(value) == 0 || string(value) == "null" {
		return fmt.Errorf("JSON string is required")
	}
	if err := json.Unmarshal(value, &decoded); err != nil {
		return fmt.Errorf("JSON string is required")
	}
	return nil
}

func (handler *teamRoleProposalSubjectHandler) Decide(ctx context.Context, command DecisionCommand) (DecisionResult, error) {
	decided, 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
	}
	return DecisionResult{Instance: decided.Approval.Instance, Value: decided,
		Replay: decided.Approval.AlreadyApplied,
		Result: map[string]any{
			"approvalInstanceId": decided.Proposal.ID,
			"proposalStatus":     decided.Proposal.Status,
		}}, nil
}

func (handler *teamRoleProposalSubjectHandler) Cancel(ctx context.Context, command CancelCommand) (DecisionResult, error) {
	result, err := handler.service.Withdraw(ctx, authenticatedAgent(command.Principal), roleproposal.WithdrawInput{
		ApprovalID: command.ApprovalID, ExpectedVersion: command.ExpectedVersion,
		ClientRequestID: command.ClientRequestID, Now: command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	return DecisionResult{Instance: result.Approval.Instance, Value: result,
		Replay: result.AlreadyClosed, Result: map[string]any{
			"approvalInstanceId": result.Proposal.ID,
			"proposalStatus":     result.Proposal.Status,
		}}, nil
}

func authenticatedAgent(principal SubjectPrincipal) agent.AuthenticatedAgent {
	return agent.AuthenticatedAgent{
		AgentID: principal.PrincipalID, WorkspaceID: principal.WorkspaceID,
		MemberID: principal.MemberID, MachineID: principal.MachineID,
	}
}

func (handler *teamRoleProposalSubjectHandler) PresentPending(ctx context.Context, result SubmitResult) (app.AppNotificationResult, error) {
	domain, ok := result.Value.(roleproposal.Result)
	if !ok {
		proposal, err := handler.service.Load(ctx, result.Instance.WorkspaceID, result.Instance.ID)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		domain = roleproposal.Result{Proposal: proposal, Approval: result.Instance, Assignments: result.Assignments}
	}
	return handler.projectPending(ctx, domain)
}

func (handler *teamRoleProposalSubjectHandler) PresentResolved(ctx context.Context, result DecisionResult, presentation PresentationContext) (app.AppNotificationResult, error) {
	proposal, err := handler.service.Load(ctx, result.Instance.WorkspaceID, result.Instance.ID)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if presentation.DeliveryID != "" {
		return handler.resolveCard(ctx, proposal, result.Instance, presentation.NotificationID, presentation.DeliveryID, presentation.AttemptID, presentation.MutationID)
	}
	return handler.projectResolved(ctx, proposal, result.Instance)
}
