package handlers

// package_migrations.go — v0.30.0 CS1
//
// Schema migration engine for extension packages. When a package declares
// schema_version and migrations in its manifest, this engine runs Starlark
// migration scripts on install (fresh or upgrade).
//
// Manifest format:
//   {
//     "schema_version": 2,
//     "migrations": {
//       "1": "def migrate(db):\n  db.insert('settings', {'key': 'v1'})",
//       "2": "def migrate(db):\n  ..."
//     }
//   }
//
// On fresh install: runs migrations 1..schema_version.
// On upgrade: runs migrations (current+1)..new_schema_version.
// On downgrade: rejected (409).

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"strconv"

	"go.starlark.net/starlark"

	"chat-switchboard/sandbox"
	"chat-switchboard/store"
)

// ParseSchemaVersion extracts the "schema_version" integer from a manifest.
// Returns 0 if not present or not a number.
func ParseSchemaVersion(manifest map[string]any) int {
	raw, ok := manifest["schema_version"]
	if !ok {
		return 0
	}
	switch v := raw.(type) {
	case float64:
		return int(v)
	case int:
		return v
	}
	return 0
}

// ParseMigrations extracts the "migrations" map from a manifest.
// Keys are string integers ("1", "2", ...), values are Starlark source code.
func ParseMigrations(manifest map[string]any) map[int]string {
	raw, ok := manifest["migrations"]
	if !ok {
		return nil
	}
	migrationsRaw, ok := raw.(map[string]any)
	if !ok || len(migrationsRaw) == 0 {
		return nil
	}

	result := make(map[int]string, len(migrationsRaw))
	for key, val := range migrationsRaw {
		version, err := strconv.Atoi(key)
		if err != nil {
			log.Printf("[pkg-migrate] skipping non-integer migration key %q", key)
			continue
		}
		script, ok := val.(string)
		if !ok || script == "" {
			log.Printf("[pkg-migrate] skipping empty migration for version %d", version)
			continue
		}
		result[version] = script
	}
	return result
}

// RunSchemaMigrations executes Starlark migration scripts from fromVersion+1
// to toVersion, updating schema_version in the store after each successful step.
//
// The sandbox runs each migration script with a db module at write level,
// scoped to the package's namespace. This bypasses the permission system
// because migrations are admin-initiated.
//
// On error, returns immediately — partial migration state is tracked via
// the per-step schema_version update.
func RunSchemaMigrations(
	ctx context.Context,
	sb *sandbox.Sandbox,
	stores store.Stores,
	db *sql.DB,
	isPostgres bool,
	packageID string,
	manifest map[string]any,
	fromVersion int,
	toVersion int,
) error {
	if fromVersion >= toVersion {
		return nil
	}

	migrations := ParseMigrations(manifest)
	if migrations == nil && toVersion > 0 {
		// No migration scripts but schema_version declared — just set the version
		return stores.Packages.SetSchemaVersion(ctx, packageID, toVersion)
	}

	for step := fromVersion + 1; step <= toVersion; step++ {
		script, ok := migrations[step]
		if !ok {
			// No script for this step — just bump the version
			log.Printf("[pkg-migrate] %s: no migration script for version %d, skipping", packageID, step)
			if err := stores.Packages.SetSchemaVersion(ctx, packageID, step); err != nil {
				return fmt.Errorf("set schema_version to %d: %w", step, err)
			}
			continue
		}

		log.Printf("[pkg-migrate] %s: running migration %d", packageID, step)

		// Build a db module at write level for the migration
		dbModule := sandbox.BuildDBModule(ctx, sandbox.DBModuleConfig{
			PackageID:  packageID,
			CanWrite:   true,
			DB:         db,
			IsPostgres: isPostgres,
		})

		modules := map[string]starlark.Value{
			"db": dbModule,
		}

		// Execute the migration script
		result, err := sb.Exec(ctx, fmt.Sprintf("%s_migrate_%d.star", packageID, step), script, modules)
		if err != nil {
			return fmt.Errorf("migration %d for %s failed: %w", step, packageID, err)
		}

		// Call migrate(db) if defined
		if migrateFn, ok := result.Globals["migrate"]; ok {
			if callable, ok := migrateFn.(starlark.Callable); ok {
				_, _, err := sb.Call(ctx, callable, starlark.Tuple{dbModule}, nil)
				if err != nil {
					return fmt.Errorf("migration %d for %s: migrate() failed: %w", step, packageID, err)
				}
			}
		}

		// Mark this step as complete
		if err := stores.Packages.SetSchemaVersion(ctx, packageID, step); err != nil {
			return fmt.Errorf("set schema_version to %d: %w", step, err)
		}

		log.Printf("[pkg-migrate] %s: migration %d complete", packageID, step)
	}

	return nil
}
