package service

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"cmr-backend/internal/apperr"
	"cmr-backend/internal/platform/security"
	"cmr-backend/internal/store/postgres"
)

type AdminEventService struct {
	store *postgres.Store
}

type AdminEventSummary struct {
	ID             string                `json:"id"`
	TenantCode     *string               `json:"tenantCode,omitempty"`
	TenantName     *string               `json:"tenantName,omitempty"`
	Slug           string                `json:"slug"`
	DisplayName    string                `json:"displayName"`
	Summary        *string               `json:"summary,omitempty"`
	Status         string                `json:"status"`
	CurrentRelease *AdminEventReleaseRef `json:"currentRelease,omitempty"`
}

type AdminEventReleaseRef struct {
	ID                     string                    `json:"id"`
	ConfigLabel            *string                   `json:"configLabel,omitempty"`
	ManifestURL            *string                   `json:"manifestUrl,omitempty"`
	ManifestChecksumSha256 *string                   `json:"manifestChecksumSha256,omitempty"`
	RouteCode              *string                   `json:"routeCode,omitempty"`
	Presentation           *PresentationSummaryView  `json:"presentation,omitempty"`
	ContentBundle          *ContentBundleSummaryView `json:"contentBundle,omitempty"`
}

type AdminEventDetail struct {
	Event                AdminEventSummary         `json:"event"`
	LatestSource         *EventConfigSourceView    `json:"latestSource,omitempty"`
	SourceCount          int                       `json:"sourceCount"`
	CurrentSource        *AdminAssembledSource     `json:"currentSource,omitempty"`
	PresentationCount    int                       `json:"presentationCount"`
	ContentBundleCount   int                       `json:"contentBundleCount"`
	CurrentPresentation  *PresentationSummaryView  `json:"currentPresentation,omitempty"`
	CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
	CurrentRuntime       *RuntimeSummaryView       `json:"currentRuntime,omitempty"`
}

type CreateAdminEventInput struct {
	TenantCode  *string `json:"tenantCode,omitempty"`
	Slug        string  `json:"slug"`
	DisplayName string  `json:"displayName"`
	Summary     *string `json:"summary,omitempty"`
	Status      string  `json:"status"`
}

type UpdateAdminEventInput struct {
	TenantCode  *string `json:"tenantCode,omitempty"`
	Slug        string  `json:"slug"`
	DisplayName string  `json:"displayName"`
	Summary     *string `json:"summary,omitempty"`
	Status      string  `json:"status"`
}

type SaveAdminEventSourceInput struct {
	Map struct {
		MapID     string `json:"mapId"`
		VersionID string `json:"versionId"`
	} `json:"map"`
	Playfield struct {
		PlayfieldID string `json:"playfieldId"`
		VersionID   string `json:"versionId"`
	} `json:"playfield"`
	ResourcePack *struct {
		ResourcePackID string `json:"resourcePackId"`
		VersionID      string `json:"versionId"`
	} `json:"resourcePack,omitempty"`
	GameModeCode string         `json:"gameModeCode"`
	RouteCode    *string        `json:"routeCode,omitempty"`
	Overrides    map[string]any `json:"overrides,omitempty"`
	Notes        *string        `json:"notes,omitempty"`
}

type AdminEventPresentationView struct {
	ID               string         `json:"id"`
	EventID          string         `json:"eventId"`
	Code             string         `json:"code"`
	Name             string         `json:"name"`
	PresentationType string         `json:"presentationType"`
	Status           string         `json:"status"`
	IsDefault        bool           `json:"isDefault"`
	TemplateKey      *string        `json:"templateKey,omitempty"`
	Version          *string        `json:"version,omitempty"`
	SourceType       *string        `json:"sourceType,omitempty"`
	SchemaURL        *string        `json:"schemaUrl,omitempty"`
	Schema           map[string]any `json:"schema"`
}

type CreateAdminEventPresentationInput struct {
	Code             string         `json:"code"`
	Name             string         `json:"name"`
	PresentationType string         `json:"presentationType"`
	Status           string         `json:"status"`
	IsDefault        bool           `json:"isDefault"`
	Schema           map[string]any `json:"schema,omitempty"`
}

type ImportAdminEventPresentationInput struct {
	Title       string `json:"title"`
	TemplateKey string `json:"templateKey"`
	SourceType  string `json:"sourceType"`
	SchemaURL   string `json:"schemaUrl"`
	Version     string `json:"version"`
	Status      string `json:"status"`
	IsDefault   bool   `json:"isDefault"`
}

