package notification

import (
	"fmt"

	"galaxy/backend/push"
	"galaxy/transcoder"

	"github.com/google/uuid"
)

// preMarshaledEvent adapts a pre-encoded FlatBuffers payload to the
// push.Event interface. The factory below pre-encodes the payload at
// construction time so the kind-specific build error surfaces inside
// the dispatcher (where it can drive retry / dead-letter logic) rather
// than inside push.Service.PublishClientEvent.
type preMarshaledEvent struct {
	kind    string
	payload []byte
}

func (e preMarshaledEvent) Kind() string             { return e.kind }
func (e preMarshaledEvent) Marshal() ([]byte, error) { return e.payload, nil }

// buildClientPushEvent maps a catalog kind together with the producer
// payload map onto a typed push.Event. Every catalog kind has a
// FlatBuffers schema in `pkg/schema/fbs/notification.fbs`; an unknown
// kind falls back to push.JSONEvent so a misconfigured producer keeps
// the pipeline flowing while the catalog catches up.
func buildClientPushEvent(kind string, payload map[string]any) (push.Event, error) {
	switch kind {
	case KindLobbyInviteReceived:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		inviter, err := mapUUID(payload, "inviter_user_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyInviteReceivedEventToPayload(&transcoder.LobbyInviteReceivedEvent{
			GameID:        gameID,
			InviterUserID: inviter,
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyInviteRevoked:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyInviteRevokedEventToPayload(&transcoder.LobbyInviteRevokedEvent{GameID: gameID})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyApplicationSubmitted:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		appID, err := mapUUID(payload, "application_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyApplicationSubmittedEventToPayload(&transcoder.LobbyApplicationSubmittedEvent{
			GameID:        gameID,
			ApplicationID: appID,
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyApplicationApproved:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyApplicationApprovedEventToPayload(&transcoder.LobbyApplicationApprovedEvent{GameID: gameID})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyApplicationRejected:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyApplicationRejectedEventToPayload(&transcoder.LobbyApplicationRejectedEvent{GameID: gameID})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyMembershipRemoved:
		bytes, err := transcoder.LobbyMembershipRemovedEventToPayload(&transcoder.LobbyMembershipRemovedEvent{
			Reason: mapStringOpt(payload, "reason"),
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyMembershipBlocked:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyMembershipBlockedEventToPayload(&transcoder.LobbyMembershipBlockedEvent{
			GameID: gameID,
			Reason: mapStringOpt(payload, "reason"),
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyRaceNameRegistered:
		raceName, err := mapString(payload, "race_name")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyRaceNameRegisteredEventToPayload(&transcoder.LobbyRaceNameRegisteredEvent{RaceName: raceName})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyRaceNamePending:
		raceName, err := mapString(payload, "race_name")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyRaceNamePendingEventToPayload(&transcoder.LobbyRaceNamePendingEvent{
			RaceName:  raceName,
			ExpiresAt: mapStringOpt(payload, "expires_at"),
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindLobbyRaceNameExpired:
		raceName, err := mapString(payload, "race_name")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.LobbyRaceNameExpiredEventToPayload(&transcoder.LobbyRaceNameExpiredEvent{RaceName: raceName})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindRuntimeImagePullFailed:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.RuntimeImagePullFailedEventToPayload(&transcoder.RuntimeImagePullFailedEvent{
			GameID:   gameID,
			ImageRef: mapStringOpt(payload, "image_ref"),
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindRuntimeContainerStartFailed:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.RuntimeContainerStartFailedEventToPayload(&transcoder.RuntimeContainerStartFailedEvent{GameID: gameID})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil

	case KindRuntimeStartConfigInvalid:
		gameID, err := mapUUID(payload, "game_id")
		if err != nil {
			return nil, err
		}
		bytes, err := transcoder.RuntimeStartConfigInvalidEventToPayload(&transcoder.RuntimeStartConfigInvalidEvent{
			GameID: gameID,
			Reason: mapStringOpt(payload, "reason"),
		})
		if err != nil {
			return nil, err
		}
		return preMarshaledEvent{kind: kind, payload: bytes}, nil
	}

	return push.JSONEvent{EventKind: kind, Payload: payload}, nil
}

// mapUUID extracts a required UUID-shaped field from the producer
// payload. Producers stringify uuid values before assembling Intent
// payloads, so the JSON-roundtripped form is `string`.
func mapUUID(payload map[string]any, key string) (uuid.UUID, error) {
	raw, ok := payload[key]
	if !ok {
		return uuid.Nil, fmt.Errorf("notification payload: %s is missing", key)
	}
	str, ok := raw.(string)
	if !ok {
		return uuid.Nil, fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
	}
	parsed, err := uuid.Parse(str)
	if err != nil {
		return uuid.Nil, fmt.Errorf("notification payload: %s is not a uuid: %w", key, err)
	}
	return parsed, nil
}

// mapString extracts a required string field from the producer payload.
func mapString(payload map[string]any, key string) (string, error) {
	raw, ok := payload[key]
	if !ok {
		return "", fmt.Errorf("notification payload: %s is missing", key)
	}
	str, ok := raw.(string)
	if !ok {
		return "", fmt.Errorf("notification payload: %s must be a string, got %T", key, raw)
	}
	if str == "" {
		return "", fmt.Errorf("notification payload: %s is empty", key)
	}
	return str, nil
}

// mapStringOpt returns the string value for key, or "" when the key is
// missing or carries a non-string value.
func mapStringOpt(payload map[string]any, key string) string {
	raw, ok := payload[key]
	if !ok {
		return ""
	}
	str, _ := raw.(string)
	return str
}
