package execution

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

	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	executionstore "github.com/tidbcloud/AgentTeam/internal/store/role/execution"
)

const (
	executionSettleTimeout = 30 * time.Second

	failureInvalidRequestMessage    = "The approved role request is invalid."
	failureAuthorityDeniedMessage   = "The role operation is no longer authorized."
	failureNotFoundMessage          = "The requested role resource was not found."
	failureVersionConflictMessage   = "The role changed before this request was applied."
	failureOperationConflictMessage = "A conflicting role operation already exists."
)

// Execute revalidates current authority, calls MeshX with Approval ID and
// Approval.PayloadDigest, and fences every settle/release by the live token.
func (service *Service) Execute(
	ctx context.Context,
	claim executionstore.ClaimedExecution,
	claimNow time.Time,
) error {
	if !validExecutionClaim(claim, claimNow) {
		return ErrInvariant
	}
	if service == nil || service.dependencies.LoadApproval == nil {
		return ErrInvariant
	}
	instance, err := service.dependencies.LoadApproval(
		ctx, claim.Execution.WorkspaceID, claim.Execution.ApprovalID,
	)
	if err != nil {
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	payload, err := validateExecutionApproval(instance, claim)
	if err != nil {
		return service.failExecution(
			ctx, claim.Fence, claimNow, deterministicFailure(ApplyInvalidRequest),
		)
	}
	if service.dependencies.LoadProjectedTerminal == nil {
		return ErrInvariant
	}
	projected, resolved, err := service.dependencies.LoadProjectedTerminal(
		ctx, instance, payload,
	)
	if err != nil {
		return err
	}
	if resolved {
		return service.settleProjectedTerminal(
			ctx, claim.Fence, claimNow, instance, payload, projected,
		)
	}
	if projected != (RequestResult{}) {
		return ErrInvariant
	}
	if handled, err := service.recoverApplied(
		ctx, claim.Fence, claimNow, instance, payload,
	); handled {
		return err
	}
	if service.dependencies.RecheckExecutionAuthority == nil {
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	applyTargetAgentID := ""
	if payload.Apply != nil {
		applyTargetAgentID = payload.Apply.AgentID
	}
	authority, err := service.dependencies.RecheckExecutionAuthority(
		ctx,
		ExecutionAuthorityCommand{
			Approval:                instance,
			Payload:                 payload,
			RequiredManagerMemberID: instance.DecidedByMemberID,
			ApplyTargetAgentID:      applyTargetAgentID,
		},
	)
	if err != nil {
		if outcome, ok := deterministicApplyOutcome(err); ok && outcome == ApplyAuthorityDenied {
			return service.projectAndFailExecution(
				ctx, instance, payload, claim.Fence, claimNow,
				deterministicFailure(outcome),
			)
		}
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	if !validExecutionAuthority(authority) {
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	if applyTargetAgentID != "" && !containsString(authority.OwnedAgentIDs, applyTargetAgentID) {
		return service.projectAndFailExecution(
			ctx, instance, payload, claim.Fence, claimNow,
			deterministicFailure(ApplyAuthorityDenied),
		)
	}
	if service.dependencies.Apply == nil {
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	success, err := service.dependencies.Apply(ctx, ApplyCommand{
		Approval:      instance,
		Payload:       payload,
		OperationKey:  instance.ID,
		RequestDigest: instance.PayloadDigest,
		Authority:     authority,
	})
	if err != nil {
		if outcome, ok := deterministicApplyOutcome(err); ok {
			return service.projectAndFailExecution(
				ctx, instance, payload, claim.Fence, claimNow,
				deterministicFailure(outcome),
			)
		}
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	if !validExecutionSuccess(success) {
		return service.releaseExecution(ctx, claim.Fence, claimNow)
	}
	result := baseRequestResult(instance, payload)
	result.Status = RequestSucceeded
	result.RoleID = success.RoleID
	result.RoleVersionID = success.RoleVersionID
	if err := service.projectExecutionTerminal(ctx, instance, result); err != nil {
		return err
	}
	return service.succeedExecution(ctx, claim.Fence, claimNow, success)
}

func (service *Service) recoverApplied(
	ctx context.Context,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
	instance approval.Instance,
	payload ApprovalPayload,
) (bool, error) {
	if service.dependencies.RecoverApplied == nil {
		return true, service.releaseExecution(ctx, fence, claimNow)
	}
	recovered, found, err := service.dependencies.RecoverApplied(ctx, RecoverAppliedCommand{
		Approval:      instance,
		Payload:       payload,
		OperationKey:  instance.ID,
		RequestDigest: instance.PayloadDigest,
	})
	if err != nil {
		if outcome, ok := deterministicApplyOutcome(err); ok && outcome == ApplyOperationConflict {
			return true, service.projectAndFailExecution(
				ctx, instance, payload, fence, claimNow,
				deterministicFailure(outcome),
			)
		}
		return true, service.releaseExecution(ctx, fence, claimNow)
	}
	if !found {
		return false, nil
	}
	if !validExecutionSuccess(recovered) {
		return true, service.releaseExecution(ctx, fence, claimNow)
	}
	result := baseRequestResult(instance, payload)
	result.Status = RequestSucceeded
	result.RoleID = recovered.RoleID
	result.RoleVersionID = recovered.RoleVersionID
	if err := service.projectExecutionTerminal(ctx, instance, result); err != nil {
		return true, err
	}
	return true, service.succeedExecution(ctx, fence, claimNow, recovered)
}

func (service *Service) settleProjectedTerminal(
	ctx context.Context,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
	instance approval.Instance,
	payload ApprovalPayload,
	result RequestResult,
) error {
	expected := baseRequestResult(instance, payload)
	switch result.Status {
	case RequestSucceeded:
		expected.Status = RequestSucceeded
		expected.RoleID = result.RoleID
		expected.RoleVersionID = result.RoleVersionID
		if result != expected ||
			!validExecutionSuccess(executionstore.Success{
				RoleID: result.RoleID, RoleVersionID: result.RoleVersionID,
			}) {
			return ErrInvariant
		}
		return service.succeedExecution(ctx, fence, claimNow, executionstore.Success{
			RoleID: result.RoleID, RoleVersionID: result.RoleVersionID,
		})
	case RequestFailed:
		failure := deterministicFailure(ApplyOutcome(result.FailureCode))
		expected.Status = RequestFailed
		expected.FailureCode = failure.Code
		expected.FailureMessage = failure.Message
		if failure.Code == "" || result != expected {
			return ErrInvariant
		}
		return service.failExecution(ctx, fence, claimNow, failure)
	default:
		return ErrInvariant
	}
}

func (service *Service) projectAndFailExecution(
	ctx context.Context,
	instance approval.Instance,
	payload ApprovalPayload,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
	failure executionstore.Failure,
) error {
	result := baseRequestResult(instance, payload)
	result.Status = RequestFailed
	result.FailureCode = failure.Code
	result.FailureMessage = failure.Message
	if err := service.projectExecutionTerminal(ctx, instance, result); err != nil {
		return err
	}
	return service.failExecution(ctx, fence, claimNow, failure)
}

func (service *Service) projectExecutionTerminal(
	ctx context.Context,
	instance approval.Instance,
	result RequestResult,
) error {
	if service == nil || service.dependencies.ProjectTerminal == nil {
		return ErrInvariant
	}
	return service.dependencies.ProjectTerminal(ctx, instance, result)
}

func validExecutionClaim(claim executionstore.ClaimedExecution, claimNow time.Time) bool {
	execution, fence := claim.Execution, claim.Fence
	return !claimNow.IsZero() &&
		isTrimmedString(execution.WorkspaceID) &&
		isTrimmedString(execution.ApprovalID) &&
		isTrimmedString(execution.LeaseToken) &&
		isTrimmedString(fence.WorkspaceID) &&
		isTrimmedString(fence.ApprovalID) &&
		isTrimmedString(fence.LeaseToken) &&
		execution.WorkspaceID == fence.WorkspaceID &&
		execution.ApprovalID == fence.ApprovalID &&
		execution.LeaseToken == fence.LeaseToken &&
		execution.Status == executionstore.ExecutionRunning &&
		execution.LeaseExpiresAt != nil &&
		execution.LeaseExpiresAt.After(claimNow)
}

func validateExecutionApproval(
	instance approval.Instance,
	claim executionstore.ClaimedExecution,
) (ApprovalPayload, error) {
	if !isTrimmedString(instance.ID) ||
		!isTrimmedString(instance.WorkspaceID) ||
		!isTrimmedString(instance.ApplicantMemberID) ||
		!isTrimmedString(instance.DecidedByMemberID) ||
		instance.ID != claim.Execution.ApprovalID ||
		instance.WorkspaceID != claim.Execution.WorkspaceID ||
		instance.Version <= 1 ||
		instance.State != approval.StateApproved ||
		instance.Decision != approval.DecisionApproved {
		return ApprovalPayload{}, ErrInvariant
	}
	payload, err := decodeApprovalPayload(instance)
	if err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	idempotencyKey, err := RoleIdempotencyKey(
		instance.WorkspaceID, payload.ApplicantAgentID, payload.RequestID,
	)
	if err != nil ||
		instance.SubjectRef != idempotencyKey ||
		instance.IdempotencyKey != idempotencyKey {
		return ApprovalPayload{}, ErrInvariant
	}
	return payload, nil
}

func validExecutionAuthority(authority ExecutionAuthority) bool {
	if !isTrimmedString(authority.ManagerUserID) {
		return false
	}
	for index, agentID := range authority.OwnedAgentIDs {
		if !isTrimmedString(agentID) {
			return false
		}
		for prior := 0; prior < index; prior++ {
			if authority.OwnedAgentIDs[prior] == agentID {
				return false
			}
		}
	}
	return true
}

func validExecutionSuccess(success executionstore.Success) bool {
	return isTrimmedString(success.RoleID) &&
		isTrimmedString(success.RoleVersionID)
}

func isTrimmedString(value string) bool {
	return value != "" && value == strings.TrimSpace(value)
}

func containsString(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func deterministicApplyOutcome(err error) (ApplyOutcome, bool) {
	var applyErr *ApplyError
	if !errors.As(err, &applyErr) || applyErr == nil {
		return "", false
	}
	switch applyErr.Outcome {
	case ApplyInvalidRequest, ApplyAuthorityDenied, ApplyNotFound,
		ApplyVersionConflict, ApplyOperationConflict:
		return applyErr.Outcome, true
	default:
		return "", false
	}
}

func deterministicFailure(outcome ApplyOutcome) executionstore.Failure {
	switch outcome {
	case ApplyInvalidRequest:
		return executionstore.Failure{
			Code: string(outcome), Message: failureInvalidRequestMessage,
		}
	case ApplyAuthorityDenied:
		return executionstore.Failure{
			Code: string(outcome), Message: failureAuthorityDeniedMessage,
		}
	case ApplyNotFound:
		return executionstore.Failure{
			Code: string(outcome), Message: failureNotFoundMessage,
		}
	case ApplyVersionConflict:
		return executionstore.Failure{
			Code: string(outcome), Message: failureVersionConflictMessage,
		}
	case ApplyOperationConflict:
		return executionstore.Failure{
			Code: string(outcome), Message: failureOperationConflictMessage,
		}
	default:
		return executionstore.Failure{}
	}
}

func (service *Service) succeedExecution(
	ctx context.Context,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
	success executionstore.Success,
) error {
	if service == nil || service.dependencies.Succeed == nil {
		return ErrInvariant
	}
	return service.settleExecution(
		ctx, claimNow,
		func(settleCtx context.Context, settleNow time.Time) error {
			return service.dependencies.Succeed(settleCtx, fence, success, settleNow)
		},
	)
}

func (service *Service) failExecution(
	ctx context.Context,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
	failure executionstore.Failure,
) error {
	if service == nil || service.dependencies.FailDeterministic == nil {
		return ErrInvariant
	}
	return service.settleExecution(
		ctx, claimNow,
		func(settleCtx context.Context, settleNow time.Time) error {
			return service.dependencies.FailDeterministic(
				settleCtx, fence, failure, settleNow,
			)
		},
	)
}

func (service *Service) releaseExecution(
	ctx context.Context,
	fence executionstore.ExecutionFence,
	claimNow time.Time,
) error {
	if service == nil || service.dependencies.ReleaseUncertain == nil {
		return ErrInvariant
	}
	return service.settleExecution(
		ctx, claimNow,
		func(settleCtx context.Context, settleNow time.Time) error {
			return service.dependencies.ReleaseUncertain(
				settleCtx, fence, settleNow,
			)
		},
	)
}

func (service *Service) settleExecution(
	ctx context.Context,
	claimNow time.Time,
	settle func(context.Context, time.Time) error,
) error {
	if service == nil || service.dependencies.Now == nil || settle == nil {
		return ErrInvariant
	}
	settleNow := service.dependencies.Now()
	if settleNow.IsZero() || settleNow.Before(claimNow) {
		return ErrInvariant
	}
	settleCtx, cancel := context.WithTimeout(
		context.WithoutCancel(ctx), executionSettleTimeout,
	)
	defer cancel()
	return settle(settleCtx, settleNow)
}

func decodeApprovalPayload(instance approval.Instance) (ApprovalPayload, error) {
	if instance.SubjectType != SubjectType || instance.SubjectVersion != 1 ||
		len(instance.Payload) == 0 ||
		len(instance.PayloadDigest) != sha256.Size*2 {
		return ApprovalPayload{}, ErrInvariant
	}
	deterministicPayload, err := deterministicJSONBytes(instance.Payload)
	if err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	deterministicSum := sha256.Sum256(deterministicPayload)
	if instance.PayloadDigest != hex.EncodeToString(deterministicSum[:]) {
		return ApprovalPayload{}, ErrInvariant
	}
	if err := rejectDuplicateJSONFields(instance.Payload); err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	var payload ApprovalPayload
	decoder := json.NewDecoder(bytes.NewReader(instance.Payload))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&payload); err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	if err := requireJSONEOF(decoder); err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	if payload.SchemaVersion != PayloadSchemaVersion ||
		!isTrimmedString(payload.RequestID) ||
		!isTrimmedString(payload.ApplicantAgentID) {
		return ApprovalPayload{}, ErrInvariant
	}
	var proposal RoleProposal
	branchCount := 0
	if payload.Create != nil {
		proposal = *payload.Create
		branchCount++
	}
	if payload.Update != nil {
		proposal = *payload.Update
		branchCount++
	}
	if payload.Delete != nil {
		proposal = *payload.Delete
		branchCount++
	}
	if payload.Apply != nil {
		proposal = *payload.Apply
		branchCount++
	}
	if branchCount != 1 {
		return ApprovalPayload{}, ErrInvariant
	}
	normalized, err := NormalizeRoleRequest(RoleRequest{
		WorkspaceID:      "durable",
		ApplicantAgentID: payload.ApplicantAgentID,
		RequestID:        payload.RequestID,
		Proposal:         proposal,
	})
	if err != nil || normalized.Proposal.Action() != payload.Action {
		return ApprovalPayload{}, ErrInvariant
	}
	expected := ApprovalPayload{
		SchemaVersion:    PayloadSchemaVersion,
		RequestID:        normalized.RequestID,
		ApplicantAgentID: normalized.ApplicantAgentID,
		Action:           normalized.Proposal.Action(),
	}
	switch proposal := normalized.Proposal.(type) {
	case RoleCreateProposal:
		expected.Create = &proposal
	case RoleUpdateProposal:
		expected.Update = &proposal
	case RoleDeleteProposal:
		expected.Delete = &proposal
	case RoleApplyProposal:
		expected.Apply = &proposal
	default:
		return ApprovalPayload{}, ErrInvariant
	}
	expectedRaw, err := json.Marshal(expected)
	if err != nil {
		return ApprovalPayload{}, ErrInvariant
	}
	expectedDeterministic, err := deterministicJSONBytes(expectedRaw)
	if err != nil || !bytes.Equal(deterministicPayload, expectedDeterministic) {
		return ApprovalPayload{}, ErrInvariant
	}
	return expected, nil
}

func requireJSONEOF(decoder *json.Decoder) error {
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

func rejectDuplicateJSONFields(raw []byte) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	first, err := decoder.Token()
	if err != nil {
		return err
	}
	if err := walkJSONFields(decoder, first); err != nil {
		return err
	}
	if _, err := decoder.Token(); err != io.EOF {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

func walkJSONFields(decoder *json.Decoder, token json.Token) error {
	delimiter, composite := token.(json.Delim)
	if !composite {
		return nil
	}
	switch delimiter {
	case '{':
		keys := make([]string, 0)
		for decoder.More() {
			keyToken, err := decoder.Token()
			if err != nil {
				return err
			}
			key, ok := keyToken.(string)
			if !ok {
				return errors.New("JSON object key is not a string")
			}
			for _, existing := range keys {
				if strings.EqualFold(existing, key) {
					return fmt.Errorf("duplicate JSON field %q", key)
				}
			}
			keys = append(keys, key)
			value, err := decoder.Token()
			if err != nil {
				return err
			}
			if err := walkJSONFields(decoder, value); err != nil {
				return err
			}
		}
		closing, err := decoder.Token()
		if err != nil {
			return err
		}
		if closing != json.Delim('}') {
			return errors.New("JSON object is not closed")
		}
	case '[':
		for decoder.More() {
			value, err := decoder.Token()
			if err != nil {
				return err
			}
			if err := walkJSONFields(decoder, value); err != nil {
				return err
			}
		}
		closing, err := decoder.Token()
		if err != nil {
			return err
		}
		if closing != json.Delim(']') {
			return errors.New("JSON array is not closed")
		}
	default:
		return errors.New("invalid JSON delimiter")
	}
	return nil
}
