package main

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

const (
	defaultAdminBaseURL = "http://127.0.0.1:13100/api/v1"
	defaultAdminUser    = "admin"
	defaultAdminPass    = "admin123"
	defaultMySQLDSN     = "hyapp:hyapp@tcp(127.0.0.1:23306)/%s?parseTime=true&charset=utf8mb4&loc=UTC"

	appCode        = "lalu"
	roomScene      = "voice"
	normalGiftID   = "robot_room_test_normal"
	luckyGiftID    = "robot_room_test_lucky"
	robotUserStart = int64(920001)
	robotUserCount = 12
)

type apiBody struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data"`
}

type loginData struct {
	AccessToken string `json:"accessToken"`
}

type availableRobot struct {
	UserID string `json:"userId"`
}

type robotRoom struct {
	RoomID           string   `json:"roomId"`
	Status           string   `json:"status"`
	OwnerRobotUserID string   `json:"ownerRobotUserId"`
	RobotUserIDs     []string `json:"robotUserIds"`
	GiftRule         struct {
		GiftIDs              []string `json:"giftIds"`
		LuckyGiftIDs         []string `json:"luckyGiftIds"`
		NormalGiftIntervalMS int64    `json:"normalGiftIntervalMs"`
		LuckyComboMin        int64    `json:"luckyComboMin"`
		LuckyComboMax        int64    `json:"luckyComboMax"`
		LuckyPauseMinMS      int64    `json:"luckyPauseMinMs"`
		LuckyPauseMaxMS      int64    `json:"luckyPauseMaxMs"`
	} `json:"giftRule"`
}

type pageData struct {
	Items []robotRoom `json:"items"`
	Total int64       `json:"total"`
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()

	baseURL := env("ROBOT_ROOM_ADMIN_BASE_URL", defaultAdminBaseURL)
	mysqlDSN := env("ROBOT_ROOM_MYSQL_DSN", defaultMySQLDSN)
	adminUser := env("ROBOT_ROOM_ADMIN_USERNAME", defaultAdminUser)
	adminPass := env("ROBOT_ROOM_ADMIN_PASSWORD", defaultAdminPass)

	dbs, err := openDBs(ctx, mysqlDSN)
	if err != nil {
		fail(err)
	}
	defer dbs.close()

	if err := seed(ctx, dbs); err != nil {
		fail(fmt.Errorf("seed local data: %w", err))
	}
	fmt.Println("seed: ok")

	client := &http.Client{Timeout: 10 * time.Second}
	token, err := login(ctx, client, baseURL, adminUser, adminPass)
	if err != nil {
		fail(err)
	}
	fmt.Println("login: ok")

	available, err := getJSON[[]availableRobot](ctx, client, baseURL+"/admin/rooms/robot-rooms/available-robots", token)
	if err != nil {
		fail(err)
	}
	if len(available) < 10 {
		fail(fmt.Errorf("available robots = %d, want at least 10", len(available)))
	}
	fmt.Printf("available robots: %d\n", len(available))

	payload := map[string]any{
		"ownerRobotUserId":      robotUserStart,
		"candidateRobotUserIds": robotIDs(),
		"minRobotCount":         6,
		"maxRobotCount":         8,
		"roomName":              "本地接口测试机器人房",
		"visibleRegionId":       0,
		"giftIds":               []string{normalGiftID},
		"luckyGiftIds":          []string{luckyGiftID},
		"normalGiftIntervalMs":  60000,
		"luckyComboMin":         2,
		"luckyComboMax":         3,
		"luckyPauseMinMs":       600000,
		"luckyPauseMaxMs":       600000,
	}
	created, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms", token, payload)
	if err != nil {
		fail(err)
	}
	if created.RoomID == "" || created.Status != "active" {
		fail(fmt.Errorf("created room invalid: room_id=%q status=%q", created.RoomID, created.Status))
	}
	if len(created.RobotUserIDs) < 6 || len(created.RobotUserIDs) > 8 {
		fail(fmt.Errorf("created robot count = %d, want 6-8", len(created.RobotUserIDs)))
	}
	fmt.Printf("create robot room: %s robots=%d\n", created.RoomID, len(created.RobotUserIDs))

	list, err := getJSON[pageData](ctx, client, baseURL+"/admin/rooms/robot-rooms?status=active&page=1&page_size=20", token)
	if err != nil {
		fail(err)
	}
	if !containsRoom(list.Items, created.RoomID) {
		fail(fmt.Errorf("created room %s not found in active list", created.RoomID))
	}
	fmt.Printf("list active robot rooms: total=%d\n", list.Total)

	stopped, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/stop", token, nil)
	if err != nil {
		fail(err)
	}
	if stopped.Status != "stopped" {
		fail(fmt.Errorf("stop status = %q, want stopped", stopped.Status))
	}
	fmt.Println("stop robot room: ok")

	started, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/start", token, nil)
	if err != nil {
		fail(err)
	}
	if started.Status != "active" {
		fail(fmt.Errorf("start status = %q, want active", started.Status))
	}
	fmt.Println("start robot room: ok")

	if _, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/stop", token, nil); err != nil {
		fail(err)
	}
	if err := verifyDB(ctx, dbs, created.RoomID); err != nil {
		fail(err)
	}
	fmt.Println("db verify: ok")
	fmt.Println("PASS robot room admin API smoke")
}

