package main

import (
	"bufio"
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"fr.latosa-escrima/cmd/migrate/migrations"
	"fr.latosa-escrima/core"
	"fr.latosa-escrima/core/models"
	"fr.latosa-escrima/utils"
	"github.com/joho/godotenv"
	"github.com/uptrace/bun/extra/bundebug"
	"github.com/uptrace/bun/migrate"

	"github.com/urfave/cli/v2"

	"github.com/uptrace/bun"
)

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}
	environ := os.Getenv("ENVIRONMENT")

	hostname := os.Getenv("DATABASE_HOSTNAME")
	postgres_port := os.Getenv("POSTGRES_DOCKER_PORT")
	if environ == "DEV" {
		hostname = "localhost"
		postgres_port = os.Getenv("POSTGRES_PORT")
	}

	dsn := core.DSN{
		Hostname: hostname,
		Port:     postgres_port,
		DBName:   os.Getenv("POSTGRES_DB"),
		User:     os.Getenv("POSTGRES_USER"),
		Password: os.Getenv("POSTGRES_PASSWORD"),
	}
	fmt.Println(dsn.ToString())

	db, err := core.InitDatabase(dsn)
	if err != nil {
		log.Fatal(err)
	}

	db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))

	defer db.Close()
	if err != nil {
		panic(err)
	}

	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithEnabled(false),
		bundebug.FromEnv(),
	))

	app := &cli.App{
		Name: "bun",

		Commands: []*cli.Command{
			newDBCommand(migrate.NewMigrator(db, migrations.Migrations)),
			newAdminCommand(db),
		},
	}
	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

func newDBCommand(migrator *migrate.Migrator) *cli.Command {
	return &cli.Command{
		Name:  "db",
		Usage: "database migrations",
		Subcommands: []*cli.Command{
			{
				Name:  "init",
				Usage: "create migration tables",
				Action: func(c *cli.Context) error {
					return migrator.Init(c.Context)
				},
			},
			{
				Name:  "migrate",
				Usage: "migrate database",
				Action: func(c *cli.Context) error {
					if err := migrator.Lock(c.Context); err != nil {
						return err
					}
					defer migrator.Unlock(c.Context) //nolint:errcheck

					group, err := migrator.Migrate(c.Context)
					if err != nil {
						return err
					}
					if group.IsZero() {
						fmt.Printf("there are no new migrations to run (database is up to date)\n")
						return nil
					}
					fmt.Printf("migrated to %s\n", group)
					return nil
				},
			},
			{
				Name:  "rollback",
				Usage: "rollback the last migration group",
				Action: func(c *cli.Context) error {
					if err := migrator.Lock(c.Context); err != nil {
						return err
					}
					defer migrator.Unlock(c.Context) //nolint:errcheck

					group, err := migrator.Rollback(c.Context)
					if err != nil {
						return err
					}
					if group.IsZero() {
						fmt.Printf("there are no groups to roll back\n")
						return nil
					}
					fmt.Printf("rolled back %s\n", group)
					return nil
				},
			},
			{
				Name:  "lock",
				Usage: "lock migrations",
				Action: func(c *cli.Context) error {
					return migrator.Lock(c.Context)
				},
			},
			{
				Name:  "unlock",
				Usage: "unlock migrations",
				Action: func(c *cli.Context) error {
					return migrator.Unlock(c.Context)
				},
			},
			{
				Name:  "create_go",
				Usage: "create Go migration",
				Action: func(c *cli.Context) error {
					name := strings.Join(c.Args().Slice(), "_")
					mf, err := migrator.CreateGoMigration(c.Context, name)
					if err != nil {
						return err
					}
					fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
					return nil
				},
			},
			{
				Name:  "create_sql",
				Usage: "create up and down SQL migrations",
				Action: func(c *cli.Context) error {
					name := strings.Join(c.Args().Slice(), "_")
					files, err := migrator.CreateSQLMigrations(c.Context, name)
					if err != nil {
						return err
					}

					for _, mf := range files {
						fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
					}

					return nil
				},
			},
			{
				Name:  "create_tx_sql",
				Usage: "create up and down transactional SQL migrations",
				Action: func(c *cli.Context) error {
					name := strings.Join(c.Args().Slice(), "_")
					files, err := migrator.CreateTxSQLMigrations(c.Context, name)
					if err != nil {
						return err
					}

					for _, mf := range files {
						fmt.Printf("created transaction migration %s (%s)\n", mf.Name, mf.Path)
					}

					return nil
				},
			},
			{
				Name:  "status",
				Usage: "print migrations status",
				Action: func(c *cli.Context) error {
					ms, err := migrator.MigrationsWithStatus(c.Context)
					if err != nil {
						return err
					}
					fmt.Printf("migrations: %s\n", ms)
					fmt.Printf("unapplied migrations: %s\n", ms.Unapplied())
					fmt.Printf("last migration group: %s\n", ms.LastGroup())
					return nil
				},
			},
			{
				Name:  "mark_applied",
				Usage: "mark migrations as applied without actually running them",
				Action: func(c *cli.Context) error {
					group, err := migrator.Migrate(c.Context, migrate.WithNopMigration())
					if err != nil {
						return err
					}
					if group.IsZero() {
						fmt.Printf("there are no new migrations to mark as applied\n")
						return nil
					}
					fmt.Printf("marked as applied %s\n", group)
					return nil
				},
			},
		},
	}
}

