package serviceMigrations

import (
	"database/sql"
	"time"

	"github.com/iqstore/iqchannels/server/ctx"
	"github.com/iqstore/iqchannels/server/database"
	"github.com/iqstore/iqchannels/server/database/db"
	dbTypes "github.com/iqstore/iqchannels/server/database/db"

	"github.com/iqstore/iqchannels/server/library/logs"
	"github.com/iqstore/iqchannels/server/schema"
	"github.com/iqstore/iqchannels/server/services/groups"
	"github.com/iqstore/iqchannels/server/services/notifications"
	"github.com/iqstore/iqchannels/server/services/projects"
	"github.com/iqstore/iqchannels/server/services/quickreplies"
	"github.com/iqstore/iqchannels/server/services/tickets"
	"github.com/ivankorobkov/go-di"
)

func Module(m *di.Module) {
	m.Import(database.Module)
	m.Import(groups.Module)
	m.Import(quickreplies.Module)
	m.Import(projects.Module)
	m.Import(logs.Module)
	m.Import(tickets.Module)
	m.Import(notifications.Module)
	m.Add(newServiceMigrations)
}

func TestModule(m *di.Module) {
	m.Import(Module)
	m.Import(database.TestModule)
	m.Import(groups.TestModule)
	m.Import(quickreplies.TestModule)
	m.Import(projects.TestModule)
	m.Import(tickets.TestModule)
	m.Import(notifications.TestModule)
}

type ServiceMigrations interface {
	MustApply() int
	Apply(ctx ctx.Context) (int, error)
	Count(ctx ctx.Context) (int, error)
}

type serviceMigrations struct {
	db             dbTypes.DB
	groups         groups.Groups
	quickreplies   quickreplies.QuickReplies
	projects       projects.Projects
	tickets        tickets.Tickets
	notifications  notifications.Notifications
	migrations     map[string]func(ctx.Context, dbTypes.Tx) error //method name to function
	applyEveryTime map[string]bool                                // method name to is need to apply every time
	log            logs.Log
}

func newServiceMigrations(
	db dbTypes.DB,
	groups groups.Groups,
	quickreplies quickreplies.QuickReplies,
	projects projects.Projects,
	tickets tickets.Tickets,
	notifications notifications.Notifications,
	logs logs.Logs,
) ServiceMigrations {
	migrations := map[string]func(ctx.Context, dbTypes.Tx) error{
		"MergeDuplicatePersonalReplies":      func(ctx ctx.Context, tx dbTypes.Tx) error { return quickreplies.MergeDuplicatePersonalReplies(ctx, tx) },
		"GenerateNamesForGroupsPermissions":  func(ctx ctx.Context, tx dbTypes.Tx) error { return groups.GenerateNamesForGroupsPermissions(ctx, tx) },
		"FillProjectCountersTx":              func(ctx ctx.Context, tx dbTypes.Tx) error { return projects.FillProjectCountersTx(ctx, tx) },
		"FillNotificationInboxes":            func(ctx ctx.Context, tx dbTypes.Tx) error { return notifications.FillNotificationInboxes(ctx, tx) },
		"RemoveTicketsForeignKeys":           func(ctx ctx.Context, tx dbTypes.Tx) error { return tickets.DeleteForeignKeysFromTicketsTable(ctx, tx) },
		"MigrateCategoryIdsAndTagIdsToTable": func(ctx ctx.Context, tx dbTypes.Tx) error { return tickets.MigrateCategoryIdsAndTagIdsToTable(ctx, tx) },
	}
	applyEveryTime := map[string]bool{
		"FillProjectCountersTx":   true,
		"FillNotificationInboxes": true,
	}
	return &serviceMigrations{
		db:             db,
		groups:         groups,
		migrations:     migrations,
		applyEveryTime: applyEveryTime,
		projects:       projects,
		quickreplies:   quickreplies,
		tickets:        tickets,
		log:            logs.Log("service migrations"),
	}
}

func (m *serviceMigrations) MustApply() int {
	ctx := ctx.Background()
	n, err := m.Apply(ctx)

	if err != nil {
		m.log.Fatalf(ctx, "Failed to apply migrations, err=%v", err)
	}
	return n
}

func (m *serviceMigrations) Apply(ctx ctx.Context) (int, error) {
	m.log.Warnf(ctx, "Migrating services...")
	now := time.Now()

	if err := m.createTableIfNotExist(ctx); err != nil {
		return 0, err
	}

	appliedTotal := 0
	opts := &sql.TxOptions{Isolation: sql.LevelReadCommitted}
	err := m.db.WriteOptions(ctx, opts, func(tx db.Tx) error {
		for methodName, method := range m.migrations {
			applied, err := m.db.ServiceMigrations().FindByMethodName(ctx, tx, methodName)
			if err == nil && applied != nil {
				continue
			}
			if err != nil {
				return err
			}

			m.log.Infof(ctx, "Applying service migration %q", methodName)
			err = method(ctx, tx)
			if err != nil {
				return err
			}

			if !m.applyEveryTime[methodName] {
				serviceMigration := &schema.ServiceMigration{
					MethodName: methodName,
					AppliedAt:  now,
				}
				err = m.db.ServiceMigrations().Insert(ctx, tx, serviceMigration)
				if err != nil {
					return err
				}
			}

			appliedTotal++
			m.log.Warnf(ctx, "Applied service migration %q", methodName)
		}

		return nil
	})
	if err != nil {
		return 0, err
	}

	return appliedTotal, nil
}

func (m *serviceMigrations) Count(ctx ctx.Context) (int, error) {
	tx, err := m.db.Read(ctx)
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	return m.db.ServiceMigrations().Count(ctx, tx)
}

func (m *serviceMigrations) createTableIfNotExist(ctx ctx.Context) (err error) {
	exists := false
	err = m.db.Readf(ctx, func(tx db.Tx) error {
		exists, err = m.db.ServiceMigrations().TableExists(ctx, tx)
		if err != nil {
			return err
		}
		return err
	})
	if err != nil {
		return err
	}
	if exists {
		return nil
	}

	opts := &sql.TxOptions{Isolation: sql.LevelReadCommitted}
	return m.db.WriteOptions(ctx, opts, func(tx db.Tx) error {
		if err = m.db.ServiceMigrations().CreateTable(ctx, tx); err != nil {
			return err
		}

		m.log.Warnf(ctx, "Created a service migrations table")
		return nil
	})
}
