package main

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

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

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

func run() error {
	dsn := strings.TrimSpace(os.Getenv("MYSQL_DSN"))
	if dsn == "" {
		return fmt.Errorf("MYSQL_DSN is required")
	}
	if len(os.Args) < 2 {
		return fmt.Errorf("usage: platform-migrate migration.sql [migration.sql ...]")
	}
	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()
	if err := db.PingContext(ctx); err != nil {
		return fmt.Errorf("connect mysql: %w", err)
	}
	if len(os.Args) == 2 && os.Args[1] == "--server-version" {
		var version string
		if err := db.QueryRowContext(ctx, "SELECT VERSION()").Scan(&version); err != nil {
			return err
		}
		fmt.Println(version)
		return nil
	}
	if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS vehicle_platform_schema_migration (
version VARCHAR(255) NOT NULL PRIMARY KEY,
checksum CHAR(64) NOT NULL,
applied_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
)`); err != nil {
		return fmt.Errorf("ensure migration journal: %w", err)
	}
	for _, path := range os.Args[1:] {
		contents, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("read %s: %w", path, err)
		}
		version := filepath.Base(path)
		sum := sha256.Sum256(contents)
		checksum := hex.EncodeToString(sum[:])
		var recorded string
		err = db.QueryRowContext(ctx, `SELECT checksum FROM vehicle_platform_schema_migration WHERE version=?`, version).Scan(&recorded)
		if err == nil {
			if recorded != checksum {
				return fmt.Errorf("migration %s was changed after being applied", version)
			}
			fmt.Printf("skipped %s (already applied)\n", path)
			continue
		}
		if err != sql.ErrNoRows {
			return fmt.Errorf("check migration %s: %w", version, err)
		}
		statements := splitSQL(string(contents))
		for index, statement := range statements {
			if _, err := db.ExecContext(ctx, statement); err != nil {
				if isResumableMigrationDDL(err, statement) {
					fmt.Printf("skipped %s statement %d (schema object already exists)\n", path, index+1)
					continue
				}
				return fmt.Errorf("apply %s statement %d: %w", path, index+1, err)
			}
		}
		if _, err := db.ExecContext(ctx, `INSERT INTO vehicle_platform_schema_migration(version,checksum) VALUES(?,?)`, version, checksum); err != nil {
			return fmt.Errorf("record migration %s: %w", version, err)
		}
		fmt.Printf("applied %s (%d statements)\n", path, len(statements))
	}
	return nil
}

func isResumableMigrationDDL(err error, statement string) bool {
	var mysqlErr *mysql.MySQLError
	if !errors.As(err, &mysqlErr) {
		return false
	}
	upper := strings.ToUpper(strings.TrimSpace(statement))
	duplicateColumn := mysqlErr.Number == 1060 && strings.Contains(upper, "ALTER TABLE") && strings.Contains(upper, "ADD COLUMN")
	duplicateIndex := mysqlErr.Number == 1061 && strings.HasPrefix(upper, "CREATE INDEX")
	return duplicateColumn || duplicateIndex
}

// splitSQL intentionally supports the platform's forward-only DDL files. Those
// files contain no procedures or quoted semicolons; rejecting empty fragments
// keeps deployment output deterministic without enabling multiStatements in DSN.
func splitSQL(contents string) []string {
	lines := strings.Split(contents, "\n")
	withoutComments := make([]string, 0, len(lines))
	for _, line := range lines {
		if strings.HasPrefix(strings.TrimSpace(line), "--") {
			continue
		}
		withoutComments = append(withoutComments, line)
	}
	contents = strings.Join(withoutComments, "\n")
	fragments := strings.Split(contents, ";")
	statements := make([]string, 0, len(fragments))
	for _, fragment := range fragments {
		statement := strings.TrimSpace(fragment)
		if statement != "" {
			statements = append(statements, statement)
		}
	}
	return statements
}
