package accesscontrol

import (
	"xorm.io/xorm"

	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)

const PreventSeedingOnCallAccessID = "prevent seeding OnCall access"

const migSQLITERoleNameNullable = `ALTER TABLE seed_assignment ADD COLUMN tmp_role_name VARCHAR(190) DEFAULT NULL;
UPDATE seed_assignment SET tmp_role_name = role_name;
ALTER TABLE seed_assignment DROP COLUMN role_name;
ALTER TABLE seed_assignment RENAME COLUMN tmp_role_name TO role_name;`

func AddSeedAssignmentMigrations(mg *migrator.Migrator) {
	seedAssignmentTable := migrator.Table{Name: "seed_assignment"}

	mg.AddMigration("add action column to seed_assignment",
		migrator.NewAddColumnMigration(seedAssignmentTable,
			&migrator.Column{Name: "action", Type: migrator.DB_Varchar, Length: 190, Nullable: true}))

	mg.AddMigration("add scope column to seed_assignment",
		migrator.NewAddColumnMigration(seedAssignmentTable,
			&migrator.Column{Name: "scope", Type: migrator.DB_Varchar, Length: 190, Nullable: true}))

	mg.AddMigration("remove unique index builtin_role_role_name before nullable update",
		migrator.NewDropIndexMigration(seedAssignmentTable,
			&migrator.Index{Cols: []string{"builtin_role", "role_name"}, Type: migrator.UniqueIndex}))

	mg.AddMigration("update seed_assignment role_name column to nullable",
		migrator.NewRawSQLMigration("").
			SQLite(migSQLITERoleNameNullable).
			Postgres("ALTER TABLE `seed_assignment` ALTER COLUMN role_name DROP NOT NULL;").
			Mysql("ALTER TABLE seed_assignment MODIFY role_name VARCHAR(190) DEFAULT NULL;"))

	mg.AddMigration("add unique index builtin_role_name back",
		migrator.NewAddIndexMigration(seedAssignmentTable,
			&migrator.Index{Cols: []string{"builtin_role", "role_name"}, Type: migrator.UniqueIndex}))

	mg.AddMigration("add unique index builtin_role_action_scope",
		migrator.NewAddIndexMigration(seedAssignmentTable,
			&migrator.Index{Cols: []string{"builtin_role", "action", "scope"}, Type: migrator.UniqueIndex}))

	mg.AddMigration("add primary key to seed_assigment", &seedAssignmentPrimaryKeyMigrator{})

	mg.AddMigration("add origin column to seed_assignment",
		migrator.NewAddColumnMigration(seedAssignmentTable,
			&migrator.Column{Name: "origin", Type: migrator.DB_Varchar, Length: 190, Nullable: true}))

	mg.AddMigration("add origin to plugin seed_assignment", &seedAssignmentOnCallMigrator{})
	mg.AddMigration(PreventSeedingOnCallAccessID, &SeedAssignmentOnCallAccessMigrator{})
}

type seedAssignmentPrimaryKeyMigrator struct {
	migrator.MigrationBase
}

func (m *seedAssignmentPrimaryKeyMigrator) SQL(dialect migrator.Dialect) string {
	return CodeMigrationSQL
}

