package migrator

import (
	"errors"

	"github.com/Masterminds/semver"
	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/database/models"
	"github.com/portainer/portainer/api/dataservices/dockerhub"
	"github.com/portainer/portainer/api/dataservices/edgejob"
	"github.com/portainer/portainer/api/dataservices/edgestack"
	"github.com/portainer/portainer/api/dataservices/endpoint"
	"github.com/portainer/portainer/api/dataservices/endpointgroup"
	"github.com/portainer/portainer/api/dataservices/endpointrelation"
	"github.com/portainer/portainer/api/dataservices/extension"
	"github.com/portainer/portainer/api/dataservices/pendingactions"
	"github.com/portainer/portainer/api/dataservices/registry"
	"github.com/portainer/portainer/api/dataservices/resourcecontrol"
	"github.com/portainer/portainer/api/dataservices/role"
	"github.com/portainer/portainer/api/dataservices/schedule"
	"github.com/portainer/portainer/api/dataservices/settings"
	"github.com/portainer/portainer/api/dataservices/snapshot"
	"github.com/portainer/portainer/api/dataservices/stack"
	"github.com/portainer/portainer/api/dataservices/tag"
	"github.com/portainer/portainer/api/dataservices/teammembership"
	"github.com/portainer/portainer/api/dataservices/tunnelserver"
	"github.com/portainer/portainer/api/dataservices/user"
	"github.com/portainer/portainer/api/dataservices/version"
	"github.com/portainer/portainer/api/internal/authorization"
	"github.com/rs/zerolog/log"
)

type (
	// Migrator defines a service to migrate data after a Portainer version update.
	Migrator struct {
		flags            *portainer.CLIFlags
		currentDBVersion *models.Version
		migrations       []Migrations

		endpointGroupService    *endpointgroup.Service
		endpointService         *endpoint.Service
		endpointRelationService *endpointrelation.Service
		extensionService        *extension.Service
		registryService         *registry.Service
		resourceControlService  *resourcecontrol.Service
		roleService             *role.Service
		scheduleService         *schedule.Service
		settingsService         *settings.Service
		snapshotService         *snapshot.Service
		stackService            *stack.Service
		tagService              *tag.Service
		teamMembershipService   *teammembership.Service
		userService             *user.Service
		versionService          *version.Service
		fileService             portainer.FileService
		authorizationService    *authorization.Service
		dockerhubService        *dockerhub.Service
		edgeStackService        *edgestack.Service
		edgeJobService          *edgejob.Service
		TunnelServerService     *tunnelserver.Service
		pendingActionsService   *pendingactions.Service
	}

	// MigratorParameters represents the required parameters to create a new Migrator instance.
	MigratorParameters struct {
		Flags                   *portainer.CLIFlags
		CurrentDBVersion        *models.Version
		EndpointGroupService    *endpointgroup.Service
		EndpointService         *endpoint.Service
		EndpointRelationService *endpointrelation.Service
		ExtensionService        *extension.Service
		RegistryService         *registry.Service
		ResourceControlService  *resourcecontrol.Service
		RoleService             *role.Service
		ScheduleService         *schedule.Service
		SettingsService         *settings.Service
		SnapshotService         *snapshot.Service
		StackService            *stack.Service
		TagService              *tag.Service
		TeamMembershipService   *teammembership.Service
		UserService             *user.Service
		VersionService          *version.Service
		FileService             portainer.FileService
		AuthorizationService    *authorization.Service
		DockerhubService        *dockerhub.Service
		EdgeStackService        *edgestack.Service
		EdgeJobService          *edgejob.Service
		TunnelServerService     *tunnelserver.Service
		PendingActionsService   *pendingactions.Service
	}
)

// NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator {
	migrator := &Migrator{
		flags:                   parameters.Flags,
		currentDBVersion:        parameters.CurrentDBVersion,
		endpointGroupService:    parameters.EndpointGroupService,
		endpointService:         parameters.EndpointService,
		endpointRelationService: parameters.EndpointRelationService,
		extensionService:        parameters.ExtensionService,
		registryService:         parameters.RegistryService,
		resourceControlService:  parameters.ResourceControlService,
		roleService:             parameters.RoleService,
		scheduleService:         parameters.ScheduleService,
		settingsService:         parameters.SettingsService,
		snapshotService:         parameters.SnapshotService,
		tagService:              parameters.TagService,
		teamMembershipService:   parameters.TeamMembershipService,
		stackService:            parameters.StackService,
		userService:             parameters.UserService,
		versionService:          parameters.VersionService,
		fileService:             parameters.FileService,
		authorizationService:    parameters.AuthorizationService,
		dockerhubService:        parameters.DockerhubService,
		edgeStackService:        parameters.EdgeStackService,
		edgeJobService:          parameters.EdgeJobService,
		TunnelServerService:     parameters.TunnelServerService,
		pendingActionsService:   parameters.PendingActionsService,
	}

	migrator.initMigrations()
	return migrator
}