type AdminContentBundleView struct {
	ID            string         `json:"id"`
	EventID       string         `json:"eventId"`
	Code          string         `json:"code"`
	Name          string         `json:"name"`
	Status        string         `json:"status"`
	IsDefault     bool           `json:"isDefault"`
	BundleType    *string        `json:"bundleType,omitempty"`
	Version       *string        `json:"version,omitempty"`
	SourceType    *string        `json:"sourceType,omitempty"`
	ManifestURL   *string        `json:"manifestUrl,omitempty"`
	AssetManifest any            `json:"assetManifest,omitempty"`
	EntryURL      *string        `json:"entryUrl,omitempty"`
	AssetRootURL  *string        `json:"assetRootUrl,omitempty"`
	Metadata      map[string]any `json:"metadata"`
}

type CreateAdminContentBundleInput struct {
	Code         string         `json:"code"`
	Name         string         `json:"name"`
	Status       string         `json:"status"`
	IsDefault    bool           `json:"isDefault"`
	EntryURL     *string        `json:"entryUrl,omitempty"`
	AssetRootURL *string        `json:"assetRootUrl,omitempty"`
	Metadata     map[string]any `json:"metadata,omitempty"`
}

type ImportAdminContentBundleInput struct {
	Title         string         `json:"title"`
	BundleType    string         `json:"bundleType"`
	SourceType    string         `json:"sourceType"`
	ManifestURL   string         `json:"manifestUrl"`
	Version       string         `json:"version"`
	Status        string         `json:"status"`
	IsDefault     bool           `json:"isDefault"`
	AssetManifest map[string]any `json:"assetManifest,omitempty"`
}

type UpdateAdminEventDefaultsInput struct {
	PresentationID   *string `json:"presentationId,omitempty"`
	ContentBundleID  *string `json:"contentBundleId,omitempty"`
	RuntimeBindingID *string `json:"runtimeBindingId,omitempty"`
}

type AdminAssembledSource struct {
	Refs      map[string]any `json:"refs"`
	Runtime   map[string]any `json:"runtime"`
	Overrides map[string]any `json:"overrides,omitempty"`
}

func NewAdminEventService(store *postgres.Store) *AdminEventService {
	return &AdminEventService{store: store}
}

func (s *AdminEventService) ListEvents(ctx context.Context, limit int) ([]AdminEventSummary, error) {
	items, err := s.store.ListAdminEvents(ctx, limit)
	if err != nil {
		return nil, err
	}
	results := make([]AdminEventSummary, 0, len(items))
	for _, item := range items {
		results = append(results, buildAdminEventSummary(item))
	}
	return results, nil
}

