package push

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/go-errors/errors"
	"github.com/jackc/pgconn"
	"github.com/jackc/pgx/v4"
	"github.com/spf13/afero"
	"github.com/supabase/cli/internal/migration/up"
	"github.com/supabase/cli/internal/utils"
	"github.com/supabase/cli/internal/utils/flags"
	"github.com/supabase/cli/pkg/migration"
	"github.com/supabase/cli/pkg/vault"
)

func Run(ctx context.Context, dryRun, ignoreVersionMismatch bool, includeRoles, includeSeed bool, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
	if dryRun {
		fmt.Fprintln(os.Stderr, "DRY RUN: migrations will *not* be pushed to the database.")
	}
	conn, err := utils.ConnectByConfig(ctx, config, options...)
	if err != nil {
		return err
	}
	defer conn.Close(context.Background())
	pending, err := up.GetPendingMigrations(ctx, ignoreVersionMismatch, conn, fsys)
	if err != nil {
		return err
	}
	var seeds []migration.SeedFile
	if includeSeed {
		// TODO: flag should override config but we don't resolve glob paths when seed is disabled.
		if !utils.Config.Db.Seed.Enabled {
			fmt.Fprintln(os.Stderr, "Skipping seed because it is disabled in config.toml for project:", flags.ProjectRef)
		} else if seeds, err = migration.GetPendingSeeds(ctx, utils.Config.Db.Seed.SqlPaths, conn, afero.NewIOFS(fsys)); err != nil {
			return err
		}
	}
	var globals []string
	if includeRoles {
		if exists, err := afero.Exists(fsys, utils.CustomRolesPath); err != nil {
			return errors.Errorf("failed to find custom roles: %w", err)
		} else if exists {
			globals = append(globals, utils.CustomRolesPath)
		}
	}
	if len(pending) == 0 && len(seeds) == 0 && len(globals) == 0 {
		fmt.Println("Remote database is up to date.")
		return nil
	}
	// Push pending migrations
	if dryRun {
		if len(globals) > 0 {
			fmt.Fprintln(os.Stderr, "Would create custom roles "+utils.Bold(globals[0])+"...")
		}
		if len(pending) > 0 {
			fmt.Fprintln(os.Stderr, "Would push these migrations:")
			fmt.Fprint(os.Stderr, confirmPushAll(pending))
		}
		if len(seeds) > 0 {
			fmt.Fprintln(os.Stderr, "Would seed these files:")
			fmt.Fprint(os.Stderr, confirmSeedAll(seeds))
		}
	} else {
		if len(globals) > 0 {
			msg := "Do you want to create custom roles in the database cluster?"
			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
				return err
			} else if !shouldPush {
				return errors.New(context.Canceled)
			}
			if err := migration.SeedGlobals(ctx, globals, conn, afero.NewIOFS(fsys)); err != nil {
				return err
			}
		}
		if len(pending) > 0 {
			msg := fmt.Sprintf("Do you want to push these migrations to the remote database?\n%s\n", confirmPushAll(pending))
			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
				return err
			} else if !shouldPush {
				return errors.New(context.Canceled)
			}
			if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil {
				return err
			}
			if err := migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)); err != nil {
				return err
			}
		} else {
			fmt.Fprintln(os.Stderr, "Schema migrations are up to date.")
		}
		if len(seeds) > 0 {
			msg := fmt.Sprintf("Do you want to seed the remote database with these files?\n%s\n", confirmSeedAll(seeds))
			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
				return err
			} else if !shouldPush {
				return errors.New(context.Canceled)
			}
			if err := migration.SeedData(ctx, seeds, conn, afero.NewIOFS(fsys)); err != nil {
				return err
			}
		} else if includeSeed {
			fmt.Fprintln(os.Stderr, "Seed files are up to date.")
		}
	}
	fmt.Println("Finished " + utils.Aqua("supabase db push") + ".")
	return nil
}

func confirmPushAll(pending []string) (msg string) {
	for _, path := range pending {
		filename := filepath.Base(path)
		msg += fmt.Sprintf(" • %s\n", utils.Bold(filename))
	}
	return msg
}

func confirmSeedAll(pending []migration.SeedFile) (msg string) {
	for _, seed := range pending {
		notice := seed.Path
		if seed.Dirty {
			notice += " (hash update)"
		}
		msg += fmt.Sprintf(" • %s\n", utils.Bold(notice))
	}
	return msg
}
