// Package mysql 实现 room-service 的 MySQL 持久化边界。
package mysql

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	roomservice "hyapp/services/room-service/internal/room/service"
)

// SaveRoomMeta 插入房间元数据。
func (r *Repository) SaveRoomMeta(ctx context.Context, meta roomservice.RoomMeta) error {
	// rooms 表只保存恢复初始状态需要的元数据，实时态以 snapshot/command log 为准。
	appCode := normalizedRecordAppCode(ctx, meta.AppCode)
	nowMS := time.Now().UTC().UnixMilli()
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO rooms (app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id, created_at_ms, updated_at_ms)
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		appCode,
		meta.RoomID,
		meta.RoomShortID,
		meta.OwnerUserID,
		meta.SeatCount,
		meta.Mode,
		meta.Status,
		meta.RoomPasswordHash,
		meta.VisibleRegionID,
		nowMS,
		nowMS,
	)

	return mapRoomMetaDuplicateError(err)
}

// GetRoomMeta 查询房间元数据。
func (r *Repository) GetRoomMeta(ctx context.Context, roomID string) (roomservice.RoomMeta, bool, error) {
	// meta 是无内存 Cell 恢复的入口；不存在时上层返回 room not found。
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
		 FROM rooms
		 WHERE app_code = ? AND room_id = ?`,
		appcode.FromContext(ctx),
		roomID,
	)

	var meta roomservice.RoomMeta
	if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			// 未创建房间不是数据库错误，用 exists=false 表达。
			return roomservice.RoomMeta{}, false, nil
		}

		return roomservice.RoomMeta{}, false, err
	}

	return meta, true, nil
}

// GetRoomMetaByGlobalID 从全局 room_id 解析房间所属 App。
// 该查询只允许可信内部回调使用；uk_rooms_room_id 同时保证查询命中索引并阻止跨 App 歧义。
func (r *Repository) GetRoomMetaByGlobalID(ctx context.Context, roomID string) (roomservice.RoomMeta, bool, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
		 FROM rooms
		 WHERE room_id = ?
		 LIMIT 1`,
		strings.TrimSpace(roomID),
	)

	var meta roomservice.RoomMeta
	if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomMeta{}, false, nil
		}
		return roomservice.RoomMeta{}, false, err
	}
	return meta, true, nil
}

// GetRoomMetaByShortID 通过展示短房号解析内部房间 ID。
func (r *Repository) GetRoomMetaByShortID(ctx context.Context, roomShortID string) (roomservice.RoomMeta, bool, error) {
	// rooms.uk_rooms_short_id 保证同 app 下短房号唯一；这里只做解析，不改变 GetRoomMeta 的精确 room_id 语义。
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
		 FROM rooms
		 WHERE app_code = ? AND room_short_id = ?
		 LIMIT 1`,
		appcode.FromContext(ctx),
		strings.TrimSpace(roomShortID),
	)

	var meta roomservice.RoomMeta
	if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomMeta{}, false, nil
		}

		return roomservice.RoomMeta{}, false, err
	}

	return meta, true, nil
}

// GetRoomMetaByOwner 查询用户已经创建的房间。
func (r *Repository) GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) (roomservice.RoomMeta, bool, error) {
	// rooms.owner_user_id 有唯一约束，查询最多返回一条；该读路径用于 CreateRoom 前置失败。
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, room_password_hash, visible_region_id
		 FROM rooms
		 WHERE app_code = ? AND owner_user_id = ?
		 LIMIT 1`,
		appcode.FromContext(ctx),
		ownerUserID,
	)

	var meta roomservice.RoomMeta
	if err := row.Scan(&meta.AppCode, &meta.RoomID, &meta.RoomShortID, &meta.OwnerUserID, &meta.SeatCount, &meta.Mode, &meta.Status, &meta.RoomPasswordHash, &meta.VisibleRegionID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomMeta{}, false, nil
		}

		return roomservice.RoomMeta{}, false, err
	}

	return meta, true, nil
}

// SaveRoomBackground 保存房间背景图素材。command_id 是素材保存幂等键；URL 唯一键只负责去重，
// 不能把相同 command_id 的另一份媒体悄悄折叠成旧记录。
func (r *Repository) SaveRoomBackground(ctx context.Context, background roomservice.RoomBackgroundImage) (roomservice.RoomBackgroundImage, error) {
	appCode := normalizedRecordAppCode(ctx, background.AppCode)
	roomID := strings.TrimSpace(background.RoomID)
	imageURL := strings.TrimSpace(background.ImageURL)
	nowMS := background.CreatedAtMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}

	if existing, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID); err != nil {
		return roomservice.RoomBackgroundImage{}, err
	} else if exists {
		if !sameRoomBackgroundMedia(existing, background) {
			return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict
		}
		return existing, nil
	}
	if strings.TrimSpace(background.Media.ObjectKey) == "" {
		// legacy_timed 以 URL 作为历史幂等键；迁移前的行没有 command_id/media，重复保存必须继续返回原素材。
		if existing, exists, err := r.getRoomBackgroundByURL(ctx, appCode, roomID, imageURL); err != nil {
			return roomservice.RoomBackgroundImage{}, err
		} else if exists {
			return existing, nil
		}
	}

	_, err := r.db.ExecContext(ctx, `
		INSERT INTO room_background_images (
		  app_code, room_id, image_url, command_id, object_key, content_type, media_format,
		  size_bytes, width, height, animated, frame_count, duration_ms, poster_url,
		  thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE background_id = LAST_INSERT_ID(background_id)
	`, appCode, roomID, imageURL, strings.TrimSpace(background.CommandID), background.Media.ObjectKey,
		background.Media.ContentType, background.Media.Format, background.Media.SizeBytes, background.Media.Width,
		background.Media.Height, background.Media.Animated, background.Media.FrameCount, background.Media.DurationMS,
		background.Media.PosterURL, background.Media.ThumbnailURL, background.Media.SHA256, background.Media.Status,
		background.CreatedByUserID, nowMS, nowMS)
	if err != nil {
		return roomservice.RoomBackgroundImage{}, err
	}

	saved, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID)
	if err != nil {
		return roomservice.RoomBackgroundImage{}, err
	}
	if !exists || !sameRoomBackgroundMedia(saved, background) {
		return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict
	}
	return saved, nil
}