func newAdminCommand(db *bun.DB) *cli.Command {
	return &cli.Command{
		Name:  "admin",
		Usage: "Creation of admin role and super user",
		Subcommands: []*cli.Command{
			{
				Name:  "new",
				Usage: "Creates a new admin user.",
				Action: func(c *cli.Context) error {
					ctx := context.Background()

					/* CREATING ADMIN ROLE */
					role := models.Role{
						Name: "Admin",
					}
					res, err := db.
						NewInsert().
						Ignore().
						Model(&role).
						Returning("id").
						Exec(ctx)
					if err != nil {
						return fmt.Errorf("failed to create admin role: %w", err)
					}
					fmt.Println("Admin role created successfully.")
					rowsAffected, err := res.RowsAffected()
					if err != nil {
						return fmt.Errorf("failed to check rows affected: %w", err)
					}

					// If no rows were affected, the role already exists, so query its id
					if rowsAffected == 0 {
						err = db.NewSelect().
							Model(&role).
							Where("name = ?", role.Name).
							Scan(ctx)
						if err != nil {
							return fmt.Errorf("failed to fetch existing role id: %w", err)
						}
					}
					/**/
					// }

					/* CREATING ADMIN USER */
					// Prompt user for admin details
					reader := bufio.NewReader(os.Stdin)

					fmt.Print("Enter first name: ")
					firstName, _ := reader.ReadString('\n')

					fmt.Print("Enter last name: ")
					lastName, _ := reader.ReadString('\n')

					fmt.Print("Enter email: ")
					email, _ := reader.ReadString('\n')

					fmt.Print("Enter phone: ")
					phone, _ := reader.ReadString('\n')

					fmt.Print("Enter password: ")
					password, _ := reader.ReadString('\n')

					// Trim newline characters
					firstName = strings.TrimSpace(firstName)
					lastName = strings.TrimSpace(lastName)
					email = strings.TrimSpace(email)
					phone = strings.TrimSpace(phone)
					password = strings.TrimSpace(password)

					// Hash the password
					// Create admin user
					admin := &models.User{
						FirstName: firstName,
						LastName:  lastName,
						Email:     email,
						Password:  password,
						Phone:     phone,
					}

					// Insert into database
					if _, err := admin.Insert(db, ctx); err != nil {
						return fmt.Errorf("failed to create admin user: %w", err)
					}
					/**/

					/* LINKING ADMIN USER TO ADMIN ROLE */
					userRole := models.UserToRole{
						UserID: admin.UserID,
						RoleID: role.ID,
					}

					if _, err := db.
						NewInsert().
						Model(&userRole).
						Ignore().
						Exec(ctx); err != nil {
						return fmt.Errorf("failed to link user to ro admin role: %w", err)
					}
					/**/

					/* LINKING ALL PERMISSIONS TO ADMIN ROLE */
					var permissions []*models.Permission
					if err := db.NewSelect().
						Model(&permissions).
						Scan(ctx); err != nil {
						return fmt.Errorf("failed to gather all permissions: %w", err)
					}

					permissionsToRole := utils.Map(
						permissions,
						func(permission *models.Permission) *models.PermissionToRole {
							return &models.PermissionToRole{
								PermissionAction:   permission.Action,
								PermissionResource: permission.Resource,
								RoleID:             role.ID,
							}
						})

					if _, err := db.NewInsert().
						Model(&permissionsToRole).
						Ignore().
						Exec(ctx); err != nil {
						return fmt.Errorf("failed to link every permission to admin role: %w", err)
					}

					/**/

					fmt.Println("Admin user created successfully.")
					return nil
				},
			},
		},
	}
}
