package service

import (
	"context"
	"net/http"
	"strings"
	"time"

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

type AdminProductionService struct {
	store *postgres.Store
}

type AdminPlaceSummary struct {
	ID          string         `json:"id"`
	Code        string         `json:"code"`
	Name        string         `json:"name"`
	Region      *string        `json:"region,omitempty"`
	CoverURL    *string        `json:"coverUrl,omitempty"`
	Description *string        `json:"description,omitempty"`
	CenterPoint map[string]any `json:"centerPoint,omitempty"`
	Status      string         `json:"status"`
}

type AdminPlaceDetail struct {
	Place     AdminPlaceSummary      `json:"place"`
	MapAssets []AdminMapAssetSummary `json:"mapAssets"`
}

type CreateAdminPlaceInput struct {
	Code        string         `json:"code"`
	Name        string         `json:"name"`
	Region      *string        `json:"region,omitempty"`
	CoverURL    *string        `json:"coverUrl,omitempty"`
	Description *string        `json:"description,omitempty"`
	CenterPoint map[string]any `json:"centerPoint,omitempty"`
	Status      string         `json:"status"`
}

type AdminMapAssetSummary struct {
	ID                 string                 `json:"id"`
	PlaceID            string                 `json:"placeId"`
	LegacyMapID        *string                `json:"legacyMapId,omitempty"`
	Code               string                 `json:"code"`
	Name               string                 `json:"name"`
	MapType            string                 `json:"mapType"`
	CoverURL           *string                `json:"coverUrl,omitempty"`
	Description        *string                `json:"description,omitempty"`
	Status             string                 `json:"status"`
	CurrentTileRelease *AdminTileReleaseBrief `json:"currentTileRelease,omitempty"`
}

type AdminTileReleaseBrief struct {
	ID          string `json:"id"`
	VersionCode string `json:"versionCode"`
	Status      string `json:"status"`
}

type AdminMapAssetDetail struct {
	MapAsset     AdminMapAssetSummary   `json:"mapAsset"`
	TileReleases []AdminTileReleaseView `json:"tileReleases"`
	CourseSets   []AdminCourseSetBrief  `json:"courseSets"`
}

type CreateAdminMapAssetInput struct {
	Code        string  `json:"code"`
	Name        string  `json:"name"`
	MapType     string  `json:"mapType"`
	LegacyMapID *string `json:"legacyMapId,omitempty"`
	CoverURL    *string `json:"coverUrl,omitempty"`
	Description *string `json:"description,omitempty"`
	Status      string  `json:"status"`
}

type AdminTileReleaseView struct {
	ID                 string         `json:"id"`
	LegacyVersionID    *string        `json:"legacyVersionId,omitempty"`
	VersionCode        string         `json:"versionCode"`
	Status             string         `json:"status"`
	TileBaseURL        string         `json:"tileBaseUrl"`
	MetaURL            string         `json:"metaUrl"`
	PublishedAssetRoot *string        `json:"publishedAssetRoot,omitempty"`
	Metadata           map[string]any `json:"metadata,omitempty"`
	PublishedAt        *time.Time     `json:"publishedAt,omitempty"`
}

type CreateAdminTileReleaseInput struct {
	LegacyVersionID    *string        `json:"legacyVersionId,omitempty"`
	VersionCode        string         `json:"versionCode"`
	Status             string         `json:"status"`
	TileBaseURL        string         `json:"tileBaseUrl"`
	MetaURL            string         `json:"metaUrl"`
	PublishedAssetRoot *string        `json:"publishedAssetRoot,omitempty"`
	Metadata           map[string]any `json:"metadata,omitempty"`
	SetAsCurrent       bool           `json:"setAsCurrent"`
}

type AdminCourseSourceSummary struct {
	ID              string         `json:"id"`
	LegacyVersionID *string        `json:"legacyVersionId,omitempty"`
	SourceType      string         `json:"sourceType"`
	FileURL         string         `json:"fileUrl"`
	Checksum        *string        `json:"checksum,omitempty"`
	ParserVersion   *string        `json:"parserVersion,omitempty"`
	ImportStatus    string         `json:"importStatus"`
	Metadata        map[string]any `json:"metadata,omitempty"`
	ImportedAt      time.Time      `json:"importedAt"`
}

type CreateAdminCourseSourceInput struct {
	LegacyPlayfieldID *string        `json:"legacyPlayfieldId,omitempty"`
	LegacyVersionID   *string        `json:"legacyVersionId,omitempty"`
	SourceType        string         `json:"sourceType"`
	FileURL           string         `json:"fileUrl"`
	Checksum          *string        `json:"checksum,omitempty"`
	ParserVersion     *string        `json:"parserVersion,omitempty"`
	ImportStatus      string         `json:"importStatus"`
	Metadata          map[string]any `json:"metadata,omitempty"`
}

type AdminCourseSetBrief struct {
	ID             string                   `json:"id"`
	Code           string                   `json:"code"`
	Mode           string                   `json:"mode"`
	Name           string                   `json:"name"`
	Description    *string                  `json:"description,omitempty"`
	Status         string                   `json:"status"`
	CurrentVariant *AdminCourseVariantBrief `json:"currentVariant,omitempty"`
}

type AdminCourseVariantBrief struct {
	ID        string  `json:"id"`
	Name      string  `json:"name"`
	RouteCode *string `json:"routeCode,omitempty"`
	Status    string  `json:"status"`
}

type AdminCourseSetDetail struct {
	CourseSet AdminCourseSetBrief      `json:"courseSet"`
	Variants  []AdminCourseVariantView `json:"variants"`
}

type CreateAdminCourseSetInput struct {
	Code        string  `json:"code"`
	Mode        string  `json:"mode"`
	Name        string  `json:"name"`
	Description *string `json:"description,omitempty"`
	Status      string  `json:"status"`
}

type AdminCourseVariantView struct {
	ID           string         `json:"id"`
	SourceID     *string        `json:"sourceId,omitempty"`
	Name         string         `json:"name"`
	RouteCode    *string        `json:"routeCode,omitempty"`
	Mode         string         `json:"mode"`
	ControlCount *int           `json:"controlCount,omitempty"`
	Difficulty   *string        `json:"difficulty,omitempty"`
	Status       string         `json:"status"`
	IsDefault    bool           `json:"isDefault"`
	ConfigPatch  map[string]any `json:"configPatch,omitempty"`
	Metadata     map[string]any `json:"metadata,omitempty"`
}

type CreateAdminCourseVariantInput struct {
	SourceID     *string        `json:"sourceId,omitempty"`
	Name         string         `json:"name"`
	RouteCode    *string        `json:"routeCode,omitempty"`
	Mode         string         `json:"mode"`
	ControlCount *int           `json:"controlCount,omitempty"`
	Difficulty   *string        `json:"difficulty,omitempty"`
	Status       string         `json:"status"`
	IsDefault    bool           `json:"isDefault"`
	ConfigPatch  map[string]any `json:"configPatch,omitempty"`
	Metadata     map[string]any `json:"metadata,omitempty"`
}

type AdminRuntimeBindingSummary struct {
	ID              string  `json:"id"`
	EventID         string  `json:"eventId"`
	PlaceID         string  `json:"placeId"`
	MapAssetID      string  `json:"mapAssetId"`
	TileReleaseID   string  `json:"tileReleaseId"`
	CourseSetID     string  `json:"courseSetId"`
	CourseVariantID string  `json:"courseVariantId"`
	Status          string  `json:"status"`
	Notes           *string `json:"notes,omitempty"`
}

type CreateAdminRuntimeBindingInput struct {
	EventID         string  `json:"eventId"`
	PlaceID         string  `json:"placeId"`
	MapAssetID      string  `json:"mapAssetId"`
	TileReleaseID   string  `json:"tileReleaseId"`
	CourseSetID     string  `json:"courseSetId"`
	CourseVariantID string  `json:"courseVariantId"`
	Status          string  `json:"status"`
	Notes           *string `json:"notes,omitempty"`
}

func NewAdminProductionService(store *postgres.Store) *AdminProductionService {
	return &AdminProductionService{store: store}
}

func (s *AdminProductionService) ListPlaces(ctx context.Context, limit int) ([]AdminPlaceSummary, error) {
	items, err := s.store.ListPlaces(ctx, limit)
	if err != nil {
		return nil, err
	}
	result := make([]AdminPlaceSummary, 0, len(items))
	for _, item := range items {
		result = append(result, buildAdminPlaceSummary(item))
	}
	return result, nil
}

func (s *AdminProductionService) CreatePlace(ctx context.Context, input CreateAdminPlaceInput) (*AdminPlaceSummary, error) {
	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("place")
	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.CreatePlace(ctx, tx, postgres.CreatePlaceParams{
		PublicID:    publicID,
		Code:        input.Code,
		Name:        input.Name,
		Region:      trimStringPtr(input.Region),
		CoverURL:    trimStringPtr(input.CoverURL),
		Description: trimStringPtr(input.Description),
		CenterPoint: input.CenterPoint,
		Status:      normalizeCatalogStatus(input.Status),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	result := buildAdminPlaceSummary(*item)
	return &result, nil
}

func (s *AdminProductionService) GetPlaceDetail(ctx context.Context, placePublicID string) (*AdminPlaceDetail, error) {
	place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
	if err != nil {
		return nil, err
	}
	if place == nil {
		return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
	}
	mapAssets, err := s.store.ListMapAssetsByPlaceID(ctx, place.ID)
	if err != nil {
		return nil, err
	}
	result := &AdminPlaceDetail{
		Place:     buildAdminPlaceSummary(*place),
		MapAssets: make([]AdminMapAssetSummary, 0, len(mapAssets)),
	}
	for _, item := range mapAssets {
		summary, err := s.buildAdminMapAssetSummary(ctx, item)
		if err != nil {
			return nil, err
		}
		result.MapAssets = append(result.MapAssets, summary)
	}
	return result, nil
}

func (s *AdminProductionService) CreateMapAsset(ctx context.Context, placePublicID string, input CreateAdminMapAssetInput) (*AdminMapAssetSummary, error) {
	place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(placePublicID))
	if err != nil {
		return nil, err
	}
	if place == nil {
		return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
	}
	input.Code = strings.TrimSpace(input.Code)
	input.Name = strings.TrimSpace(input.Name)
	mapType := strings.TrimSpace(input.MapType)
	if mapType == "" {
		mapType = "standard"
	}
	if input.Code == "" || input.Name == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
	}

	var legacyMapID *string
	if input.LegacyMapID != nil && strings.TrimSpace(*input.LegacyMapID) != "" {
		legacyMap, err := s.store.GetResourceMapByPublicID(ctx, strings.TrimSpace(*input.LegacyMapID))
		if err != nil {
			return nil, err
		}
		if legacyMap == nil {
			return nil, apperr.New(http.StatusNotFound, "legacy_map_not_found", "legacy map not found")
		}
		legacyMapID = &legacyMap.ID
	}

	publicID, err := security.GeneratePublicID("mapasset")
	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.CreateMapAsset(ctx, tx, postgres.CreateMapAssetParams{
		PublicID:    publicID,
		PlaceID:     place.ID,
		LegacyMapID: legacyMapID,
		Code:        input.Code,
		Name:        input.Name,
		MapType:     mapType,
		CoverURL:    trimStringPtr(input.CoverURL),
		Description: trimStringPtr(input.Description),
		Status:      normalizeCatalogStatus(input.Status),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	result, err := s.buildAdminMapAssetSummary(ctx, *item)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

func (s *AdminProductionService) GetMapAssetDetail(ctx context.Context, mapAssetPublicID string) (*AdminMapAssetDetail, error) {
	item, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
	if err != nil {
		return nil, err
	}
	if item == nil {
		return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
	}
	summary, err := s.buildAdminMapAssetSummary(ctx, *item)
	if err != nil {
		return nil, err
	}
	tileReleases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
	if err != nil {
		return nil, err
	}
	courseSets, err := s.store.ListCourseSetsByMapAssetID(ctx, item.ID)
	if err != nil {
		return nil, err
	}
	result := &AdminMapAssetDetail{
		MapAsset:     summary,
		TileReleases: make([]AdminTileReleaseView, 0, len(tileReleases)),
		CourseSets:   make([]AdminCourseSetBrief, 0, len(courseSets)),
	}
	for _, release := range tileReleases {
		result.TileReleases = append(result.TileReleases, buildAdminTileReleaseView(release))
	}
	for _, courseSet := range courseSets {
		brief, err := s.buildAdminCourseSetBrief(ctx, courseSet)
		if err != nil {
			return nil, err
		}
		result.CourseSets = append(result.CourseSets, brief)
	}
	return result, nil
}

func (s *AdminProductionService) CreateTileRelease(ctx context.Context, mapAssetPublicID string, input CreateAdminTileReleaseInput) (*AdminTileReleaseView, error) {
	mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
	if err != nil {
		return nil, err
	}
	if mapAsset == nil {
		return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
	}
	input.VersionCode = strings.TrimSpace(input.VersionCode)
	input.TileBaseURL = strings.TrimSpace(input.TileBaseURL)
	input.MetaURL = strings.TrimSpace(input.MetaURL)
	if input.VersionCode == "" || input.TileBaseURL == "" || input.MetaURL == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode, tileBaseUrl and metaUrl are required")
	}

	var legacyVersionID *string
	if input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyVersionID) != "" {
		if mapAsset.LegacyMapPublicID == nil || strings.TrimSpace(*mapAsset.LegacyMapPublicID) == "" {
			return nil, apperr.New(http.StatusBadRequest, "legacy_map_missing", "map asset has no linked legacy map")
		}
		legacyVersion, err := s.store.GetResourceMapVersionByPublicID(ctx, *mapAsset.LegacyMapPublicID, strings.TrimSpace(*input.LegacyVersionID))
		if err != nil {
			return nil, err
		}
		if legacyVersion == nil {
			return nil, apperr.New(http.StatusNotFound, "legacy_tile_version_not_found", "legacy map version not found")
		}
		legacyVersionID = &legacyVersion.ID
	}

	publicID, err := security.GeneratePublicID("tile")
	if err != nil {
		return nil, err
	}
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)
	publishedAt := time.Now()
	release, err := s.store.CreateTileRelease(ctx, tx, postgres.CreateTileReleaseParams{
		PublicID:           publicID,
		MapAssetID:         mapAsset.ID,
		LegacyMapVersionID: legacyVersionID,
		VersionCode:        input.VersionCode,
		Status:             normalizeReleaseStatus(input.Status),
		TileBaseURL:        input.TileBaseURL,
		MetaURL:            input.MetaURL,
		PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
		MetadataJSON:       input.Metadata,
		PublishedAt:        &publishedAt,
	})
	if err != nil {
		return nil, err
	}
	if input.SetAsCurrent {
		if err := s.store.SetMapAssetCurrentTileRelease(ctx, tx, mapAsset.ID, release.ID); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	view := buildAdminTileReleaseView(*release)
	return &view, nil
}

func (s *AdminProductionService) ListCourseSources(ctx context.Context, limit int) ([]AdminCourseSourceSummary, error) {
	items, err := s.store.ListCourseSources(ctx, limit)
	if err != nil {
		return nil, err
	}
	result := make([]AdminCourseSourceSummary, 0, len(items))
	for _, item := range items {
		result = append(result, buildAdminCourseSourceSummary(item))
	}
	return result, nil
}

func (s *AdminProductionService) CreateCourseSource(ctx context.Context, input CreateAdminCourseSourceInput) (*AdminCourseSourceSummary, error) {
	sourceType := strings.TrimSpace(input.SourceType)
	fileURL := strings.TrimSpace(input.FileURL)
	if sourceType == "" || fileURL == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "sourceType and fileUrl are required")
	}

	var legacyPlayfieldVersionID *string
	if input.LegacyPlayfieldID != nil && input.LegacyVersionID != nil && strings.TrimSpace(*input.LegacyPlayfieldID) != "" && strings.TrimSpace(*input.LegacyVersionID) != "" {
		version, err := s.store.GetResourcePlayfieldVersionByPublicID(ctx, strings.TrimSpace(*input.LegacyPlayfieldID), strings.TrimSpace(*input.LegacyVersionID))
		if err != nil {
			return nil, err
		}
		if version == nil {
			return nil, apperr.New(http.StatusNotFound, "legacy_playfield_version_not_found", "legacy playfield version not found")
		}
		legacyPlayfieldVersionID = &version.ID
	}

	publicID, err := security.GeneratePublicID("csrc")
	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.CreateCourseSource(ctx, tx, postgres.CreateCourseSourceParams{
		PublicID:                 publicID,
		LegacyPlayfieldVersionID: legacyPlayfieldVersionID,
		SourceType:               sourceType,
		FileURL:                  fileURL,
		Checksum:                 trimStringPtr(input.Checksum),
		ParserVersion:            trimStringPtr(input.ParserVersion),
		ImportStatus:             normalizeCourseSourceStatus(input.ImportStatus),
		MetadataJSON:             input.Metadata,
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	result := buildAdminCourseSourceSummary(*item)
	return &result, nil
}

func (s *AdminProductionService) GetCourseSource(ctx context.Context, sourcePublicID string) (*AdminCourseSourceSummary, error) {
	item, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(sourcePublicID))
	if err != nil {
		return nil, err
	}
	if item == nil {
		return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
	}
	result := buildAdminCourseSourceSummary(*item)
	return &result, nil
}

func (s *AdminProductionService) CreateCourseSet(ctx context.Context, mapAssetPublicID string, input CreateAdminCourseSetInput) (*AdminCourseSetBrief, error) {
	mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(mapAssetPublicID))
	if err != nil {
		return nil, err
	}
	if mapAsset == nil {
		return nil, apperr.New(http.StatusNotFound, "map_asset_not_found", "map asset not found")
	}
	input.Code = strings.TrimSpace(input.Code)
	input.Mode = strings.TrimSpace(input.Mode)
	input.Name = strings.TrimSpace(input.Name)
	if input.Code == "" || input.Mode == "" || input.Name == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code, mode and name are required")
	}
	publicID, err := security.GeneratePublicID("cset")
	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.CreateCourseSet(ctx, tx, postgres.CreateCourseSetParams{
		PublicID:    publicID,
		PlaceID:     mapAsset.PlaceID,
		MapAssetID:  mapAsset.ID,
		Code:        input.Code,
		Mode:        input.Mode,
		Name:        input.Name,
		Description: trimStringPtr(input.Description),
		Status:      normalizeCatalogStatus(input.Status),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	brief, err := s.buildAdminCourseSetBrief(ctx, *item)
	if err != nil {
		return nil, err
	}
	return &brief, nil
}

func (s *AdminProductionService) GetCourseSetDetail(ctx context.Context, courseSetPublicID string) (*AdminCourseSetDetail, error) {
	item, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
	if err != nil {
		return nil, err
	}
	if item == nil {
		return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
	}
	brief, err := s.buildAdminCourseSetBrief(ctx, *item)
	if err != nil {
		return nil, err
	}
	variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
	if err != nil {
		return nil, err
	}
	result := &AdminCourseSetDetail{
		CourseSet: brief,
		Variants:  make([]AdminCourseVariantView, 0, len(variants)),
	}
	for _, variant := range variants {
		result.Variants = append(result.Variants, buildAdminCourseVariantView(variant))
	}
	return result, nil
}

func (s *AdminProductionService) CreateCourseVariant(ctx context.Context, courseSetPublicID string, input CreateAdminCourseVariantInput) (*AdminCourseVariantView, error) {
	courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(courseSetPublicID))
	if err != nil {
		return nil, err
	}
	if courseSet == nil {
		return nil, apperr.New(http.StatusNotFound, "course_set_not_found", "course set not found")
	}
	input.Name = strings.TrimSpace(input.Name)
	input.Mode = strings.TrimSpace(input.Mode)
	if input.Name == "" || input.Mode == "" {
		return nil, apperr.New(http.StatusBadRequest, "invalid_params", "name and mode are required")
	}

	var sourceID *string
	if input.SourceID != nil && strings.TrimSpace(*input.SourceID) != "" {
		source, err := s.store.GetCourseSourceByPublicID(ctx, strings.TrimSpace(*input.SourceID))
		if err != nil {
			return nil, err
		}
		if source == nil {
			return nil, apperr.New(http.StatusNotFound, "course_source_not_found", "course source not found")
		}
		sourceID = &source.ID
	}

	publicID, err := security.GeneratePublicID("cvar")
	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.CreateCourseVariant(ctx, tx, postgres.CreateCourseVariantParams{
		PublicID:     publicID,
		CourseSetID:  courseSet.ID,
		SourceID:     sourceID,
		Name:         input.Name,
		RouteCode:    trimStringPtr(input.RouteCode),
		Mode:         input.Mode,
		ControlCount: input.ControlCount,
		Difficulty:   trimStringPtr(input.Difficulty),
		Status:       normalizeCatalogStatus(input.Status),
		IsDefault:    input.IsDefault,
		ConfigPatch:  input.ConfigPatch,
		MetadataJSON: input.Metadata,
	})
	if err != nil {
		return nil, err
	}
	if input.IsDefault {
		if err := s.store.SetCourseSetCurrentVariant(ctx, tx, courseSet.ID, item.ID); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	view := buildAdminCourseVariantView(*item)
	return &view, nil
}

func (s *AdminProductionService) ListRuntimeBindings(ctx context.Context, limit int) ([]AdminRuntimeBindingSummary, error) {
	items, err := s.store.ListMapRuntimeBindings(ctx, limit)
	if err != nil {
		return nil, err
	}
	result := make([]AdminRuntimeBindingSummary, 0, len(items))
	for _, item := range items {
		result = append(result, buildAdminRuntimeBindingSummary(item))
	}
	return result, nil
}

func (s *AdminProductionService) CreateRuntimeBinding(ctx context.Context, input CreateAdminRuntimeBindingInput) (*AdminRuntimeBindingSummary, error) {
	eventRecord, err := s.store.GetAdminEventByPublicID(ctx, strings.TrimSpace(input.EventID))
	if err != nil {
		return nil, err
	}
	if eventRecord == nil {
		return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
	}
	place, err := s.store.GetPlaceByPublicID(ctx, strings.TrimSpace(input.PlaceID))
	if err != nil {
		return nil, err
	}
	if place == nil {
		return nil, apperr.New(http.StatusNotFound, "place_not_found", "place not found")
	}
	mapAsset, err := s.store.GetMapAssetByPublicID(ctx, strings.TrimSpace(input.MapAssetID))
	if err != nil {
		return nil, err
	}
	if mapAsset == nil || mapAsset.PlaceID != place.ID {
		return nil, apperr.New(http.StatusBadRequest, "map_asset_mismatch", "map asset does not belong to place")
	}
	tileRelease, err := s.store.GetTileReleaseByPublicID(ctx, strings.TrimSpace(input.TileReleaseID))
	if err != nil {
		return nil, err
	}
	if tileRelease == nil || tileRelease.MapAssetID != mapAsset.ID {
		return nil, apperr.New(http.StatusBadRequest, "tile_release_mismatch", "tile release does not belong to map asset")
	}
	courseSet, err := s.store.GetCourseSetByPublicID(ctx, strings.TrimSpace(input.CourseSetID))
	if err != nil {
		return nil, err
	}
	if courseSet == nil || courseSet.PlaceID != place.ID || courseSet.MapAssetID != mapAsset.ID {
		return nil, apperr.New(http.StatusBadRequest, "course_set_mismatch", "course set does not match place/map asset")
	}
	courseVariant, err := s.store.GetCourseVariantByPublicID(ctx, strings.TrimSpace(input.CourseVariantID))
	if err != nil {
		return nil, err
	}
	if courseVariant == nil || courseVariant.CourseSetID != courseSet.ID {
		return nil, apperr.New(http.StatusBadRequest, "course_variant_mismatch", "course variant does not belong to course set")
	}

	publicID, err := security.GeneratePublicID("rtbind")
	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.CreateMapRuntimeBinding(ctx, tx, postgres.CreateMapRuntimeBindingParams{
		PublicID:        publicID,
		EventID:         eventRecord.ID,
		PlaceID:         place.ID,
		MapAssetID:      mapAsset.ID,
		TileReleaseID:   tileRelease.ID,
		CourseSetID:     courseSet.ID,
		CourseVariantID: courseVariant.ID,
		Status:          normalizeRuntimeBindingStatus(input.Status),
		Notes:           trimStringPtr(input.Notes),
	})
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	created, err := s.store.GetMapRuntimeBindingByPublicID(ctx, item.PublicID)
	if err != nil {
		return nil, err
	}
	if created == nil {
		return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
	}
	result := buildAdminRuntimeBindingSummary(*created)
	return &result, nil
}

func (s *AdminProductionService) GetRuntimeBinding(ctx context.Context, runtimeBindingPublicID string) (*AdminRuntimeBindingSummary, error) {
	item, err := s.store.GetMapRuntimeBindingByPublicID(ctx, strings.TrimSpace(runtimeBindingPublicID))
	if err != nil {
		return nil, err
	}
	if item == nil {
		return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
	}
	result := buildAdminRuntimeBindingSummary(*item)
	return &result, nil
}

func (s *AdminProductionService) buildAdminMapAssetSummary(ctx context.Context, item postgres.MapAsset) (AdminMapAssetSummary, error) {
	result := AdminMapAssetSummary{
		ID:          item.PublicID,
		PlaceID:     item.PlaceID,
		LegacyMapID: item.LegacyMapPublicID,
		Code:        item.Code,
		Name:        item.Name,
		MapType:     item.MapType,
		CoverURL:    item.CoverURL,
		Description: item.Description,
		Status:      item.Status,
	}
	if item.CurrentTileReleaseID != nil {
		releases, err := s.store.ListTileReleasesByMapAssetID(ctx, item.ID)
		if err != nil {
			return result, err
		}
		for _, release := range releases {
			if release.ID == *item.CurrentTileReleaseID {
				result.CurrentTileRelease = &AdminTileReleaseBrief{
					ID:          release.PublicID,
					VersionCode: release.VersionCode,
					Status:      release.Status,
				}
				break
			}
		}
	}
	return result, nil
}

func (s *AdminProductionService) buildAdminCourseSetBrief(ctx context.Context, item postgres.CourseSet) (AdminCourseSetBrief, error) {
	result := AdminCourseSetBrief{
		ID:          item.PublicID,
		Code:        item.Code,
		Mode:        item.Mode,
		Name:        item.Name,
		Description: item.Description,
		Status:      item.Status,
	}
	if item.CurrentVariantID != nil {
		variants, err := s.store.ListCourseVariantsByCourseSetID(ctx, item.ID)
		if err != nil {
			return result, err
		}
		for _, variant := range variants {
			if variant.ID == *item.CurrentVariantID {
				result.CurrentVariant = &AdminCourseVariantBrief{
					ID:        variant.PublicID,
					Name:      variant.Name,
					RouteCode: variant.RouteCode,
					Status:    variant.Status,
				}
				break
			}
		}
	}
	return result, nil
}

func buildAdminPlaceSummary(item postgres.Place) AdminPlaceSummary {
	return AdminPlaceSummary{
		ID:          item.PublicID,
		Code:        item.Code,
		Name:        item.Name,
		Region:      item.Region,
		CoverURL:    item.CoverURL,
		Description: item.Description,
		CenterPoint: decodeJSONMap(item.CenterPoint),
		Status:      item.Status,
	}
}

func buildAdminTileReleaseView(item postgres.TileRelease) AdminTileReleaseView {
	return AdminTileReleaseView{
		ID:                 item.PublicID,
		LegacyVersionID:    item.LegacyMapVersionPub,
		VersionCode:        item.VersionCode,
		Status:             item.Status,
		TileBaseURL:        item.TileBaseURL,
		MetaURL:            item.MetaURL,
		PublishedAssetRoot: item.PublishedAssetRoot,
		Metadata:           decodeJSONMap(item.MetadataJSON),
		PublishedAt:        item.PublishedAt,
	}
}

func buildAdminCourseSourceSummary(item postgres.CourseSource) AdminCourseSourceSummary {
	return AdminCourseSourceSummary{
		ID:              item.PublicID,
		LegacyVersionID: item.LegacyPlayfieldVersionPub,
		SourceType:      item.SourceType,
		FileURL:         item.FileURL,
		Checksum:        item.Checksum,
		ParserVersion:   item.ParserVersion,
		ImportStatus:    item.ImportStatus,
		Metadata:        decodeJSONMap(item.MetadataJSON),
		ImportedAt:      item.ImportedAt,
	}
}

func buildAdminCourseVariantView(item postgres.CourseVariant) AdminCourseVariantView {
	return AdminCourseVariantView{
		ID:           item.PublicID,
		SourceID:     item.SourcePublicID,
		Name:         item.Name,
		RouteCode:    item.RouteCode,
		Mode:         item.Mode,
		ControlCount: item.ControlCount,
		Difficulty:   item.Difficulty,
		Status:       item.Status,
		IsDefault:    item.IsDefault,
		ConfigPatch:  decodeJSONMap(item.ConfigPatch),
		Metadata:     decodeJSONMap(item.MetadataJSON),
	}
}

func buildAdminRuntimeBindingSummary(item postgres.MapRuntimeBinding) AdminRuntimeBindingSummary {
	return AdminRuntimeBindingSummary{
		ID:              item.PublicID,
		EventID:         item.EventPublicID,
		PlaceID:         item.PlacePublicID,
		MapAssetID:      item.MapAssetPublicID,
		TileReleaseID:   item.TileReleasePublicID,
		CourseSetID:     item.CourseSetPublicID,
		CourseVariantID: item.CourseVariantPublicID,
		Status:          item.Status,
		Notes:           item.Notes,
	}
}

func normalizeCourseSourceStatus(value string) string {
	switch strings.TrimSpace(value) {
	case "draft":
		return "draft"
	case "parsed":
		return "parsed"
	case "failed":
		return "failed"
	case "archived":
		return "archived"
	default:
		return "imported"
	}
}

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

func normalizeReleaseStatus(value string) string {
	switch strings.TrimSpace(value) {
	case "active":
		return "active"
	case "published":
		return "published"
	case "retired":
		return "retired"
	case "archived":
		return "archived"
	default:
		return "draft"
	}
}
