package approvalapp

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/kbgovernance"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type publicationMembers interface {
	ResolveMember(context.Context, string, string) (string, error)
	ResolveUser(context.Context, string, string) (string, error)
	ListApprovers(context.Context, string) ([]string, error)
	ListPublicationApprovers(context.Context, string, string) ([]publicationApproverAssignment, error)
}

// PublicationSubjectHandler adapts MeshX's frozen review snapshot to AgentTeam's
// generic Approval authority.
type PublicationSubjectHandler struct {
	approvals *approval.Store
	authority kbgovernance.PublicationAuthority
	members   publicationMembers
	projector *Projector
}

func NewPublicationSubjectHandler(approvals *approval.Store, authority kbgovernance.PublicationAuthority, db coredb.DB, projector *Projector) *PublicationSubjectHandler {
	return &PublicationSubjectHandler{approvals: approvals, authority: authority, members: coreWikiGenerationMembers{db: db}, projector: projector}
}

func (*PublicationSubjectHandler) SubjectType() string               { return kbgovernance.PublicationSubjectType }
func (*PublicationSubjectHandler) RequestType() string               { return kbgovernance.PublicationRequestType }
func (*PublicationSubjectHandler) CancelPolicy() SubjectCancelPolicy { return SubjectCancelByApplicant }
func (handler *PublicationSubjectHandler) ReconcilePolicy() SubjectReconcilePolicy {
	if handler == nil || handler.projector == nil {
		return SubjectReconcileNone
	}
	return SubjectReconcileProjection
}

func (handler *PublicationSubjectHandler) Validate(_ context.Context, instance approval.Instance) error {
	if handler == nil || handler.approvals == nil || handler.authority == nil || handler.members == nil {
		return fmt.Errorf("publication Approval subject handler is not configured")
	}
	if instance.SubjectType != kbgovernance.PublicationSubjectType || instance.SubjectVersion != kbgovernance.PublicationPayloadSchema {
		return fmt.Errorf("publication Approval subject is invalid")
	}
	var payload kbgovernance.PublicationPayload
	if err := json.Unmarshal(instance.Payload, &payload); err != nil || payload.SchemaVersion != kbgovernance.PublicationPayloadSchema {
		return fmt.Errorf("publication Approval payload is invalid")
	}
	if payload.WorkspaceID != instance.WorkspaceID || strings.TrimSpace(instance.SubjectRef) == "" {
		return fmt.Errorf("publication Approval scope is invalid")
	}
	return nil
}

func (*PublicationSubjectHandler) DecodeSubmitPayload(value any) (any, error) {
	prepared, ok := value.(kbgovernance.PreparedPublication)
	if !ok {
		return nil, storeerr.BadRequest("publication Approval payload is invalid")
	}
	return prepared, nil
}

func (handler *PublicationSubjectHandler) Submit(ctx context.Context, command SubmitCommand) (SubmitResult, error) {
	prepared, ok := command.Payload.(kbgovernance.PreparedPublication)
	if !ok {
		return SubmitResult{}, storeerr.BadRequest("publication Approval payload is invalid")
	}
	if err := validatePreparedPublication(prepared); err != nil {
		return SubmitResult{}, err
	}
	payload := prepared.Payload
	if command.Principal.ActorType != "human" || command.Principal.WorkspaceID != payload.WorkspaceID || command.Principal.PrincipalID != payload.RequesterID || command.Principal.MemberID == "" {
		return SubmitResult{}, storeerr.Forbidden("publication applicant does not match the authenticated Human")
	}
	key := wikiApprovalHash("publication", payload.WorkspaceID, command.Principal.MemberID, payload.IdempotencyKey)
	approvalID, requestID := "appr_"+key[:40], "kbpub_"+key[:40]
	approvers, err := handler.replayApprovers(ctx, payload.WorkspaceID, command.Principal.MemberID, key)
	if err != nil {
		return SubmitResult{}, err
	}
	instance, assignments, created, err := handler.approvals.Create(ctx, approval.CreateParams{
		ID: approvalID, WorkspaceID: payload.WorkspaceID, SubjectType: kbgovernance.PublicationSubjectType,
		SubjectRef: requestID, SubjectVersion: kbgovernance.PublicationPayloadSchema,
		Payload: prepared.PayloadJSON, PayloadDigest: prepared.PayloadDigest,
		IdempotencyKey: key, PendingKey: wikiApprovalHash("publication-kb", payload.WorkspaceID, payload.KnowledgeBaseID),
		ApplicantMemberID: command.Principal.MemberID, ApproverMemberIDs: approvers,
		CorrelationID: prepared.CorrelationID, Now: prepared.PreparedAt,
	})
	if err != nil {
		return SubmitResult{}, err
	}
	state, err := handler.authority.RecordPublicationApproval(ctx, kbgovernance.RecordPublicationApprovalCommand{
		RequestID: requestID, ApprovalID: instance.ID, ApprovalVersion: instance.Version,
		Prepared: prepared, Now: command.Now,
	})
	if err != nil {
		return SubmitResult{}, err
	}
	return SubmitResult{Instance: instance, Assignments: assignments, Created: created, Value: state}, nil
}