func (s *AdminEventService) CreateEvent(ctx context.Context, input CreateAdminEventInput) (*AdminEventSummary, error) {
	input.Slug = strings.TrimSpace(input.Slug)
	input.DisplayName = strings.TrimSpace(input.DisplayName)
	if input.Slug == "" || input.DisplayName == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "slug and displayName are required")
	}

	var tenantID *string
	var tenantCode *string
	var tenantName *string
	if input.TenantCode != nil && strings.TrimSpace(*input.TenantCode) != "" {
		tenant, err := s.store.GetTenantByCode(ctx, strings.TrimSpace(*input.TenantCode))
		if err != nil {
			return nil, err
		}
		if tenant == nil {
			return nil, apperr.New(http.StatusNotFound, "tenant_not_found", "tenant not found")
		}
		tenantID = &tenant.ID
		tenantCode = &tenant.TenantCode
		tenantName = &tenant.Name
	}

	publicID, err := security.GeneratePublicID("evt")
	if err != nil {
		return nil, err
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	item, err := s.store.CreateAdminEvent(ctx, tx, postgres.CreateAdminEventParams{
		PublicID:    publicID,
		TenantID:    tenantID,
		Slug:        input.Slug,
		DisplayName: input.DisplayName,
		Summary:     trimStringPtr(input.Summary),
		Status:      normalizeEventCatalogStatus(input.Status),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	return &AdminEventSummary{
		ID:          item.PublicID,
		TenantCode:  tenantCode,
		TenantName:  tenantName,
		Slug:        item.Slug,
		DisplayName: item.DisplayName,
		Summary:     item.Summary,
		Status:      item.Status,
	}, nil
}

func (s *AdminEventService) UpdateEvent(ctx context.Context, eventPublicID string, input UpdateAdminEventInput) (*AdminEventSummary, error) {
	record, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if record == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	input.Slug = strings.TrimSpace(input.Slug)
	input.DisplayName = strings.TrimSpace(input.DisplayName)
	if input.Slug == "" || input.DisplayName == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "slug and displayName are required")
	}

	var tenantID *string
	clearTenant := false
	if input.TenantCode != nil {
		if trimmed := strings.TrimSpace(*input.TenantCode); trimmed != "" {
			tenant, err := s.store.GetTenantByCode(ctx, trimmed)
			if err != nil {
				return nil, err
			}
			if tenant == nil {
				return nil, apperr.New(http.StatusNotFound, "tenant_not_found", "tenant not found")
			}
			tenantID = &tenant.ID
		} else {
			clearTenant = true
		}
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	updated, err := s.store.UpdateAdminEvent(ctx, tx, postgres.UpdateAdminEventParams{
		EventID:     record.ID,
		TenantID:    tenantID,
		Slug:        input.Slug,
		DisplayName: input.DisplayName,
		Summary:     trimStringPtr(input.Summary),
		Status:      normalizeEventCatalogStatus(input.Status),
		ClearTenant: clearTenant,
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	refreshed, err := s.store.GetAdminEventByPublicID(ctx, updated.PublicID)
	if err != nil {
		return nil, err
	}
	if refreshed == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	summary := buildAdminEventSummary(*refreshed)
	return &summary, nil
}

func (s *AdminEventService) GetEventDetail(ctx context.Context, eventPublicID string) (*AdminEventDetail, error) {
	record, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if record == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	sources, err := s.store.ListEventConfigSourcesByEventID(ctx, record.ID, 1)
	if err != nil {
		return nil, err
	}
	allSources, err := s.store.ListEventConfigSourcesByEventID(ctx, record.ID, 200)
	if err != nil {
		return nil, err
	}
	presentations, err := s.store.ListEventPresentationsByEventID(ctx, record.ID, 200)
	if err != nil {
		return nil, err
	}
	contentBundles, err := s.store.ListContentBundlesByEventID(ctx, record.ID, 200)
	if err != nil {
		return nil, err
	}

	result := &AdminEventDetail{
		Event:              buildAdminEventSummary(*record),
		SourceCount:        len(allSources),
		PresentationCount:  len(presentations),
		ContentBundleCount: len(contentBundles),
	}
	if len(sources) > 0 {
		latest, err := buildEventConfigSourceView(&sources[0], record.PublicID)
		if err != nil {
			return nil, err
		}
		result.LatestSource = latest
		result.CurrentSource = buildAdminAssembledSource(latest.Source)
	}
	result.CurrentPresentation = buildPresentationSummaryFromEventRecord(record)
	result.CurrentContentBundle = buildContentBundleSummaryFromEventRecord(record)
	result.CurrentRuntime = buildRuntimeSummaryFromAdminEventRecord(record)
	if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, record.CurrentPresentationID); err != nil {
		return nil, err
	} else if enrichedPresentation != nil {
		result.CurrentPresentation = enrichedPresentation
	}
	if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, record.CurrentContentBundleID); err != nil {
		return nil, err
	} else if enrichedBundle != nil {
		result.CurrentContentBundle = enrichedBundle
	}
	return result, nil
}

func (s *AdminEventService) ListEventPresentations(ctx context.Context, eventPublicID string, limit int) ([]AdminEventPresentationView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	items, err := s.store.ListEventPresentationsByEventID(ctx, eventRecord.ID, limit)
	if err != nil {
		return nil, err
	}
	result := make([]AdminEventPresentationView, 0, len(items))
	for _, item := range items {
		view, err := buildAdminEventPresentationView(item)
		if err != nil {
			return nil, err
		}
		result = append(result, view)
	}
	return result, nil
}

func (s *AdminEventService) CreateEventPresentation(ctx context.Context, eventPublicID string, input CreateAdminEventPresentationInput) (*AdminEventPresentationView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	input.Code = strings.TrimSpace(input.Code)
	input.Name = strings.TrimSpace(input.Name)
	input.PresentationType = normalizePresentationType(input.PresentationType)
	if input.Code == "" || input.Name == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
	}
	publicID, err := security.GeneratePublicID("pres")
	if err != nil {
		return nil, err
	}
	schema := input.Schema
	if schema == nil {
		schema = map[string]any{}
	}
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)
	record, err := s.store.CreateEventPresentation(ctx, tx, postgres.CreateEventPresentationParams{
		PublicID:         publicID,
		EventID:          eventRecord.ID,
		Code:             input.Code,
		Name:             input.Name,
		PresentationType: input.PresentationType,
		Status:           normalizeEventCatalogStatus(input.Status),
		IsDefault:        input.IsDefault,
		SchemaJSON:       mustMarshalJSONObject(schema),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	view, err := buildAdminEventPresentationView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) ImportEventPresentation(ctx context.Context, eventPublicID string, input ImportAdminEventPresentationInput) (*AdminEventPresentationView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	input.Title = strings.TrimSpace(input.Title)
	input.TemplateKey = strings.TrimSpace(input.TemplateKey)
	input.SourceType = strings.TrimSpace(input.SourceType)
	input.SchemaURL = strings.TrimSpace(input.SchemaURL)
	input.Version = strings.TrimSpace(input.Version)
	if input.Title == "" || input.TemplateKey == "" || input.SourceType == "" || input.SchemaURL == "" || input.Version == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "title, templateKey, sourceType, schemaUrl and version are required")
	}

	publicID, err := security.GeneratePublicID("pres")
	if err != nil {
		return nil, err
	}
	code := generateImportedPresentationCode(input.Title, publicID)
	status := normalizeEventCatalogStatus(input.Status)
	if strings.TrimSpace(input.Status) == "" {
		status = "active"
	}
	schema := map[string]any{
		"templateKey": input.TemplateKey,
		"sourceType":  input.SourceType,
		"schemaUrl":   input.SchemaURL,
		"version":     input.Version,
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	record, err := s.store.CreateEventPresentation(ctx, tx, postgres.CreateEventPresentationParams{
		PublicID:         publicID,
		EventID:          eventRecord.ID,
		Code:             code,
		Name:             input.Title,
		PresentationType: "generic",
		Status:           status,
		IsDefault:        input.IsDefault,
		SchemaJSON:       mustMarshalJSONObject(schema),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	view, err := buildAdminEventPresentationView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) GetEventPresentation(ctx context.Context, presentationPublicID string) (*AdminEventPresentationView, error) {
	record, err := s.store.GetEventPresentationByPublicID(ctx, strings.TrimSpace(presentationPublicID))
	if err != nil {
		return nil, err
	}
	if record == nil {
		return nil, apperr.New(http.StatusNotFound, "presentation_not_found", "presentation not found")
	}
	view, err := buildAdminEventPresentationView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) ListContentBundles(ctx context.Context, eventPublicID string, limit int) ([]AdminContentBundleView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	items, err := s.store.ListContentBundlesByEventID(ctx, eventRecord.ID, limit)
	if err != nil {
		return nil, err
	}
	result := make([]AdminContentBundleView, 0, len(items))
	for _, item := range items {
		view, err := buildAdminContentBundleView(item)
		if err != nil {
			return nil, err
		}
		result = append(result, view)
	}
	return result, nil
}

func (s *AdminEventService) CreateContentBundle(ctx context.Context, eventPublicID string, input CreateAdminContentBundleInput) (*AdminContentBundleView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	input.Code = strings.TrimSpace(input.Code)
	input.Name = strings.TrimSpace(input.Name)
	if input.Code == "" || input.Name == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
	}
	publicID, err := security.GeneratePublicID("bundle")
	if err != nil {
		return nil, err
	}
	metadata := input.Metadata
	if metadata == nil {
		metadata = map[string]any{}
	}
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)
	record, err := s.store.CreateContentBundle(ctx, tx, postgres.CreateContentBundleParams{
		PublicID:     publicID,
		EventID:      eventRecord.ID,
		Code:         input.Code,
		Name:         input.Name,
		Status:       normalizeEventCatalogStatus(input.Status),
		IsDefault:    input.IsDefault,
		EntryURL:     trimStringPtr(input.EntryURL),
		AssetRootURL: trimStringPtr(input.AssetRootURL),
		MetadataJSON: mustMarshalJSONObject(metadata),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	view, err := buildAdminContentBundleView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) ImportContentBundle(ctx context.Context, eventPublicID string, input ImportAdminContentBundleInput) (*AdminContentBundleView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	input.Title = strings.TrimSpace(input.Title)
	input.BundleType = strings.TrimSpace(input.BundleType)
	input.SourceType = strings.TrimSpace(input.SourceType)
	input.ManifestURL = strings.TrimSpace(input.ManifestURL)
	input.Version = strings.TrimSpace(input.Version)
	if input.Title == "" || input.BundleType == "" || input.SourceType == "" || input.ManifestURL == "" || input.Version == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "title, bundleType, sourceType, manifestUrl and version are required")
	}

	publicID, err := security.GeneratePublicID("bundle")
	if err != nil {
		return nil, err
	}
	code := generateImportedBundleCode(input.Title, publicID)
	assetManifest := input.AssetManifest
	if assetManifest == nil {
		assetManifest = map[string]any{
			"manifestUrl": input.ManifestURL,
			"sourceType":  input.SourceType,
		}
	}
	metadata := map[string]any{
		"bundleType":    input.BundleType,
		"sourceType":    input.SourceType,
		"manifestUrl":   input.ManifestURL,
		"version":       input.Version,
		"assetManifest": assetManifest,
	}
	status := normalizeEventCatalogStatus(input.Status)
	if strings.TrimSpace(input.Status) == "" {
		status = "active"
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	record, err := s.store.CreateContentBundle(ctx, tx, postgres.CreateContentBundleParams{
		PublicID:     publicID,
		EventID:      eventRecord.ID,
		Code:         code,
		Name:         input.Title,
		Status:       status,
		IsDefault:    input.IsDefault,
		EntryURL:     nil,
		AssetRootURL: nil,
		MetadataJSON: mustMarshalJSONObject(metadata),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	view, err := buildAdminContentBundleView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) GetContentBundle(ctx context.Context, contentBundlePublicID string) (*AdminContentBundleView, error) {
	record, err := s.store.GetContentBundleByPublicID(ctx, strings.TrimSpace(contentBundlePublicID))
	if err != nil {
		return nil, err
	}
	if record == nil {
		return nil, apperr.New(http.StatusNotFound, "content_bundle_not_found", "content bundle not found")
	}
	view, err := buildAdminContentBundleView(*record)
	if err != nil {
		return nil, err
	}
	return &view, nil
}

func (s *AdminEventService) UpdateEventDefaults(ctx context.Context, eventPublicID string, input UpdateAdminEventDefaultsInput) (*AdminEventDetail, error) {
	record, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if record == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	var presentationID *string
	updatePresentation := false
	if input.PresentationID != nil {
		updatePresentation = true
		trimmed := strings.TrimSpace(*input.PresentationID)
		if trimmed != "" {
			presentation, err := s.store.GetEventPresentationByPublicID(ctx, trimmed)
			if err != nil {
				return nil, err
			}
			if presentation == nil {
				return nil, apperr.New(http.StatusNotFound, "presentation_not_found", "presentation not found")
			}
			if presentation.EventID != record.ID {
				return nil, apperr.New(http.StatusConflict, "presentation_not_belong_to_event", "presentation does not belong to event")
			}
			presentationID = &presentation.ID
		}
	}

	var contentBundleID *string
	updateContent := false
	if input.ContentBundleID != nil {
		updateContent = true
		trimmed := strings.TrimSpace(*input.ContentBundleID)
		if trimmed != "" {
			contentBundle, err := s.store.GetContentBundleByPublicID(ctx, trimmed)
			if err != nil {
				return nil, err
			}
			if contentBundle == nil {
				return nil, apperr.New(http.StatusNotFound, "content_bundle_not_found", "content bundle not found")
			}
			if contentBundle.EventID != record.ID {
				return nil, apperr.New(http.StatusConflict, "content_bundle_not_belong_to_event", "content bundle does not belong to event")
			}
			contentBundleID = &contentBundle.ID
		}
	}

	var runtimeBindingID *string
	updateRuntime := false
	if input.RuntimeBindingID != nil {
		updateRuntime = true
		trimmed := strings.TrimSpace(*input.RuntimeBindingID)
		if trimmed != "" {
			runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, trimmed)
			if err != nil {
				return nil, err
			}
			if runtimeBinding == nil {
				return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
			}
			if runtimeBinding.EventID != record.ID {
				return nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to event")
			}
			runtimeBindingID = &runtimeBinding.ID
		}
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	if err := s.store.SetEventDefaultBindings(ctx, tx, postgres.SetEventDefaultBindingsParams{
		EventID:            record.ID,
		PresentationID:     presentationID,
		ContentBundleID:    contentBundleID,
		RuntimeBindingID:   runtimeBindingID,
		UpdatePresentation: updatePresentation,
		UpdateContent:      updateContent,
		UpdateRuntime:      updateRuntime,
	}); err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	return s.GetEventDetail(ctx, eventPublicID)
}

func (s *AdminEventService) SaveEventSource(ctx context.Context, eventPublicID string, input SaveAdminEventSourceInput) (*EventConfigSourceView, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}

	input.GameModeCode = strings.TrimSpace(input.GameModeCode)
	input.Map.MapID = strings.TrimSpace(input.Map.MapID)
	input.Map.VersionID = strings.TrimSpace(input.Map.VersionID)
	input.Playfield.PlayfieldID = strings.TrimSpace(input.Playfield.PlayfieldID)
	input.Playfield.VersionID = strings.TrimSpace(input.Playfield.VersionID)
	if input.Map.MapID == "" || input.Map.VersionID == "" || input.Playfield.PlayfieldID == "" || input.Playfield.VersionID == "" || input.GameModeCode == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "map, playfield and gameModeCode are required")
	}

	mapVersion, err := s.store.GetResourceMapVersionByPublicID(ctx, input.Map.MapID, input.Map.VersionID)
	if err != nil {
		return nil, err
	}
	if mapVersion == nil {
		return nil, apperr.New(http.StatusNotFound, "map_version_not_found", "map version not found")
	}
	playfieldVersion, err := s.store.GetResourcePlayfieldVersionByPublicID(ctx, input.Playfield.PlayfieldID, input.Playfield.VersionID)
	if err != nil {
		return nil, err
	}
	if playfieldVersion == nil {
		return nil, apperr.New(http.StatusNotFound, "playfield_version_not_found", "playfield version not found")
	}

	var resourcePackVersion *postgres.ResourcePackVersion
	if input.ResourcePack != nil {
		input.ResourcePack.ResourcePackID = strings.TrimSpace(input.ResourcePack.ResourcePackID)
		input.ResourcePack.VersionID = strings.TrimSpace(input.ResourcePack.VersionID)
		if input.ResourcePack.ResourcePackID == "" || input.ResourcePack.VersionID == "" {
			return nil, apperr.New(http.StatusBadRequest, "invalid_params", "resourcePackId and versionId are required when resourcePack is provided")
		}
		resourcePackVersion, err = s.store.GetResourcePackVersionByPublicID(ctx, input.ResourcePack.ResourcePackID, input.ResourcePack.VersionID)
		if err != nil {
			return nil, err
		}
		if resourcePackVersion == nil {
			return nil, apperr.New(http.StatusNotFound, "resource_pack_version_not_found", "resource pack version not found")
		}
	}

	source := s.buildEventSource(eventRecord, mapVersion, playfieldVersion, resourcePackVersion, input)
	if err := validateSourceConfig(source); err != nil {
		return nil, err
	}

	nextVersion, err := s.store.NextEventConfigSourceVersion(ctx, eventRecord.ID)
	if err != nil {
		return nil, err
	}

	note := trimStringPtr(input.Notes)
	if note == nil {
		defaultNote := fmt.Sprintf("assembled from admin refs: map=%s/%s playfield=%s/%s", input.Map.MapID, input.Map.VersionID, input.Playfield.PlayfieldID, input.Playfield.VersionID)
		note = &defaultNote
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	record, err := s.store.UpsertEventConfigSource(ctx, tx, postgres.UpsertEventConfigSourceParams{
		EventID:         eventRecord.ID,
		SourceVersionNo: nextVersion,
		SourceKind:      "admin_assembled_bundle",
		SchemaID:        "event-source",
		SchemaVersion:   resolveSchemaVersion(source),
		Status:          "active",
		Source:          source,
		Notes:           note,
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return buildEventConfigSourceView(record, eventRecord.PublicID)
}

func (s *AdminEventService) buildEventSource(event *postgres.AdminEventRecord, mapVersion *postgres.ResourceMapVersion, playfieldVersion *postgres.ResourcePlayfieldVersion, resourcePackVersion *postgres.ResourcePackVersion, input SaveAdminEventSourceInput) map[string]any {
	source := map[string]any{
		"schemaVersion": "1",
		"app": map[string]any{
			"id":    event.PublicID,
			"title": event.DisplayName,
		},
		"refs": map[string]any{
			"map": map[string]any{
				"id":        input.Map.MapID,
				"versionId": input.Map.VersionID,
			},
			"playfield": map[string]any{
				"id":        input.Playfield.PlayfieldID,
				"versionId": input.Playfield.VersionID,
			},
			"gameMode": map[string]any{
				"code": input.GameModeCode,
			},
		},
		"map": map[string]any{
			"tiles":   mapVersion.TilesRootURL,
			"mapmeta": mapVersion.MapmetaURL,
		},
		"playfield": map[string]any{
			"kind": "course",
			"source": map[string]any{
				"type": playfieldVersion.SourceType,
				"url":  playfieldVersion.SourceURL,
			},
		},
		"game": map[string]any{
			"mode": input.GameModeCode,
		},
	}
	if event.Summary != nil && strings.TrimSpace(*event.Summary) != "" {
		source["summary"] = *event.Summary
	}
	if event.TenantCode != nil && strings.TrimSpace(*event.TenantCode) != "" {
		source["branding"] = map[string]any{
			"tenantCode": *event.TenantCode,
		}
	}
	if input.RouteCode != nil && strings.TrimSpace(*input.RouteCode) != "" {
		source["playfield"].(map[string]any)["metadata"] = map[string]any{
			"routeCode": strings.TrimSpace(*input.RouteCode),
		}
	}
	if resourcePackVersion != nil {
		source["refs"].(map[string]any)["resourcePack"] = map[string]any{
			"id":        input.ResourcePack.ResourcePackID,
			"versionId": input.ResourcePack.VersionID,
		}
		resources := map[string]any{}
		assets := map[string]any{}
		if resourcePackVersion.ThemeProfileCode != nil && strings.TrimSpace(*resourcePackVersion.ThemeProfileCode) != "" {
			resources["themeProfile"] = *resourcePackVersion.ThemeProfileCode
		}
		if resourcePackVersion.ContentEntryURL != nil && strings.TrimSpace(*resourcePackVersion.ContentEntryURL) != "" {
			assets["contentHtml"] = *resourcePackVersion.ContentEntryURL
		}
		if resourcePackVersion.AudioRootURL != nil && strings.TrimSpace(*resourcePackVersion.AudioRootURL) != "" {
			resources["audioRoot"] = *resourcePackVersion.AudioRootURL
		}
		if len(resources) > 0 {
			source["resources"] = resources
		}
		if len(assets) > 0 {
			source["assets"] = assets
		}
	}
	if len(input.Overrides) > 0 {
		source["overrides"] = input.Overrides
		mergeJSONObject(source, input.Overrides)
	}
	return source
}

func buildAdminEventSummary(item postgres.AdminEventRecord) AdminEventSummary {
	summary := AdminEventSummary{
		ID:          item.PublicID,
		TenantCode:  item.TenantCode,
		TenantName:  item.TenantName,
		Slug:        item.Slug,
		DisplayName: item.DisplayName,
		Summary:     item.Summary,
		Status:      item.Status,
	}
	if item.CurrentReleasePubID != nil {
		summary.CurrentRelease = &AdminEventReleaseRef{
			ID:                     *item.CurrentReleasePubID,
			ConfigLabel:            item.ConfigLabel,
			ManifestURL:            item.ManifestURL,
			ManifestChecksumSha256: item.ManifestChecksum,
			RouteCode:              item.RouteCode,
			Presentation:           buildPresentationSummaryFromEventRecord(&item),
			ContentBundle:          buildContentBundleSummaryFromEventRecord(&item),
		}
	}
	return summary
}

func buildPresentationSummaryFromEventRecord(item *postgres.AdminEventRecord) *PresentationSummaryView {
	if item == nil || item.CurrentPresentationID == nil {
		return nil
	}
	return &PresentationSummaryView{
		PresentationID:   *item.CurrentPresentationID,
		Name:             item.CurrentPresentationName,
		PresentationType: item.CurrentPresentationType,
	}
}

func buildContentBundleSummaryFromEventRecord(item *postgres.AdminEventRecord) *ContentBundleSummaryView {
	if item == nil || item.CurrentContentBundleID == nil {
		return nil
	}
	return &ContentBundleSummaryView{
		ContentBundleID: *item.CurrentContentBundleID,
		Name:            item.CurrentContentBundleName,
		EntryURL:        item.CurrentContentEntryURL,
		AssetRootURL:    item.CurrentContentAssetRootURL,
	}
}

func buildRuntimeSummaryFromAdminEventRecord(item *postgres.AdminEventRecord) *RuntimeSummaryView {
	if item == nil ||
		item.CurrentRuntimeBindingID == nil ||
		item.CurrentPlaceID == nil ||
		item.CurrentMapAssetID == nil ||
		item.CurrentTileReleaseID == nil ||
		item.CurrentCourseSetID == nil ||
		item.CurrentCourseVariantID == nil {
		return nil
	}
	return &RuntimeSummaryView{
		RuntimeBindingID:  *item.CurrentRuntimeBindingID,
		PlaceID:           *item.CurrentPlaceID,
		MapID:             *item.CurrentMapAssetID,
		TileReleaseID:     *item.CurrentTileReleaseID,
		CourseSetID:       *item.CurrentCourseSetID,
		CourseVariantID:   *item.CurrentCourseVariantID,
		CourseVariantName: item.CurrentCourseVariantName,
		RouteCode:         item.CurrentRuntimeRouteCode,
	}
}

func buildAdminEventPresentationView(item postgres.EventPresentation) (AdminEventPresentationView, error) {
	schema, err := decodeJSONObject(item.SchemaJSON)
	if err != nil {
		return AdminEventPresentationView{}, err
	}
	return AdminEventPresentationView{
		ID:               item.PublicID,
		EventID:          item.EventPublicID,
		Code:             item.Code,
		Name:             item.Name,
		PresentationType: item.PresentationType,
		Status:           item.Status,
		IsDefault:        item.IsDefault,
		TemplateKey:      readStringField(schema, "templateKey"),
		Version:          readStringField(schema, "version"),
		SourceType:       readStringField(schema, "sourceType"),
		SchemaURL:        readStringField(schema, "schemaUrl"),
		Schema:           schema,
	}, nil
}

func buildAdminContentBundleView(item postgres.ContentBundle) (AdminContentBundleView, error) {
	metadata, err := decodeJSONObject(item.MetadataJSON)
	if err != nil {
		return AdminContentBundleView{}, err
	}
	return AdminContentBundleView{
		ID:            item.PublicID,
		EventID:       item.EventPublicID,
		Code:          item.Code,
		Name:          item.Name,
		Status:        item.Status,
		IsDefault:     item.IsDefault,
		BundleType:    readStringField(metadata, "bundleType"),
		Version:       readStringField(metadata, "version"),
		SourceType:    readStringField(metadata, "sourceType"),
		ManifestURL:   readStringField(metadata, "manifestUrl"),
		AssetManifest: metadata["assetManifest"],
		EntryURL:      item.EntryURL,
		AssetRootURL:  item.AssetRootURL,
		Metadata:      metadata,
	}, nil
}

func generateImportedBundleCode(title, publicID string) string {
	var builder strings.Builder
	for _, r := range strings.ToLower(title) {
		switch {
		case r >= 'a' && r <= 'z':
			builder.WriteRune(r)
		case r >= '0' && r <= '9':
			builder.WriteRune(r)
		case r == ' ' || r == '-' || r == '_':
			if builder.Len() == 0 {
				continue
			}
			last := builder.String()[builder.Len()-1]
			if last != '-' {
				builder.WriteByte('-')
			}
		}
	}
	code := strings.Trim(builder.String(), "-")
	if code == "" {
		code = "bundle"
	}
	suffix := publicID
	if len(suffix) > 8 {
		suffix = suffix[len(suffix)-8:]
	}
	return code + "-" + suffix
}

func generateImportedPresentationCode(title, publicID string) string {
	var builder strings.Builder
	for _, r := range strings.ToLower(title) {
		switch {
		case r >= 'a' && r <= 'z':
			builder.WriteRune(r)
		case r >= '0' && r <= '9':
			builder.WriteRune(r)
		case r == ' ' || r == '-' || r == '_':
			if builder.Len() == 0 {
				continue
			}
			last := builder.String()[builder.Len()-1]
			if last != '-' {
				builder.WriteByte('-')
			}
		}
	}
	code := strings.Trim(builder.String(), "-")
	if code == "" {
		code = "presentation"
	}
	suffix := publicID
	if len(suffix) > 8 {
		suffix = suffix[len(suffix)-8:]
	}
	return code + "-" + suffix
}

func buildAdminAssembledSource(source map[string]any) *AdminAssembledSource {
	result := &AdminAssembledSource{}
	if refs, ok := source["refs"].(map[string]any); ok {
		result.Refs = refs
	}
	runtime := cloneJSONObject(source)
	delete(runtime, "refs")
	delete(runtime, "overrides")
	if overrides, ok := source["overrides"].(map[string]any); ok && len(overrides) > 0 {
		result.Overrides = overrides
	}
	result.Runtime = runtime
	return result
}

func normalizeEventCatalogStatus(value string) string {
	switch strings.TrimSpace(value) {
	case "active":
		return "active"
	case "disabled":
		return "disabled"
	case "archived":
		return "archived"
	default:
		return "draft"
	}
}

func normalizePresentationType(value string) string {
	switch strings.TrimSpace(value) {
	case "card":
		return "card"
	case "detail":
		return "detail"
	case "h5":
		return "h5"
	case "result":
		return "result"
	default:
		return "generic"
	}
}

func mustMarshalJSONObject(value map[string]any) string {
	raw, _ := json.Marshal(value)
	return string(raw)
}

func mergeJSONObject(target map[string]any, overrides map[string]any) {
	for key, value := range overrides {
		if valueMap, ok := value.(map[string]any); ok {
			existing, ok := target[key].(map[string]any)
			if !ok {
				existing = map[string]any{}
				target[key] = existing
			}
			mergeJSONObject(existing, valueMap)
			continue
		}
		target[key] = value
	}
}