func (m *Migrator) CurrentDBVersion() string {
	return m.currentDBVersion.SchemaVersion
}

func (m *Migrator) CurrentDBEdition() portainer.SoftwareEdition {
	return portainer.SoftwareEdition(m.currentDBVersion.Edition)
}

func (m *Migrator) CurrentSemanticDBVersion() *semver.Version {
	v, err := semver.NewVersion(m.currentDBVersion.SchemaVersion)
	if err != nil {
		log.Fatal().Stack().Err(err).Msg("failed to parse current version")
	}

	return v
}

func (m *Migrator) addMigrations(v string, funcs ...func() error) {
	m.migrations = append(m.migrations, Migrations{
		Version:        semver.MustParse(v),
		MigrationFuncs: funcs,
	})
}

func (m *Migrator) LatestMigrations() Migrations {
	return m.migrations[len(m.migrations)-1]
}

func (m *Migrator) GetMigratorCountOfCurrentAPIVersion() int {
	migratorCount := 0
	latestMigrations := m.LatestMigrations()

	if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
		migratorCount = len(latestMigrations.MigrationFuncs)
	}

	return migratorCount
}

// !NOTE: Migration funtions should ideally be idempotent.
// !      Which simply means the function can run over the same data many times but only transform it once.
// !      In practice this really just means an extra check or two to ensure we're not destroying valid data.
// !      This is not a hard rule though.  Understand the limitations.  A migration function may only run over
// !      the data more than once if a new migration function is added and the version of your database schema is
// !      the same.  e.g. two developers working on the same version add two different functions for different things.
// !      This increases the migration funcs count and so they all run again.

type Migrations struct {
	Version        *semver.Version
	MigrationFuncs MigrationFuncs
}

type MigrationFuncs []func() error

func (m *Migrator) initMigrations() {
	// !IMPORTANT: Do not be tempted to alter the order of these migrations.
	// !           Even though one of them looks out of order. Caused by history related
	// !           to maintaining two versions and releasing at different times

	m.addMigrations("1.0.0", dbTooOldError) // default version found after migration

	m.addMigrations("1.21",
		m.updateUsersToDBVersion18,
		m.updateEndpointsToDBVersion18,
		m.updateEndpointGroupsToDBVersion18,
		m.updateRegistriesToDBVersion18)

	m.addMigrations("1.22", m.updateSettingsToDBVersion19)

	m.addMigrations("1.22.1",
		m.updateUsersToDBVersion20,
		m.updateSettingsToDBVersion20,
		m.updateSchedulesToDBVersion20)

	m.addMigrations("1.23",
		m.updateResourceControlsToDBVersion22,
		m.updateUsersAndRolesToDBVersion22)

	m.addMigrations("1.24",
		m.updateTagsToDBVersion23,
		m.updateEndpointsAndEndpointGroupsToDBVersion23)

	m.addMigrations("1.24.1", m.updateSettingsToDB24)

	m.addMigrations("2.0",
		m.updateSettingsToDB25,
		m.updateStacksToDB24)

	m.addMigrations("2.1", m.updateEndpointSettingsToDB25)
	m.addMigrations("2.2", m.updateStackResourceControlToDB27)
	m.addMigrations("2.6", m.migrateDBVersionToDB30)
	m.addMigrations("2.9", m.migrateDBVersionToDB32)
	m.addMigrations("2.9.2", m.migrateDBVersionToDB33)
	m.addMigrations("2.10.0", m.migrateDBVersionToDB34)
	m.addMigrations("2.9.3", m.migrateDBVersionToDB35)
	m.addMigrations("2.12", m.migrateDBVersionToDB36)
	m.addMigrations("2.13", m.migrateDBVersionToDB40)
	m.addMigrations("2.14", m.migrateDBVersionToDB50)
	m.addMigrations("2.15", m.migrateDBVersionToDB60)
	m.addMigrations("2.16", m.migrateDBVersionToDB70)
	m.addMigrations("2.16.1", m.migrateDBVersionToDB71)
	m.addMigrations("2.17", m.migrateDBVersionToDB80)
	m.addMigrations("2.18", m.migrateDBVersionToDB90)
	m.addMigrations("2.19",
		m.convertSeedToPrivateKeyForDB100,
		m.migrateDockerDesktopExtensionSetting,
		m.updateEdgeStackStatusForDB100,
	)
	m.addMigrations("2.20",
		m.updateAppTemplatesVersionForDB110,
		m.updateResourceOverCommitToDB110,
	)
	m.addMigrations("2.20.2",
		m.cleanPendingActionsForDeletedEndpointsForDB111,
	)
	m.addMigrations("2.22.0",
		m.migratePendingActionsDataForDB130,
	)

	// Add new migrations above...
	// One function per migration, each versions migration funcs in the same file.
}

// Always is always run at the end of migrations
func (m *Migrator) Always() error {
	// currently nothing to be done in CE... yet
	return nil
}

func dbTooOldError() error {
	return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support")
}
