package orchestrator

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	workflowpayload "github.com/tidbcloud/AgentTeam/internal/workflow/payload"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
	"github.com/tidbcloud/AgentTeam/internal/workflow/run"
)

func callbackCompletionWorkItems(intent run.Intent, supplied json.RawMessage) ([]run.WorkItem, error) {
	payload := bytes.TrimSpace(supplied)
	if intent.WorkItemsAllowedResponders == nil {
		if len(payload) != 0 {
			cause := errors.New("callback does not accept workItems")
			return nil, callbackValidationError(
				CallbackValidationCodeWorkItems,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkItems, "callback.workItems", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		return nil, nil
	}
	if len(payload) == 0 {
		cause := errors.New("workItems is required")
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItems,
			[]publicerr.Diagnostic{callbackDiagnostic(
				CallbackValidationCodeWorkItems, "callback.workItems", "", nil, cause.Error(),
			)},
			cause,
		)
	}
	if intent.WorkItemsDependencyValueContract == nil {
		cause := errors.New("callback dependency value contract is missing")
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItemReference,
			[]publicerr.Diagnostic{callbackDiagnostic(
				CallbackValidationCodeWorkItemReference, "callback.workItems", "", nil, cause.Error(),
			)},
			cause,
		)
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		cause := fmt.Errorf("decode workItems: %w", err)
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItems,
			callbackDiagnosticsFromCause(cause),
			cause,
		)
	}
	if err := requireJSONEOF(decoder); err != nil {
		cause := fmt.Errorf("decode workItems: %w", err)
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItems,
			callbackDiagnosticsFromCause(cause),
			cause,
		)
	}
	workItems, err := run.NormalizeWorkItems(
		"callback.workItems",
		value,
		intent.WorkItemsAllowedResponders,
	)
	if err != nil {
		cause := errors.Join(err)
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItems,
			callbackDiagnosticsFromCause(cause),
			cause,
		)
	}
	if err := run.ValidateWorkItemInstructionDependencyContract(
		"callback.workItems",
		workItems,
		*intent.WorkItemsDependencyValueContract,
	); err != nil {
		cause := errors.Join(err)
		return nil, callbackValidationError(
			CallbackValidationCodeWorkItemReference,
			callbackDiagnosticsFromCause(cause),
			cause,
		)
	}
	// Preserve a present, intentionally empty batch. Nil means the callback did
	// not declare workItems at all and is handled above.
	if workItems == nil {
		workItems = []run.WorkItem{}
	}
	return workItems, nil
}

func decodeCallbackValues(payload []byte) (run.Values, error) {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var values run.Values
	if err := decoder.Decode(&values); err != nil {
		return nil, err
	}
	if err := requireJSONEOF(decoder); err != nil {
		return nil, err
	}
	if values == nil {
		return nil, fmt.Errorf("callback output must be an object")
	}
	return values, nil
}