func validatePreparedPublication(prepared kbgovernance.PreparedPublication) error {
	payload := prepared.Payload
	if payload.SchemaVersion != kbgovernance.PublicationPayloadSchema || payload.WorkspaceID == "" || payload.KnowledgeBaseID == "" || payload.WorkingRevision == "" || payload.RequesterID == "" || payload.IdempotencyKey == "" || len(payload.ContentDigest) != 64 || len(prepared.SnapshotJSON) == 0 {
		return storeerr.BadRequest("publication Approval payload is incomplete")
	}
	canonical, payloadDigest, err := kbgovernance.EncodePayload(payload)
	if err != nil {
		return fmt.Errorf("encode publication Approval payload: %w", err)
	}
	contentDigest := sha256.Sum256(prepared.SnapshotJSON)
	if !bytes.Equal(canonical, prepared.PayloadJSON) || payloadDigest != prepared.PayloadDigest || hex.EncodeToString(contentDigest[:]) != payload.ContentDigest {
		return storeerr.BadRequest("publication Approval payload digest does not match its canonical snapshot")
	}
	return nil
}

func (handler *PublicationSubjectHandler) replayApprovers(ctx context.Context, workspaceID, memberID, key string) ([]string, error) {
	existing, err := handler.approvals.LoadByIdempotencyKey(ctx, workspaceID, memberID, key)
	if err == nil {
		assignments, err := handler.approvals.ListAssignments(ctx, workspaceID, existing.ID)
		if err != nil {
			return nil, err
		}
		approvers := make([]string, 0, len(assignments))
		for _, assignment := range assignments {
			approvers = append(approvers, assignment.ApproverMemberID)
		}
		return approvers, nil
	}
	if !errors.Is(err, storeerr.ErrNotFound) {
		return nil, err
	}
	return handler.members.ListApprovers(ctx, workspaceID)
}