func (m *seedAssignmentPrimaryKeyMigrator) Exec(sess *xorm.Session, mig *migrator.Migrator) error {
	driver := mig.Dialect.DriverName()

	if driver == migrator.MySQL {
		_, err := sess.Exec("ALTER TABLE seed_assignment ADD id INT NOT NULL AUTO_INCREMENT FIRST, ADD PRIMARY KEY (id)")
		return err
	} else if driver == migrator.Postgres {
		_, err := sess.Exec("ALTER TABLE seed_assignment ADD COLUMN id SERIAL PRIMARY KEY")
		return err
	}

	// sqlite does not allow to add constraint after a table is created
	// We need to create a new table with desired columns, move data to new table, delete old table and rename new table to old

	// create temp table
	_, err := sess.Exec(`
		CREATE TABLE seed_assignment_temp (
		    id INTEGER PRIMARY KEY AUTOINCREMENT,
			builtin_role TEXT,
			action TEXT,
			scope TEXT,
			role_name TEXT
		);
	`)
	if err != nil {
		return err
	}

	// copy data to temp table
	_, err = sess.Exec("INSERT INTO seed_assignment_temp (builtin_role, action, scope, role_name) SELECT * FROM seed_assignment;")
	if err != nil {
		return err
	}

	// drop indices on old table
	_, err = sess.Exec("DROP INDEX UQE_seed_assignment_builtin_role_action_scope;")
	if err != nil {
		return err
	}
	_, err = sess.Exec("DROP INDEX UQE_seed_assignment_builtin_role_role_name;")
	if err != nil {
		return err
	}

	// drop old table
	_, err = sess.Exec("DROP TABLE seed_assignment;")
	if err != nil {
		return err
	}

	// rename temp table to old name
	_, err = sess.Exec("ALTER TABLE seed_assignment_temp RENAME TO seed_assignment;")
	if err != nil {
		return err
	}

	// recreate indexes on new table
	_, err = sess.Exec("CREATE UNIQUE INDEX UQE_seed_assignment_builtin_role_action_scope ON seed_assignment (builtin_role, action, scope);")
	if err != nil {
		return err
	}
	_, err = sess.Exec("CREATE UNIQUE INDEX UQE_seed_assignment_builtin_role_role_name ON seed_assignment (builtin_role, role_name);")
	if err != nil {
		return err
	}

	return nil
}

type seedAssignmentOnCallMigrator struct {
	migrator.MigrationBase
}

func (m *seedAssignmentOnCallMigrator) SQL(dialect migrator.Dialect) string {
	return CodeMigrationSQL
}

func (m *seedAssignmentOnCallMigrator) Exec(sess *xorm.Session, mig *migrator.Migrator) error {
	_, err := sess.Exec(
		`UPDATE seed_assignment SET origin = ? WHERE action LIKE ? OR scope = ?`,
		"grafana-oncall-app",
		"grafana-oncall-app%",
		"plugins:id:grafana-oncall-app",
	)
	return err
}

type SeedAssignmentOnCallAccessMigrator struct {
	migrator.MigrationBase
}

func (m *SeedAssignmentOnCallAccessMigrator) SQL(dialect migrator.Dialect) string {
	return CodeMigrationSQL
}

func (m *SeedAssignmentOnCallAccessMigrator) Exec(sess *xorm.Session, mig *migrator.Migrator) error {
	// Check if the migration is necessary
	hasEntry := 0
	if _, err := sess.SQL(`SELECT 1 FROM seed_assignment LIMIT 1`).Get(&hasEntry); err != nil {
		return err
	}
	if hasEntry == 0 {
		// Skip migration the seed assignment table has not been populated
		// Hence the oncall access permission can be granted without any risk
		return nil
	}

	// Check if the permission has not already been seeded
	// This is the case for instances that activated the accessControlOnCall feature already.
	type SeedAssignment struct {
		BuiltinRole, Action, Scope, Origin string
	}
	assigns := []SeedAssignment{}
	err := sess.SQL(`SELECT builtin_role, action, scope, origin FROM seed_assignment WHERE action = ? AND scope = ?`,
		"plugins.app:access", "plugins:id:grafana-oncall-app").
		Find(&assigns)
	if err != nil {
		return err
	}

	basicRoles := map[string]bool{"Viewer": true, "Editor": true, "Admin": true, "Grafana Admin": true}
	for i := range assigns {
		delete(basicRoles, assigns[i].BuiltinRole)
	}
	if len(basicRoles) == 0 {
		return nil
	}

	// By default, basic roles have access to all app plugins; no need for extra permission.
	// Mark OnCall Access permission as already seeded to prevent it from being added to basic roles.
	toSeed := []SeedAssignment{}
	for br := range basicRoles {
		toSeed = append(toSeed, SeedAssignment{
			BuiltinRole: br,
			Action:      "plugins.app:access",
			Scope:       "plugins:id:grafana-oncall-app",
			Origin:      "grafana-oncall-app",
		})
	}
	_, err = sess.Table("seed_assignment").InsertMulti(&toSeed)
	return err
}
