package postgres

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
)

type Place struct {
	ID          string
	PublicID    string
	Code        string
	Name        string
	Region      *string
	CoverURL    *string
	Description *string
	CenterPoint json.RawMessage
	Status      string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type MapAsset struct {
	ID                   string
	PublicID             string
	PlaceID              string
	LegacyMapID          *string
	LegacyMapPublicID    *string
	Code                 string
	Name                 string
	MapType              string
	CoverURL             *string
	Description          *string
	Status               string
	CurrentTileReleaseID *string
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

type TileRelease struct {
	ID                  string
	PublicID            string
	MapAssetID          string
	LegacyMapVersionID  *string
	LegacyMapVersionPub *string
	VersionCode         string
	Status              string
	TileBaseURL         string
	MetaURL             string
	PublishedAssetRoot  *string
	MetadataJSON        json.RawMessage
	PublishedAt         *time.Time
	CreatedAt           time.Time
	UpdatedAt           time.Time
}

type CourseSource struct {
	ID                        string
	PublicID                  string
	LegacyPlayfieldVersionID  *string
	LegacyPlayfieldVersionPub *string
	SourceType                string
	FileURL                   string
	Checksum                  *string
	ParserVersion             *string
	ImportStatus              string
	MetadataJSON              json.RawMessage
	ImportedAt                time.Time
	CreatedAt                 time.Time
	UpdatedAt                 time.Time
}

type CourseSet struct {
	ID               string
	PublicID         string
	PlaceID          string
	MapAssetID       string
	Code             string
	Mode             string
	Name             string
	Description      *string
	Status           string
	CurrentVariantID *string
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

type CourseVariant struct {
	ID             string
	PublicID       string
	CourseSetID    string
	SourceID       *string
	SourcePublicID *string
	Name           string
	RouteCode      *string
	Mode           string
	ControlCount   *int
	Difficulty     *string
	Status         string
	IsDefault      bool
	ConfigPatch    json.RawMessage
	MetadataJSON   json.RawMessage
	CreatedAt      time.Time
	UpdatedAt      time.Time
}

type MapRuntimeBinding struct {
	ID                    string
	PublicID              string
	EventID               string
	EventPublicID         string
	PlaceID               string
	PlacePublicID         string
	MapAssetID            string
	MapAssetPublicID      string
	TileReleaseID         string
	TileReleasePublicID   string
	CourseSetID           string
	CourseSetPublicID     string
	CourseVariantID       string
	CourseVariantPublicID string
	Status                string
	Notes                 *string
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

type CreatePlaceParams struct {
	PublicID    string
	Code        string
	Name        string
	Region      *string
	CoverURL    *string
	Description *string
	CenterPoint map[string]any
	Status      string
}

type CreateMapAssetParams struct {
	PublicID    string
	PlaceID     string
	LegacyMapID *string
	Code        string
	Name        string
	MapType     string
	CoverURL    *string
	Description *string
	Status      string
}

type CreateTileReleaseParams struct {
	PublicID           string
	MapAssetID         string
	LegacyMapVersionID *string
	VersionCode        string
	Status             string
	TileBaseURL        string
	MetaURL            string
	PublishedAssetRoot *string
	MetadataJSON       map[string]any
	PublishedAt        *time.Time
}

type CreateCourseSourceParams struct {
	PublicID                 string
	LegacyPlayfieldVersionID *string
	SourceType               string
	FileURL                  string
	Checksum                 *string
	ParserVersion            *string
	ImportStatus             string
	MetadataJSON             map[string]any
	ImportedAt               *time.Time
}

type CreateCourseSetParams struct {
	PublicID    string
	PlaceID     string
	MapAssetID  string
	Code        string
	Mode        string
	Name        string
	Description *string
	Status      string
}

type CreateCourseVariantParams struct {
	PublicID     string
	CourseSetID  string
	SourceID     *string
	Name         string
	RouteCode    *string
	Mode         string
	ControlCount *int
	Difficulty   *string
	Status       string
	IsDefault    bool
	ConfigPatch  map[string]any
	MetadataJSON map[string]any
}

type CreateMapRuntimeBindingParams struct {
	PublicID        string
	EventID         string
	PlaceID         string
	MapAssetID      string
	TileReleaseID   string
	CourseSetID     string
	CourseVariantID string
	Status          string
	Notes           *string
}

func (s *Store) ListPlaces(ctx context.Context, limit int) ([]Place, error) {
	if limit <= 0 || limit > 200 {
		limit = 50
	}
	rows, err := s.pool.Query(ctx, `
		SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
		FROM places
		ORDER BY created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, fmt.Errorf("list places: %w", err)
	}
	defer rows.Close()
	items := []Place{}
	for rows.Next() {
		item, err := scanPlaceFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate places: %w", err)
	}
	return items, nil
}

func (s *Store) GetPlaceByPublicID(ctx context.Context, publicID string) (*Place, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
		FROM places
		WHERE place_public_id = $1
		LIMIT 1
	`, publicID)
	return scanPlace(row)
}

func (s *Store) CreatePlace(ctx context.Context, tx Tx, params CreatePlaceParams) (*Place, error) {
	centerPointJSON, err := marshalJSONMap(params.CenterPoint)
	if err != nil {
		return nil, fmt.Errorf("marshal place center point: %w", err)
	}
	row := tx.QueryRow(ctx, `
		INSERT INTO places (place_public_id, code, name, region, cover_url, description, center_point_jsonb, status)
		VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
		RETURNING id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
	`, params.PublicID, params.Code, params.Name, params.Region, params.CoverURL, params.Description, centerPointJSON, params.Status)
	return scanPlace(row)
}

func (s *Store) ListMapAssetsByPlaceID(ctx context.Context, placeID string) ([]MapAsset, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
		       ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
		FROM map_assets ma
		LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
		WHERE ma.place_id = $1
		ORDER BY ma.created_at DESC
	`, placeID)
	if err != nil {
		return nil, fmt.Errorf("list map assets: %w", err)
	}
	defer rows.Close()
	items := []MapAsset{}
	for rows.Next() {
		item, err := scanMapAssetFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate map assets: %w", err)
	}
	return items, nil
}

func (s *Store) GetMapAssetByPublicID(ctx context.Context, publicID string) (*MapAsset, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
		       ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
		FROM map_assets ma
		LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
		WHERE ma.map_asset_public_id = $1
		LIMIT 1
	`, publicID)
	return scanMapAsset(row)
}

func (s *Store) CreateMapAsset(ctx context.Context, tx Tx, params CreateMapAssetParams) (*MapAsset, error) {
	row := tx.QueryRow(ctx, `
		INSERT INTO map_assets (map_asset_public_id, place_id, legacy_map_id, code, name, map_type, cover_url, description, status)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		RETURNING id, map_asset_public_id, place_id, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
	`, params.PublicID, params.PlaceID, params.LegacyMapID, params.Code, params.Name, params.MapType, params.CoverURL, params.Description, params.Status)
	return scanMapAsset(row)
}

func (s *Store) ListTileReleasesByMapAssetID(ctx context.Context, mapAssetID string) ([]TileRelease, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
		       tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
		       tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
		FROM tile_releases tr
		LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
		WHERE tr.map_asset_id = $1
		ORDER BY tr.created_at DESC
	`, mapAssetID)
	if err != nil {
		return nil, fmt.Errorf("list tile releases: %w", err)
	}
	defer rows.Close()
	items := []TileRelease{}
	for rows.Next() {
		item, err := scanTileReleaseFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate tile releases: %w", err)
	}
	return items, nil
}

func (s *Store) GetTileReleaseByPublicID(ctx context.Context, publicID string) (*TileRelease, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
		       tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
		       tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
		FROM tile_releases tr
		LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
		WHERE tr.tile_release_public_id = $1
		LIMIT 1
	`, publicID)
	return scanTileRelease(row)
}

func (s *Store) CreateTileRelease(ctx context.Context, tx Tx, params CreateTileReleaseParams) (*TileRelease, error) {
	metadataJSON, err := marshalJSONMap(params.MetadataJSON)
	if err != nil {
		return nil, fmt.Errorf("marshal tile release metadata: %w", err)
	}
	row := tx.QueryRow(ctx, `
		INSERT INTO tile_releases (
			tile_release_public_id, map_asset_id, legacy_map_version_id, version_code, status,
			tile_base_url, meta_url, published_asset_root, metadata_jsonb, published_at
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)
		RETURNING id, tile_release_public_id, map_asset_id, legacy_map_version_id, NULL::text, version_code, status,
		          tile_base_url, meta_url, published_asset_root, metadata_jsonb::text, published_at, created_at, updated_at
	`, params.PublicID, params.MapAssetID, params.LegacyMapVersionID, params.VersionCode, params.Status, params.TileBaseURL, params.MetaURL, params.PublishedAssetRoot, metadataJSON, params.PublishedAt)
	return scanTileRelease(row)
}

func (s *Store) SetMapAssetCurrentTileRelease(ctx context.Context, tx Tx, mapAssetID, tileReleaseID string) error {
	_, err := tx.Exec(ctx, `UPDATE map_assets SET current_tile_release_id = $2 WHERE id = $1`, mapAssetID, tileReleaseID)
	if err != nil {
		return fmt.Errorf("set map asset current tile release: %w", err)
	}
	return nil
}

func (s *Store) ListCourseSources(ctx context.Context, limit int) ([]CourseSource, error) {
	if limit <= 0 || limit > 200 {
		limit = 50
	}
	rows, err := s.pool.Query(ctx, `
		SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
		       cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
		FROM course_sources cs
		LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
		ORDER BY cs.created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, fmt.Errorf("list course sources: %w", err)
	}
	defer rows.Close()
	items := []CourseSource{}
	for rows.Next() {
		item, err := scanCourseSourceFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate course sources: %w", err)
	}
	return items, nil
}

func (s *Store) GetCourseSourceByPublicID(ctx context.Context, publicID string) (*CourseSource, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
		       cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
		FROM course_sources cs
		LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
		WHERE cs.course_source_public_id = $1
		LIMIT 1
	`, publicID)
	return scanCourseSource(row)
}

func (s *Store) CreateCourseSource(ctx context.Context, tx Tx, params CreateCourseSourceParams) (*CourseSource, error) {
	metadataJSON, err := marshalJSONMap(params.MetadataJSON)
	if err != nil {
		return nil, fmt.Errorf("marshal course source metadata: %w", err)
	}
	importedAt := time.Now()
	if params.ImportedAt != nil {
		importedAt = *params.ImportedAt
	}
	row := tx.QueryRow(ctx, `
		INSERT INTO course_sources (
			course_source_public_id, legacy_playfield_version_id, source_type, file_url, checksum,
			parser_version, import_status, metadata_jsonb, imported_at
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
		RETURNING id, course_source_public_id, legacy_playfield_version_id, NULL::text, source_type, file_url,
		          checksum, parser_version, import_status, metadata_jsonb::text, imported_at, created_at, updated_at
	`, params.PublicID, params.LegacyPlayfieldVersionID, params.SourceType, params.FileURL, params.Checksum, params.ParserVersion, params.ImportStatus, metadataJSON, importedAt)
	return scanCourseSource(row)
}

func (s *Store) ListCourseSets(ctx context.Context, limit int) ([]CourseSet, error) {
	if limit <= 0 || limit > 200 {
		limit = 50
	}
	rows, err := s.pool.Query(ctx, `
		SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
		FROM course_sets
		ORDER BY created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, fmt.Errorf("list course sets: %w", err)
	}
	defer rows.Close()
	items := []CourseSet{}
	for rows.Next() {
		item, err := scanCourseSetFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate course sets: %w", err)
	}
	return items, nil
}

func (s *Store) ListCourseSetsByMapAssetID(ctx context.Context, mapAssetID string) ([]CourseSet, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
		FROM course_sets
		WHERE map_asset_id = $1
		ORDER BY created_at DESC
	`, mapAssetID)
	if err != nil {
		return nil, fmt.Errorf("list course sets by map asset: %w", err)
	}
	defer rows.Close()
	items := []CourseSet{}
	for rows.Next() {
		item, err := scanCourseSetFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate course sets by map asset: %w", err)
	}
	return items, nil
}

func (s *Store) GetCourseSetByPublicID(ctx context.Context, publicID string) (*CourseSet, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
		FROM course_sets
		WHERE course_set_public_id = $1
		LIMIT 1
	`, publicID)
	return scanCourseSet(row)
}

func (s *Store) CreateCourseSet(ctx context.Context, tx Tx, params CreateCourseSetParams) (*CourseSet, error) {
	row := tx.QueryRow(ctx, `
		INSERT INTO course_sets (course_set_public_id, place_id, map_asset_id, code, mode, name, description, status)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
		RETURNING id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
	`, params.PublicID, params.PlaceID, params.MapAssetID, params.Code, params.Mode, params.Name, params.Description, params.Status)
	return scanCourseSet(row)
}

func (s *Store) ListCourseVariantsByCourseSetID(ctx context.Context, courseSetID string) ([]CourseVariant, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
		       cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
		       cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
		FROM course_variants cv
		LEFT JOIN course_sources cs ON cs.id = cv.source_id
		WHERE cv.course_set_id = $1
		ORDER BY cv.created_at DESC
	`, courseSetID)
	if err != nil {
		return nil, fmt.Errorf("list course variants: %w", err)
	}
	defer rows.Close()
	items := []CourseVariant{}
	for rows.Next() {
		item, err := scanCourseVariantFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate course variants: %w", err)
	}
	return items, nil
}

func (s *Store) GetCourseVariantByPublicID(ctx context.Context, publicID string) (*CourseVariant, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
		       cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
		       cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
		FROM course_variants cv
		LEFT JOIN course_sources cs ON cs.id = cv.source_id
		WHERE cv.course_variant_public_id = $1
		LIMIT 1
	`, publicID)
	return scanCourseVariant(row)
}

func (s *Store) CreateCourseVariant(ctx context.Context, tx Tx, params CreateCourseVariantParams) (*CourseVariant, error) {
	configPatchJSON, err := marshalJSONMap(params.ConfigPatch)
	if err != nil {
		return nil, fmt.Errorf("marshal course variant config patch: %w", err)
	}
	metadataJSON, err := marshalJSONMap(params.MetadataJSON)
	if err != nil {
		return nil, fmt.Errorf("marshal course variant metadata: %w", err)
	}
	row := tx.QueryRow(ctx, `
		INSERT INTO course_variants (
			course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count,
			difficulty, status, is_default, config_patch_jsonb, metadata_jsonb
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb)
		RETURNING id, course_variant_public_id, course_set_id, source_id, NULL::text, name, route_code, mode,
		          control_count, difficulty, status, is_default, config_patch_jsonb::text, metadata_jsonb::text, created_at, updated_at
	`, params.PublicID, params.CourseSetID, params.SourceID, params.Name, params.RouteCode, params.Mode, params.ControlCount, params.Difficulty, params.Status, params.IsDefault, configPatchJSON, metadataJSON)
	return scanCourseVariant(row)
}

func (s *Store) SetCourseSetCurrentVariant(ctx context.Context, tx Tx, courseSetID, variantID string) error {
	_, err := tx.Exec(ctx, `UPDATE course_sets SET current_variant_id = $2 WHERE id = $1`, courseSetID, variantID)
	if err != nil {
		return fmt.Errorf("set course set current variant: %w", err)
	}
	return nil
}

func (s *Store) ListMapRuntimeBindings(ctx context.Context, limit int) ([]MapRuntimeBinding, error) {
	if limit <= 0 || limit > 200 {
		limit = 50
	}
	rows, err := s.pool.Query(ctx, `
		SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
		       mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
		       mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
		       mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
		FROM map_runtime_bindings mrb
		JOIN events e ON e.id = mrb.event_id
		JOIN places p ON p.id = mrb.place_id
		JOIN map_assets ma ON ma.id = mrb.map_asset_id
		JOIN tile_releases tr ON tr.id = mrb.tile_release_id
		JOIN course_sets cset ON cset.id = mrb.course_set_id
		JOIN course_variants cv ON cv.id = mrb.course_variant_id
		ORDER BY mrb.created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, fmt.Errorf("list runtime bindings: %w", err)
	}
	defer rows.Close()
	items := []MapRuntimeBinding{}
	for rows.Next() {
		item, err := scanMapRuntimeBindingFromRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, *item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate runtime bindings: %w", err)
	}
	return items, nil
}