// GetRoomBackground 精确读取一个房间背景图素材。
func (r *Repository) GetRoomBackground(ctx context.Context, roomID string, backgroundID int64) (roomservice.RoomBackgroundImage, bool, error) {
	row := r.db.QueryRowContext(ctx, `
		SELECT `+roomBackgroundSelectColumns+`
		FROM room_background_images
		WHERE app_code = ? AND room_id = ? AND background_id = ?
		LIMIT 1
	`, appcode.FromContext(ctx), strings.TrimSpace(roomID), backgroundID)

	background, err := scanRoomBackground(row)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return roomservice.RoomBackgroundImage{}, false, nil
		}
		return roomservice.RoomBackgroundImage{}, false, err
	}
	return background, true, nil
}

// GetRoomBackgroundByCommand 供 service 在任何可变权限/房态检查之前重放已保存素材。
// command_id 唯一索引保证最多一行；app_code 继续从已验签 RequestMeta context 读取，避免跨租户重放。
func (r *Repository) GetRoomBackgroundByCommand(ctx context.Context, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) {
	return r.getRoomBackgroundByCommand(ctx, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID))
}

// ListRoomBackgrounds 返回某个房间保存过的背景图素材，按最近保存优先。
func (r *Repository) ListRoomBackgrounds(ctx context.Context, roomID string) ([]roomservice.RoomBackgroundImage, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT `+roomBackgroundSelectColumns+`
		FROM room_background_images
		WHERE app_code = ? AND room_id = ?
		ORDER BY created_at_ms DESC, background_id DESC
	`, appcode.FromContext(ctx), strings.TrimSpace(roomID))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	backgrounds := []roomservice.RoomBackgroundImage{}
	for rows.Next() {
		background, err := scanRoomBackground(rows)
		if err != nil {
			return nil, err
		}
		backgrounds = append(backgrounds, background)
	}
	return backgrounds, rows.Err()
}

func scanRoomBackground(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomBackgroundImage, error) {
	var background roomservice.RoomBackgroundImage
	err := scanner.Scan(
		&background.AppCode,
		&background.BackgroundID,
		&background.RoomID,
		&background.ImageURL,
		&background.CommandID,
		&background.Media.ObjectKey,
		&background.Media.ContentType,
		&background.Media.Format,
		&background.Media.SizeBytes,
		&background.Media.Width,
		&background.Media.Height,
		&background.Media.Animated,
		&background.Media.FrameCount,
		&background.Media.DurationMS,
		&background.Media.PosterURL,
		&background.Media.ThumbnailURL,
		&background.Media.SHA256,
		&background.Media.Status,
		&background.CreatedByUserID,
		&background.CreatedAtMS,
		&background.UpdatedAtMS,
	)
	return background, err
}

const roomBackgroundSelectColumns = `app_code, background_id, room_id, image_url, COALESCE(command_id, ''),
	object_key, content_type, media_format, size_bytes, width, height, animated, frame_count,
	duration_ms, poster_url, thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms`

func (r *Repository) getRoomBackgroundByCommand(ctx context.Context, appCode string, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) {
	row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+`
		FROM room_background_images WHERE app_code = ? AND room_id = ? AND command_id = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(commandID))
	background, err := scanRoomBackground(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.RoomBackgroundImage{}, false, nil
	}
	return background, err == nil, err
}

func (r *Repository) getRoomBackgroundByURL(ctx context.Context, appCode string, roomID string, imageURL string) (roomservice.RoomBackgroundImage, bool, error) {
	row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+`
		FROM room_background_images WHERE app_code = ? AND room_id = ? AND image_url = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(imageURL))
	background, err := scanRoomBackground(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.RoomBackgroundImage{}, false, nil
	}
	return background, err == nil, err
}

func sameRoomBackgroundMedia(left roomservice.RoomBackgroundImage, right roomservice.RoomBackgroundImage) bool {
	return strings.TrimSpace(left.ImageURL) == strings.TrimSpace(right.ImageURL) &&
		left.CreatedByUserID == right.CreatedByUserID && left.Media == right.Media
}
