package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

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

const (
	temporaryMode  = "temporary"
	durableMode    = "durable"
	temporaryTable = "vehicle_alert_candidate_benchmark"
	durableTable   = "vehicle_alert_candidate_benchmark_durable"
	durableLock    = "lingniu.vehicle-alert-candidate-benchmark"
)

type benchmarkConfig struct {
	Rows           int
	BatchSize      int
	Mode           string
	ConfirmDurable bool
}

type benchmarkResult struct {
	Mode                  string            `json:"mode"`
	Rows                  int               `json:"rows"`
	BatchSize             int               `json:"batchSize"`
	Batches               int               `json:"batches"`
	Transactions          int               `json:"transactions"`
	DurationMS            int64             `json:"durationMs"`
	RowsPerSec            float64           `json:"rowsPerSec"`
	VerifiedRows          int               `json:"verifiedRows"`
	Temporary             bool              `json:"temporaryTable"`
	DurableWrite          bool              `json:"durableWrite"`
	CleanupMS             int64             `json:"cleanupMs"`
	CleanupVerified       bool              `json:"cleanupVerified"`
	GlobalStatusDelta     map[string]uint64 `json:"globalStatusDelta,omitempty"`
	GlobalStatusAvailable bool              `json:"globalStatusAvailable"`
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "alert benchmark failed:", err)
		os.Exit(1)
	}
}

func run() error {
	config := benchmarkConfig{}
	flag.IntVar(&config.Rows, "rows", 10_000, "candidate rows to write")
	flag.IntVar(&config.BatchSize, "batch-size", 500, "rows per insert")
	flag.StringVar(&config.Mode, "mode", temporaryMode, "storage mode: temporary or durable")
	flag.BoolVar(&config.ConfirmDurable, "confirm-durable-write", false, "required acknowledgement for durable writes and DDL")
	flag.Parse()
	if err := config.validate(); err != nil {
		return err
	}
	dsn := strings.TrimSpace(os.Getenv("MYSQL_DSN"))
	if dsn == "" {
		return fmt.Errorf("MYSQL_DSN is required")
	}
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	defer db.Close()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	conn, err := db.Conn(ctx)
	if err != nil {
		return err
	}
	defer conn.Close()
	result, err := executeBenchmark(ctx, conn, config)
	if err != nil {
		return err
	}
	return json.NewEncoder(os.Stdout).Encode(result)
}

func (config benchmarkConfig) validate() error {
	config.Mode = strings.ToLower(strings.TrimSpace(config.Mode))
	if config.Rows < 1 || config.Rows > 100_000 || config.BatchSize < 1 || config.BatchSize > 1_000 {
		return fmt.Errorf("rows must be 1..100000 and batch-size 1..1000")
	}
	if config.Mode != temporaryMode && config.Mode != durableMode {
		return fmt.Errorf("mode must be temporary or durable")
	}
	if config.Mode == durableMode && !config.ConfirmDurable {
		return fmt.Errorf("durable mode creates, commits to, and drops a dedicated physical table; pass --confirm-durable-write")
	}
	return nil
}