type dbs struct {
	user   *sql.DB
	wallet *sql.DB
	robot  *sql.DB
	room   *sql.DB
}

func openDBs(ctx context.Context, dsnTemplate string) (dbs, error) {
	open := func(name string) (*sql.DB, error) {
		dsn := fmt.Sprintf(dsnTemplate, name)
		if !strings.Contains(dsnTemplate, "%s") {
			dsn = dsnTemplate
		}
		db, err := sql.Open("mysql", dsn)
		if err != nil {
			return nil, err
		}
		if err := db.PingContext(ctx); err != nil {
			_ = db.Close()
			return nil, fmt.Errorf("%s: %w", name, err)
		}
		return db, nil
	}
	userDB, err := open("hyapp_user")
	if err != nil {
		return dbs{}, err
	}
	walletDB, err := open("hyapp_wallet")
	if err != nil {
		_ = userDB.Close()
		return dbs{}, err
	}
	robotDB, err := open("hyapp_robot")
	if err != nil {
		_ = userDB.Close()
		_ = walletDB.Close()
		return dbs{}, err
	}
	roomDB, err := open("hyapp_room")
	if err != nil {
		_ = userDB.Close()
		_ = walletDB.Close()
		_ = robotDB.Close()
		return dbs{}, err
	}
	return dbs{user: userDB, wallet: walletDB, robot: robotDB, room: roomDB}, nil
}

func (d dbs) close() {
	_ = d.user.Close()
	_ = d.wallet.Close()
	_ = d.robot.Close()
	_ = d.room.Close()
}