func (handler *PublicationSubjectHandler) Decide(ctx context.Context, command DecisionCommand) (DecisionResult, error) {
	if command.Decision == approval.DecisionRejected && strings.TrimSpace(command.Reason) == "" {
		return DecisionResult{}, storeerr.BadRequest("publication rejection reason is required")
	}
	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
	}
	actorUserID, err := handler.members.ResolveUser(ctx, command.WorkspaceID, command.ActorMemberID)
	if err != nil {
		return DecisionResult{}, err
	}
	state, err := handler.authority.ResolvePublicationApproval(ctx, kbgovernance.ResolvePublicationApprovalCommand{
		WorkspaceID: command.WorkspaceID, ApprovalID: command.ApprovalID,
		ApprovalVersion: decided.Receipt.ResultVersion, ActorPrincipalID: actorUserID,
		ClientDecisionID: command.ClientDecisionID, Decision: command.Decision,
		Reason: command.Reason, Now: command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	if state.Stale {
		decided, err = handler.approvals.ReconcileApprovedDecisionAsStale(ctx, approval.ApprovedStaleParams{
			WorkspaceID: command.WorkspaceID, ApprovalID: command.ApprovalID,
			SubjectType: kbgovernance.PublicationSubjectType, ActorMemberID: command.ActorMemberID,
			ClientDecisionID: command.ClientDecisionID, ApprovedVersion: decided.Receipt.ResultVersion,
			Reason: "stale_subject", Now: command.Now,
		})
		if err != nil {
			return DecisionResult{}, err
		}
	}
	return DecisionResult{Instance: decided.Instance, Replay: decided.AlreadyApplied, Value: state}, nil
}

func (handler *PublicationSubjectHandler) Cancel(ctx context.Context, command CancelCommand) (DecisionResult, error) {
	cancelled, err := handler.approvals.CancelByApplicant(ctx, approval.ApplicantCancelParams{
		WorkspaceID: command.Principal.WorkspaceID, ApprovalID: command.ApprovalID,
		ActorMemberID: command.Principal.MemberID, ExpectedVersion: command.ExpectedVersion,
		ClientCancellationID: command.ClientRequestID, Reason: "withdrawn_by_applicant", Now: command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	state, err := handler.authority.ResolvePublicationApproval(ctx, kbgovernance.ResolvePublicationApprovalCommand{
		WorkspaceID: command.Principal.WorkspaceID, ApprovalID: command.ApprovalID,
		ApprovalVersion: cancelled.Instance.Version, ActorPrincipalID: command.Principal.PrincipalID,
		ClientDecisionID: command.ClientRequestID, Decision: kbgovernance.DecisionWithdrawn, Now: command.Now,
	})
	if err != nil {
		return DecisionResult{}, err
	}
	return DecisionResult{Instance: cancelled.Instance, Replay: cancelled.AlreadyApplied, Value: state}, nil
}

type PublicationApprovalGateway struct {
	registry             *SubjectRegistry
	approvals            *approval.Store
	members              publicationMembers
	projectNotifications bool
}

func NewPublicationApprovalGateway(registry *SubjectRegistry, approvals *approval.Store, db coredb.DB, projector *Projector) *PublicationApprovalGateway {
	return &PublicationApprovalGateway{registry: registry, approvals: approvals, members: coreWikiGenerationMembers{db: db}, projectNotifications: projector != nil}
}

func (gateway *PublicationApprovalGateway) SubmitPublication(ctx context.Context, command kbgovernance.SubmitPublicationCommand) (kbgovernance.PublicationState, error) {
	payload := command.Prepared.Payload
	memberID, err := gateway.members.ResolveMember(ctx, payload.WorkspaceID, command.ActorPrincipalID)
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	result, err := gateway.registry.SubmitRequest(ctx, kbgovernance.PublicationRequestType, SubmitCommand{
		Principal:       SubjectPrincipal{ActorType: "human", WorkspaceID: payload.WorkspaceID, PrincipalID: command.ActorPrincipalID, MemberID: memberID},
		ClientRequestID: payload.IdempotencyKey, Payload: command.Prepared,
		CorrelationID: command.Prepared.CorrelationID, Now: command.Prepared.PreparedAt,
	})
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	if gateway.projectNotifications && result.Instance.State == approval.StatePending {
		if _, err := gateway.registry.PresentPending(ctx, result); err != nil {
			return kbgovernance.PublicationState{}, publicationGatewayError(err)
		}
	}
	return publicationValue(result.Value)
}

func (gateway *PublicationApprovalGateway) DecidePublication(ctx context.Context, command kbgovernance.DecidePublicationCommand) (kbgovernance.PublicationState, error) {
	memberID, err := gateway.members.ResolveMember(ctx, command.WorkspaceID, command.ActorPrincipalID)
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	assignmentID, err := publicationAssignmentForMember(ctx, gateway.approvals, command.WorkspaceID, command.ApprovalID, memberID)
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	result, err := gateway.registry.Decide(ctx, DecisionCommand{
		WorkspaceID: command.WorkspaceID, ApprovalID: command.ApprovalID,
		AssignmentID: assignmentID, ActorMemberID: memberID, ExpectedVersion: command.ExpectedVersion,
		ClientDecisionID: command.ClientDecisionID, Decision: command.Decision,
		Reason: command.Reason, CorrelationID: command.CorrelationID, Now: command.Now,
	})
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	if gateway.projectNotifications {
		if _, err := gateway.registry.PresentResolved(ctx, result, PresentationContext{}); err != nil {
			return kbgovernance.PublicationState{}, publicationGatewayError(err)
		}
	}
	return publicationValue(result.Value)
}

func (gateway *PublicationApprovalGateway) WithdrawPublication(ctx context.Context, command kbgovernance.WithdrawPublicationCommand) (kbgovernance.PublicationState, error) {
	memberID, err := gateway.members.ResolveMember(ctx, command.WorkspaceID, command.ActorPrincipalID)
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	result, err := gateway.registry.Cancel(ctx, CancelCommand{
		Principal:  SubjectPrincipal{ActorType: "human", WorkspaceID: command.WorkspaceID, PrincipalID: command.ActorPrincipalID, MemberID: memberID},
		ApprovalID: command.ApprovalID, ExpectedVersion: command.ExpectedVersion,
		ClientRequestID: command.ClientRequestID, Now: command.Now,
	})
	if err != nil {
		return kbgovernance.PublicationState{}, publicationGatewayError(err)
	}
	if gateway.projectNotifications {
		if _, err := gateway.registry.PresentResolved(ctx, result, PresentationContext{}); err != nil {
			return kbgovernance.PublicationState{}, publicationGatewayError(err)
		}
	}
	return publicationValue(result.Value)
}

func (gateway *PublicationApprovalGateway) InspectPublication(ctx context.Context, command kbgovernance.InspectPublicationApprovalCommand) (kbgovernance.PublicationApprovalReview, error) {
	if gateway == nil || gateway.approvals == nil || gateway.members == nil {
		return kbgovernance.PublicationApprovalReview{}, fmt.Errorf("publication Approval gateway is not configured")
	}
	instance, err := gateway.approvals.Load(ctx, strings.TrimSpace(command.WorkspaceID), strings.TrimSpace(command.ApprovalID))
	if err != nil {
		return kbgovernance.PublicationApprovalReview{}, publicationGatewayError(err)
	}
	if instance.SubjectType != kbgovernance.PublicationSubjectType {
		return kbgovernance.PublicationApprovalReview{}, kbgovernance.GatewayError{
			Kind: kbgovernance.ErrorNotFound, Code: "kb-publication-approval-not-found",
			Message: "publication Approval was not found",
		}
	}
	assignments, err := gateway.members.ListPublicationApprovers(ctx, instance.WorkspaceID, instance.ID)
	if err != nil {
		return kbgovernance.PublicationApprovalReview{}, publicationGatewayError(err)
	}
	review := kbgovernance.PublicationApprovalReview{
		Approvers: make([]kbgovernance.PublicationApprover, 0, len(assignments)),
	}
	actorID := strings.TrimSpace(command.ActorPrincipalID)
	for _, assignment := range assignments {
		principalID := strings.TrimSpace(assignment.PrincipalID)
		review.Approvers = append(review.Approvers, kbgovernance.PublicationApprover{
			PrincipalID: principalID, DisplayName: strings.TrimSpace(assignment.DisplayName),
		})
		if principalID == actorID {
			review.ActorAssigned = true
			review.CanDecide = assignment.MemberStatus == "active" && assignment.AssignmentStatus == approval.AssignmentPending
		}
	}
	return review, nil
}

func publicationAssignmentForMember(ctx context.Context, approvals *approval.Store, workspaceID, approvalID, memberID string) (string, error) {
	assignments, err := approvals.ListAssignments(ctx, workspaceID, approvalID)
	if err != nil {
		return "", err
	}
	for _, assignment := range assignments {
		if assignment.ApproverMemberID == memberID {
			return assignment.ID, nil
		}
	}
	return "", storeerr.Forbidden("publication Approval is not assigned to the current Human")
}

func publicationValue(value any) (kbgovernance.PublicationState, error) {
	state, ok := value.(kbgovernance.PublicationState)
	if !ok {
		return kbgovernance.PublicationState{}, fmt.Errorf("publication Approval returned an invalid result")
	}
	return state, nil
}

func publicationGatewayError(err error) error {
	mapped := wikiGatewayError(err)
	var gatewayErr kbgovernance.GatewayError
	if errors.As(mapped, &gatewayErr) {
		gatewayErr.Code = "kb-publication-approval-" + strings.ReplaceAll(gatewayErr.Kind, "_", "-")
		return gatewayErr
	}
	return mapped
}

var (
	_ SubjectHandler                          = (*PublicationSubjectHandler)(nil)
	_ SubjectSubmitter                        = (*PublicationSubjectHandler)(nil)
	_ SubjectCanceller                        = (*PublicationSubjectHandler)(nil)
	_ kbgovernance.PublicationApprovalGateway = (*PublicationApprovalGateway)(nil)
)