func executeBenchmark(ctx context.Context, conn *sql.Conn, config benchmarkConfig) (result benchmarkResult, err error) {
	mode := strings.ToLower(strings.TrimSpace(config.Mode))
	table := temporaryTable
	createPrefix := "CREATE TEMPORARY TABLE"
	dropPrefix := "DROP TEMPORARY TABLE IF EXISTS"
	if mode == durableMode {
		table = durableTable
		createPrefix = "CREATE TABLE"
		dropPrefix = "DROP TABLE IF EXISTS"
		locked, lockErr := acquireBenchmarkLock(ctx, conn)
		if lockErr != nil {
			return result, fmt.Errorf("acquire durable benchmark lock: %w", lockErr)
		}
		if !locked {
			return result, fmt.Errorf("another durable benchmark is already running")
		}
		defer releaseBenchmarkLock(conn)
		if _, err = conn.ExecContext(ctx, dropPrefix+" "+table); err != nil {
			return result, fmt.Errorf("remove stale durable benchmark table: %w", err)
		}
	}
	if _, err = conn.ExecContext(ctx, createPrefix+" "+table+" LIKE vehicle_alert_candidate"); err != nil {
		return result, fmt.Errorf("create %s benchmark table: %w", mode, err)
	}
	cleanupStarted := time.Time{}
	cleanupComplete := false
	defer func() {
		cleanupStarted = time.Now()
		cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()
		_, cleanupErr := conn.ExecContext(cleanupCtx, dropPrefix+" "+table)
		if cleanupErr == nil {
			cleanupComplete = true
			if mode == durableMode {
				var remaining int
				cleanupErr = conn.QueryRowContext(cleanupCtx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?`, table).Scan(&remaining)
				cleanupComplete = cleanupErr == nil && remaining == 0
			}
		}
		result.CleanupMS = time.Since(cleanupStarted).Milliseconds()
		result.CleanupVerified = cleanupComplete
		if err == nil && cleanupErr != nil {
			err = fmt.Errorf("cleanup benchmark table: %w", cleanupErr)
		} else if err == nil && !cleanupComplete {
			err = fmt.Errorf("benchmark table cleanup could not be verified")
		}
	}()

	statusBefore, statusBeforeErr := readGlobalStatus(ctx, conn)
	started := time.Now()
	tx, err := conn.BeginTx(ctx, nil)
	if err != nil {
		return result, err
	}
	now := time.Now().UTC()
	batches := 0
	for start := 0; start < config.Rows; start += config.BatchSize {
		end := min(start+config.BatchSize, config.Rows)
		query, args := buildCandidateInsert(table, start, end, now)
		if _, err = tx.ExecContext(ctx, query, args...); err != nil {
			tx.Rollback()
			return result, fmt.Errorf("insert batch %d: %w", batches+1, err)
		}
		batches++
	}
	if err = tx.Commit(); err != nil {
		return result, err
	}
	duration := time.Since(started)
	var verified int
	if err = conn.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table).Scan(&verified); err != nil {
		return result, err
	}
	if verified != config.Rows {
		return result, fmt.Errorf("row verification failed: wrote %d, found %d", config.Rows, verified)
	}
	statusAfter, statusAfterErr := readGlobalStatus(ctx, conn)
	statusDelta := map[string]uint64{}
	statusAvailable := statusBeforeErr == nil && statusAfterErr == nil
	if statusAvailable {
		for key, after := range statusAfter {
			if before, ok := statusBefore[key]; ok && after >= before {
				statusDelta[key] = after - before
			}
		}
	}
	result = benchmarkResult{
		Mode:                  mode,
		Rows:                  config.Rows,
		BatchSize:             config.BatchSize,
		Batches:               batches,
		Transactions:          1,
		DurationMS:            duration.Milliseconds(),
		RowsPerSec:            float64(config.Rows) / duration.Seconds(),
		VerifiedRows:          verified,
		Temporary:             mode == temporaryMode,
		DurableWrite:          mode == durableMode,
		GlobalStatusDelta:     statusDelta,
		GlobalStatusAvailable: statusAvailable,
	}
	return result, nil
}

func acquireBenchmarkLock(ctx context.Context, conn *sql.Conn) (bool, error) {
	var acquired sql.NullInt64
	if err := conn.QueryRowContext(ctx, `SELECT GET_LOCK(?,0)`, durableLock).Scan(&acquired); err != nil {
		return false, err
	}
	return acquired.Valid && acquired.Int64 == 1, nil
}

func releaseBenchmarkLock(conn *sql.Conn) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _ = conn.ExecContext(ctx, `SELECT RELEASE_LOCK(?)`, durableLock)
}

func readGlobalStatus(ctx context.Context, conn *sql.Conn) (map[string]uint64, error) {
	rows, err := conn.QueryContext(ctx, `SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_os_log_written','Binlog_cache_use','Binlog_cache_disk_use','Com_commit')`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	values := map[string]uint64{}
	for rows.Next() {
		var key, raw string
		if err := rows.Scan(&key, &raw); err != nil {
			return nil, err
		}
		value, parseErr := strconv.ParseUint(raw, 10, 64)
		if parseErr != nil {
			return nil, parseErr
		}
		values[key] = value
	}
	return values, rows.Err()
}

func buildCandidateInsert(table string, start, end int, now time.Time) (string, []any) {
	if table != temporaryTable && table != durableTable {
		panic("unsupported benchmark table")
	}
	values := make([]string, 0, end-start)
	args := make([]any, 0, (end-start)*7)
	for index := start; index < end; index++ {
		values = append(values, "(?,?,?,?,?,?,?)")
		args = append(args, "benchmark-rule", fmt.Sprintf("BENCH%017d", index), "JT808", now, now, float64(index%120), fmt.Sprintf("benchmark-event-%d", index))
	}
	return `INSERT INTO ` + table + `(rule_id,vin,protocol,first_matched_at,last_matched_at,latest_value,source_event_id) VALUES ` + strings.Join(values, ","), args
}