func seed(ctx context.Context, dbs dbs) error {
	now := time.Now().UnixMilli()
	ids := robotIDs()
	args := make([]any, 0, len(ids)+1)
	args = append(args, appCode)
	in := make([]string, 0, len(ids))
	for _, id := range ids {
		in = append(in, "?")
		args = append(args, id)
	}
	idClause := strings.Join(in, ",")
	for _, statement := range []struct {
		db   *sql.DB
		sql  string
		args []any
	}{
		{dbs.room, "DELETE FROM room_command_log WHERE app_code = ? AND (room_id LIKE 'robot\\_%' OR command_id LIKE 'admin-robot-room:%')", []any{appCode}},
		{dbs.room, "DELETE FROM room_snapshots WHERE app_code = ? AND room_id LIKE 'robot\\_%'", []any{appCode}},
		{dbs.room, "DELETE FROM room_outbox WHERE app_code = ? AND room_id LIKE 'robot\\_%'", []any{appCode}},
		{dbs.room, "DELETE FROM room_robot_rooms WHERE app_code = ? AND owner_robot_user_id IN (" + idClause + ")", args},
		{dbs.room, "DELETE FROM rooms WHERE app_code = ? AND owner_user_id IN (" + idClause + ")", args},
		{dbs.room, "DELETE FROM room_list_entries WHERE app_code = ? AND owner_user_id IN (" + idClause + ")", args},
		{dbs.room, "DELETE FROM room_user_presence WHERE app_code = ? AND user_id IN (" + idClause + ")", args},
	} {
		if _, err := statement.db.ExecContext(ctx, statement.sql, statement.args...); err != nil {
			return err
		}
	}

	for _, id := range ids {
		display := strconv.FormatInt(id, 10)
		if _, err := dbs.user.ExecContext(ctx, `
			INSERT INTO users (
			  app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
			  username, gender, country, region_id, avatar, profile_completed, onboarding_status, status,
			  created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'SA', 0, '', 1, 'completed', 'active', ?, ?)
			ON DUPLICATE KEY UPDATE
			  username = VALUES(username),
			  current_display_user_id = VALUES(current_display_user_id),
			  status = 'active',
			  updated_at_ms = VALUES(updated_at_ms)`,
			appCode, id, display, display, fmt.Sprintf("机器人%d", id), now, now,
		); err != nil {
			return err
		}
		if _, err := dbs.robot.ExecContext(ctx, `
			INSERT INTO robot_room_robots (
			  app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
			) VALUES (?, ?, ?, 'active', 1, ?, ?, 0, 0)
			ON DUPLICATE KEY UPDATE status = 'active', updated_at_ms = VALUES(updated_at_ms)`,
			appCode, roomScene, id, now, now,
		); err != nil {
			return err
		}
	}

	if err := seedGift(ctx, dbs.wallet, normalGiftID, "机器人普通测试礼物", 1001, 1, now); err != nil {
		return err
	}
	if err := seedGift(ctx, dbs.wallet, luckyGiftID, "机器人幸运测试礼物", 1002, 2, now); err != nil {
		return err
	}
	if _, err := dbs.wallet.ExecContext(ctx, `
		INSERT INTO gift_diamond_ratio_configs (
		  app_code, region_id, gift_type_code, status, ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, 0, 'normal', 'active', 100.00, 1, 1, ?, ?)
		ON DUPLICATE KEY UPDATE status = 'active', ratio_percent = VALUES(ratio_percent), updated_at_ms = VALUES(updated_at_ms)`,
		appCode, now, now,
	); err != nil {
		return err
	}
	return nil
}

func seedGift(ctx context.Context, db *sql.DB, giftID string, name string, resourceID int64, coinPrice int64, now int64) error {
	if _, err := db.ExecContext(ctx, `
		INSERT INTO resources (
		  resource_id, app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
		  grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount,
		  usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order,
		  created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'gift', ?, 'active', true, false, 'none', '', 0, 'coin', ?, 0, '{}', '', '', '', '{}', 0, 1, 1, ?, ?)
		ON DUPLICATE KEY UPDATE name = VALUES(name), status = 'active', coin_price = VALUES(coin_price), updated_at_ms = VALUES(updated_at_ms)`,
		resourceID, appCode, giftID+"_resource", name, coinPrice, now, now,
	); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		INSERT INTO gift_type_configs (
		  app_code, type_code, name, tab_key, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, 'normal', '普通礼物', 'normal', 'active', 0, 1, 1, ?, ?)
		ON DUPLICATE KEY UPDATE status = 'active', updated_at_ms = VALUES(updated_at_ms)`,
		appCode, now, now,
	); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		INSERT INTO gift_configs (
		  app_code, gift_id, resource_id, status, name, sort_order, presentation_json, gift_type_code,
		  effective_from_ms, effective_to_ms, effect_types_json, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'active', ?, 0, '{}', 'normal', 0, 0, '["global_broadcast"]', 1, 1, ?, ?)
		ON DUPLICATE KEY UPDATE status = 'active', name = VALUES(name), resource_id = VALUES(resource_id), updated_at_ms = VALUES(updated_at_ms)`,
		appCode, giftID, resourceID, name, now, now,
	); err != nil {
		return err
	}
	_, err := db.ExecContext(ctx, `
		INSERT INTO wallet_gift_prices (
		  app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount, heat_value,
		  effective_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, 'robot_room_test_v1', 'active', 'COIN', ?, 0, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE status = 'active', coin_price = VALUES(coin_price), heat_value = VALUES(heat_value), updated_at_ms = VALUES(updated_at_ms)`,
		appCode, giftID, coinPrice, coinPrice, now, now,
	)
	return err
}

