package github

import (
	"context"
	"fmt"
	"slices"
	"time"

	ent "github.com/tidbcloud/devops/entity/github"
	"github.com/tidbcloud/devops/pkg/infra/clients/githubcli"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/infra/utils/workqueue"
	"github.com/tidbcloud/devops/pkg/service/github/repo"

	"gorm.io/gorm"
)

var (
	interestedOrgs = []string{
		"tidbcloud",
	}
)

func (s *svc) ListRepository(ctx context.Context, param ListRepositoryParam) ([]*ent.Repository, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, err
	}

	opts := []repoopt.Option{}
	if len(param.Owners) > 0 {
		opts = append(opts, repo.RepoOption.OwnerIn(param.Owners))
	}
	if param.LocationLike != "" {
		opts = append(opts, repo.RepoOption.LocationLike(param.LocationLike))
	}

	// count total
	total, err := s.repo.CountRepository(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	// list
	if param.Page > 0 {
		opts = append(opts, repo.RepoOption.WithPager(param.Page, param.PerPage))
	}
	reposInDB, err := s.repo.ListRepository(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	repos := make([]*ent.Repository, 0, len(reposInDB))
	for _, repoInDB := range reposInDB {
		repos = append(repos, &ent.Repository{
			RepositoryInDB: *repoInDB,
		})
	}

	return repos, total, nil
}

func (s *svc) GetRepositoryByLoc(ctx context.Context, loc ent.RepoLocation) (*ent.Repository, error) {
	reposInDB, err := s.repo.ListRepository(ctx, repo.RepoOption.LocationIn(loc))
	if err != nil {
		return nil, err
	}
	if len(reposInDB) == 0 {
		return nil, gorm.ErrRecordNotFound
	}

	repoInDB := reposInDB[0]
	return &ent.Repository{
		RepositoryInDB: *repoInDB,
	}, nil
}

func (s *svc) SyncRepository(ctx context.Context) error {
	perPage := 20
	now := time.Now()
	logger := s.Logger(ctx)

	logger.Info("syncing repositories ...")

	// sync all repos in interested orgs
	for _, org := range interestedOrgs {
		opt := githubcli.ListRepositoryOption{
			Pager: githubcli.Pager{
				PerPage: perPage,
				Cursor:  "",
			},
			PushedBefore: pointer.Pointer(now.Add(*s.conf.SyncRepositoryPushedBefore * -1)),
		}

		for {
			result, err := s.cli.Repository.List(ctx, org, opt)
			if err != nil {
				return err
			}

			err = s.syncRepository(ctx, result.Repositories)
			if err != nil {
				return err
			}

			// next page
			if !result.Page.HasNextPage {
				break
			}
			opt.Cursor = result.Page.EndCursor
		}
	}

	// sync builtin repos
	if len(s.conf.BuiltinRepositories) > 0 {
		parallel := workqueue.NewParallelizer(s.conf.BuiltinRepositories, 10, 1)
		parallel.Do(ctx, func(ctx context.Context, job []string) {
			for _, locStr := range job {
				loc := ent.RepoLocation(locStr)
				if err := loc.Validate(); err != nil {
					logger.Info("skip a repo because of invalid builtin location", "loc", loc, "err", err.Error())
					continue
				}
				owner, name := loc.Parse()

				repoInDB, err := s.cli.Repository.Get(ctx, owner, name)
				if err != nil {
					logger.Info("skip a repo because fail to get from github", "loc", loc, "err", err.Error())
					continue
				}

				err = s.syncRepository(ctx, []githubcli.Repository{*repoInDB})
				if err != nil {
					logger.Info("skip a repo because fail to sync", "loc", loc, "err", err.Error())
					return
				}
			}
		})
	}

	return nil
}

func (s *svc) syncRepository(ctx context.Context, reposInGh []githubcli.Repository) error {
	locList := container.TransformSlice(reposInGh,
		func(r githubcli.Repository) ent.RepoLocation {
			return ent.NewRepoLocation(r.Owner.Login, r.Name)
		},
	)

	// query repos from db
	reposInDB, err := s.repo.ListRepository(ctx, repo.RepoOption.LocationIn(locList...))
	if err != nil {
		return err
	}

	// sync
	for i := range reposInGh {
		var (
			repoInGh = &reposInGh[i]
			repoInDB *ent.RepositoryInDB
		)

		loc := ent.NewRepoLocation(repoInGh.Owner.Login, repoInGh.Name)

		foundIdx := slices.IndexFunc(reposInDB, func(repoInDB *ent.RepositoryInDB) bool { return repoInDB.Location == loc })
		if foundIdx >= 0 {
			repoInDB = reposInDB[foundIdx]
		}

		err := s.syncOneRepository(ctx, repoInGh, repoInDB)
		if err != nil {
			return err
		}
	}

	return nil
}

func (s *svc) syncOneRepository(ctx context.Context, repoInGh *githubcli.Repository, repoInDB *ent.RepositoryInDB) error {
	logger := s.Logger(ctx)

	// create a repo if not exists in db
	if repoInGh != nil && repoInDB == nil {
		loc := ent.NewRepoLocation(repoInGh.Owner.Login, repoInGh.Name)

		newRepoInDB := ConvertRepository(*repoInGh).RepositoryInDB
		repoInDB, err := s.repo.CreateRepository(ctx, &newRepoInDB)
		if err != nil {
			return fmt.Errorf("create a repo %s to db failed: %w", loc, err)
		}

		logger.Info("create a repo to db success", "loc", loc, "id", repoInDB.ID)
		return nil
	}

	// delete a repo
	if repoInGh == nil && repoInDB != nil {
		return nil
	}

	// update a repo
	if repoInGh != nil && repoInDB != nil {
		loc := ent.NewRepoLocation(repoInGh.Owner.Login, repoInGh.Name)

		updatedFields := []string{}
		if repoInDB.PushedAt != repoInGh.PushedAt.Unix() {
			updatedFields = append(updatedFields, "pushed_at")
		}
		if repoInDB.UpdatedAt != repoInGh.UpdatedAt.Unix() {
			updatedFields = append(updatedFields, "updated_at")
		}

		if len(updatedFields) > 0 {
			err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
				return s.repo.PatchRepository(ctx, repoInDB, func(current *ent.RepositoryInDB) error {
					current.PushedAt = repoInGh.PushedAt.Unix()
					current.UpdatedAt = repoInGh.UpdatedAt.Unix()
					return nil
				})
			})
			if err != nil {
				return fmt.Errorf("update a repo %s in db failed: %w", loc, err)
			}
			logger.Info("update a repo in db success", "loc", loc, "id", repoInDB.ID, "updated_fields", updatedFields)
		}
		return nil
	}

	return nil
}