func (s *Store) GetMapRuntimeBindingByPublicID(ctx context.Context, publicID string) (*MapRuntimeBinding, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
		       mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
		       mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
		       mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
		FROM map_runtime_bindings mrb
		JOIN events e ON e.id = mrb.event_id
		JOIN places p ON p.id = mrb.place_id
		JOIN map_assets ma ON ma.id = mrb.map_asset_id
		JOIN tile_releases tr ON tr.id = mrb.tile_release_id
		JOIN course_sets cset ON cset.id = mrb.course_set_id
		JOIN course_variants cv ON cv.id = mrb.course_variant_id
		WHERE mrb.runtime_binding_public_id = $1
		LIMIT 1
	`, publicID)
	return scanMapRuntimeBinding(row)
}

func (s *Store) CreateMapRuntimeBinding(ctx context.Context, tx Tx, params CreateMapRuntimeBindingParams) (*MapRuntimeBinding, error) {
	row := tx.QueryRow(ctx, `
		INSERT INTO map_runtime_bindings (
			runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		RETURNING id, runtime_binding_public_id, event_id, ''::text, place_id, ''::text, map_asset_id, ''::text,
		          tile_release_id, ''::text, course_set_id, ''::text, course_variant_id, ''::text,
		          status, notes, created_at, updated_at
	`, params.PublicID, params.EventID, params.PlaceID, params.MapAssetID, params.TileReleaseID, params.CourseSetID, params.CourseVariantID, params.Status, params.Notes)
	return scanMapRuntimeBinding(row)
}

func scanPlace(row pgx.Row) (*Place, error) {
	var item Place
	var centerPoint string
	err := row.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, &centerPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan place: %w", err)
	}
	item.CenterPoint = json.RawMessage(centerPoint)
	return &item, nil
}

func scanPlaceFromRows(rows pgx.Rows) (*Place, error) {
	var item Place
	var centerPoint string
	err := rows.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, &centerPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan place row: %w", err)
	}
	item.CenterPoint = json.RawMessage(centerPoint)
	return &item, nil
}

func scanMapAsset(row pgx.Row) (*MapAsset, error) {
	var item MapAsset
	err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan map asset: %w", err)
	}
	return &item, nil
}

func scanMapAssetFromRows(rows pgx.Rows) (*MapAsset, error) {
	var item MapAsset
	err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan map asset row: %w", err)
	}
	return &item, nil
}

func scanTileRelease(row pgx.Row) (*TileRelease, error) {
	var item TileRelease
	var metadataJSON string
	err := row.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan tile release: %w", err)
	}
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanTileReleaseFromRows(rows pgx.Rows) (*TileRelease, error) {
	var item TileRelease
	var metadataJSON string
	err := rows.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan tile release row: %w", err)
	}
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanCourseSource(row pgx.Row) (*CourseSource, error) {
	var item CourseSource
	var metadataJSON string
	err := row.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan course source: %w", err)
	}
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanCourseSourceFromRows(rows pgx.Rows) (*CourseSource, error) {
	var item CourseSource
	var metadataJSON string
	err := rows.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan course source row: %w", err)
	}
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanCourseSet(row pgx.Row) (*CourseSet, error) {
	var item CourseSet
	err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan course set: %w", err)
	}
	return &item, nil
}

func scanCourseSetFromRows(rows pgx.Rows) (*CourseSet, error) {
	var item CourseSet
	err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan course set row: %w", err)
	}
	return &item, nil
}

func scanCourseVariant(row pgx.Row) (*CourseVariant, error) {
	var item CourseVariant
	var configPatch string
	var metadataJSON string
	err := row.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan course variant: %w", err)
	}
	item.ConfigPatch = json.RawMessage(configPatch)
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanCourseVariantFromRows(rows pgx.Rows) (*CourseVariant, error) {
	var item CourseVariant
	var configPatch string
	var metadataJSON string
	err := rows.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan course variant row: %w", err)
	}
	item.ConfigPatch = json.RawMessage(configPatch)
	item.MetadataJSON = json.RawMessage(metadataJSON)
	return &item, nil
}

func scanMapRuntimeBinding(row pgx.Row) (*MapRuntimeBinding, error) {
	var item MapRuntimeBinding
	err := row.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("scan runtime binding: %w", err)
	}
	return &item, nil
}

func scanMapRuntimeBindingFromRows(rows pgx.Rows) (*MapRuntimeBinding, error) {
	var item MapRuntimeBinding
	err := rows.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan runtime binding row: %w", err)
	}
	return &item, nil
}