func callbackCompletionWorkScope(intent run.Intent, supplied *domain.MessageScopeValue) (*domain.MessageScopeValue, error) {
	if intent.WorkScopeMode == "" {
		if supplied != nil {
			cause := errors.New("legacy callback does not accept workScope")
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		return nil, nil
	}
	var normalized *domain.MessageScopeValue
	if supplied != nil {
		scope, err := domain.NormalizeMessageScopeValue(*supplied)
		if err != nil {
			cause := fmt.Errorf("workScope is invalid: %w", err)
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		normalized = &scope
	}
	switch intent.WorkScopeMode {
	case run.WorkScopeModeAgent:
		if normalized == nil {
			cause := errors.New("workScope is required")
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		return normalized, nil
	case run.WorkScopeModeFixed:
		if intent.WorkScope == nil {
			cause := errors.New("fixed workScope is missing")
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		fixed, err := domain.NormalizeMessageScopeValue(*intent.WorkScope)
		if err != nil {
			cause := fmt.Errorf("fixed workScope is invalid: %w", err)
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		if normalized != nil && *normalized != fixed {
			cause := errors.New("workScope does not match the fixed scope")
			return nil, callbackValidationError(
				CallbackValidationCodeWorkScope,
				[]publicerr.Diagnostic{callbackDiagnostic(
					CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
				)},
				cause,
			)
		}
		return &fixed, nil
	default:
		cause := fmt.Errorf("workScope mode is invalid")
		return nil, callbackValidationError(
			CallbackValidationCodeWorkScope,
			[]publicerr.Diagnostic{callbackDiagnostic(
				CallbackValidationCodeWorkScope, "callback.workScope", "", nil, cause.Error(),
			)},
			cause,
		)
	}
}

func cloneCallbackValues(values run.Values) run.Values {
	cloned := make(run.Values, len(values)+1)
	for name, value := range values {
		cloned[name] = value
	}
	return cloned
}

func callbackScopeMap(scope domain.MessageScopeValue) map[string]any {
	return map[string]any{
		"kind":    string(scope.Kind),
		"scopeId": scope.ScopeID,
	}
}

func callbackCompletionMatches(actual, expected EffectResult) bool {
	actual, expected = actual.Normalize(), expected.Normalize()
	// Normalize both sides through the single shared diagnostic projection so a
	// fresh terminal result (whose diagnostics may still be unbounded) compares
	// deterministically against the persisted receipt that was already bounded at
	// write time.
	actual.FailureDiagnostics, actual.FailureTruncation = boundEffectResultFailureDiagnostics(
		actual.FailureDiagnostics,
		actual.FailureTruncation,
	)
	expected.FailureDiagnostics, expected.FailureTruncation = boundEffectResultFailureDiagnostics(
		expected.FailureDiagnostics,
		expected.FailureTruncation,
	)
	if actual.Port != expected.Port || actual.IdempotencyKey != expected.IdempotencyKey ||
		actual.CallbackResponderAgentID != expected.CallbackResponderAgentID ||
		actual.FailureCode != expected.FailureCode || actual.FailureMessage != expected.FailureMessage ||
		actual.FailureStage != expected.FailureStage || actual.FailurePath != expected.FailurePath ||
		actual.FailureProblem != expected.FailureProblem ||
		actual.FailureHint != expected.FailureHint || actual.FailureRetryable != expected.FailureRetryable ||
		!samePersistedDiagnostics(actual.FailureDiagnostics, expected.FailureDiagnostics) ||
		!samePersistedWorkItem(actual.FailureWorkItem, expected.FailureWorkItem) ||
		!samePersistedTruncation(actual.FailureTruncation, expected.FailureTruncation) ||
		!sameMessageScopeValue(actual.WorkScope, expected.WorkScope) {
		return false
	}
	actualJSON, actualErr := json.Marshal(actual.Output)
	expectedJSON, expectedErr := json.Marshal(expected.Output)
	return actualErr == nil && expectedErr == nil && bytes.Equal(actualJSON, expectedJSON)
}

func normalizeCallbackCommand(command CallbackCommand) CallbackCommand {
	command.WorkspaceID = strings.TrimSpace(command.WorkspaceID)
	command.RunID = strings.TrimSpace(command.RunID)
	command.CallbackID = strings.TrimSpace(command.CallbackID)
	command.AgentID = strings.TrimSpace(command.AgentID)
	command.MemberID = strings.TrimSpace(command.MemberID)
	command.Action = strings.TrimSpace(command.Action)
	command.Section = strings.TrimSpace(command.Section)
	command.Path = strings.TrimSpace(command.Path)
	command.Cursor = strings.TrimSpace(command.Cursor)
	command.FailureCode = strings.TrimSpace(command.FailureCode)
	command.FailureMessage = strings.TrimSpace(command.FailureMessage)
	command.IdempotencyKey = strings.TrimSpace(command.IdempotencyKey)
	return command
}

func validateCallbackCommand(command CallbackCommand) error {
	if command.WorkspaceID == "" || command.RunID == "" || command.CallbackID == "" || command.AgentID == "" {
		return fmt.Errorf("workflow Run callback trusted context is incomplete")
	}
	switch command.Action {
	case CallbackActionGetCurrent:
		if command.WorkScope != nil || len(bytes.TrimSpace(command.WorkItems)) != 0 {
			return ErrCallbackOutputInvalid
		}
		if command.Section != "" && command.Section != "compact" && command.Section != "input" && command.Section != "output_contract" && command.Section != "work_items_contract" {
			return ErrCallbackOutputInvalid
		}
		if command.Limit < 0 || command.Limit > workflowpayload.MaxChunkBytes {
			return ErrCallbackOutputInvalid
		}
		return nil
	case CallbackActionPause:
		if command.Section != "" || command.Path != "" || command.Cursor != "" || command.Limit != 0 || command.WorkScope != nil || len(bytes.TrimSpace(command.WorkItems)) != 0 || command.IdempotencyKey == "" || len(command.IdempotencyKey) > 128 {
			return ErrCallbackOutputInvalid
		}
		return nil
	case CallbackActionPark:
		if command.Section != "" || command.Path != "" || command.Cursor != "" || command.Limit != 0 || command.WorkScope != nil || len(bytes.TrimSpace(command.WorkItems)) != 0 || len(command.IdempotencyKey) > 128 {
			return ErrCallbackOutputInvalid
		}
		return nil
	case CallbackActionComplete:
		if command.IdempotencyKey == "" || len(command.IdempotencyKey) > 128 {
			return ErrCallbackOutputInvalid
		}
	case CallbackActionFail:
		if command.WorkScope != nil || len(bytes.TrimSpace(command.WorkItems)) != 0 || command.IdempotencyKey == "" || len(command.IdempotencyKey) > 128 || command.FailureCode == "" || len(command.FailureCode) > 128 || len(command.FailureMessage) > 4000 {
			return ErrCallbackOutputInvalid
		}
	default:
		return ErrCallbackOutputInvalid
	}
	return nil
}

func mapCallbackReadError(err error) error {
	if errors.Is(err, storeerr.ErrNotFound) {
		return ErrCallbackNotFound
	}
	return err
}