func verifyDB(ctx context.Context, dbs dbs, roomID string) error {
	var status string
	var owner int64
	var rawRobots string
	err := dbs.room.QueryRowContext(ctx, `
		SELECT status, owner_robot_user_id, CAST(robot_user_ids_json AS CHAR)
		FROM room_robot_rooms
		WHERE app_code = ? AND room_id = ?`,
		appCode, roomID,
	).Scan(&status, &owner, &rawRobots)
	if err != nil {
		return err
	}
	if status != "stopped" {
		return fmt.Errorf("db status = %s, want stopped", status)
	}
	if owner != robotUserStart {
		return fmt.Errorf("db owner = %d, want %d", owner, robotUserStart)
	}
	var robotIDs []int64
	if err := json.Unmarshal([]byte(rawRobots), &robotIDs); err != nil {
		return err
	}
	if len(robotIDs) < 6 || len(robotIDs) > 8 {
		return fmt.Errorf("db robot count = %d, want 6-8", len(robotIDs))
	}
	var realGiftDebits int64
	if err := dbs.wallet.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM wallet_transactions
		WHERE app_code = ? AND biz_type = 'gift_debit' AND JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')) = ?`,
		appCode, roomID,
	).Scan(&realGiftDebits); err != nil {
		return err
	}
	if realGiftDebits != 0 {
		return fmt.Errorf("real gift debit rows for robot room = %d, want 0", realGiftDebits)
	}
	return nil
}

func login(ctx context.Context, client *http.Client, baseURL string, username string, password string) (string, error) {
	data, err := postJSON[loginData](ctx, client, baseURL+"/auth/login", "", map[string]string{
		"username": username,
		"password": password,
	})
	if err != nil {
		return "", err
	}
	if strings.TrimSpace(data.AccessToken) == "" {
		return "", errors.New("login returned empty access token")
	}
	return data.AccessToken, nil
}

func getJSON[T any](ctx context.Context, client *http.Client, url string, token string) (T, error) {
	var zero T
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return zero, err
	}
	return doJSON[T](client, req, token)
}

func postJSON[T any](ctx context.Context, client *http.Client, url string, token string, payload any) (T, error) {
	var zero T
	var body io.Reader
	if payload != nil {
		raw, err := json.Marshal(payload)
		if err != nil {
			return zero, err
		}
		body = bytes.NewReader(raw)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
	if err != nil {
		return zero, err
	}
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	return doJSON[T](client, req, token)
}

func doJSON[T any](client *http.Client, req *http.Request, token string) (T, error) {
	var zero T
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := client.Do(req)
	if err != nil {
		return zero, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return zero, fmt.Errorf("%s %s: status %d body %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	var wrapped apiBody
	if err := json.Unmarshal(raw, &wrapped); err != nil {
		return zero, err
	}
	if wrapped.Code != 0 {
		return zero, fmt.Errorf("%s %s: code %d message %s", req.Method, req.URL.Path, wrapped.Code, wrapped.Message)
	}
	var out T
	if len(wrapped.Data) == 0 || string(wrapped.Data) == "null" {
		return out, nil
	}
	if err := json.Unmarshal(wrapped.Data, &out); err != nil {
		return zero, err
	}
	return out, nil
}

func robotIDs() []int64 {
	ids := make([]int64, 0, robotUserCount)
	for i := 0; i < robotUserCount; i++ {
		ids = append(ids, robotUserStart+int64(i))
	}
	return ids
}

func containsRoom(items []robotRoom, roomID string) bool {
	for _, item := range items {
		if item.RoomID == roomID {
			return true
		}
	}
	return false
}

func env(key string, fallback string) string {
	if value := strings.TrimSpace(os.Getenv(key)); value != "" {
		return value
	}
	return fallback
}

func fail(err error) {
	fmt.Fprintln(os.Stderr, "FAIL:", err)
	os.Exit(1)
}
