package release

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

	"github.com/tidbcloud/devops/api"
	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"
)

func (s *svc) ListReleases(ctx context.Context, param relsvcapi.ListReleaseParam) ([]*relent.Release, int64, error) {
	param, err := param.Normalize()
	if err != nil {
		return nil, 0, errs.APIErrorNew(api.ErrCodeInvalidArgument, err)
	}

	opts := []repoopt.Option{}
	if len(param.Names) != 0 {
		opts = append(opts, repo.ReleaseOpts.NameIn(param.Names))
	}
	if len(param.Statuses) != 0 {
		opts = append(opts, repo.ReleaseOpts.StatusIn(param.Statuses))
	}
	if !param.ReleaseBefore.IsZero() {
		opts = append(opts, repo.ReleaseOpts.ReleaseBefore(param.ReleaseBefore))
	}
	if !param.ReleaseAfter.IsZero() {
		opts = append(opts, repo.ReleaseOpts.ReleaseAfter(param.ReleaseAfter))
	}

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

	if param.Page > 0 {
		opts = append(opts, repo.ReleaseOpts.WithPager(param.Page, param.PerPage, true))
	}
	releasesInRepo, err := s.repo.ListReleases(ctx, opts...)
	if err != nil {
		return nil, 0, err
	}

	releases := make([]*relent.Release, 0, len(releasesInRepo))

	for _, releaseInRepo := range releasesInRepo {
		release := &relent.Release{
			ReleaseInRepo: *releaseInRepo,
		}
		releases = append(releases, release)
	}

	return releases, total, nil
}

func (s *svc) GetRelease(ctx context.Context, id uint64, param relsvcapi.GetReleaseParam) (*relent.Release, error) {
	releaseInRepo, err := s.repo.GetRelease(ctx, id)
	if err != nil {
		return nil, err
	}

	release := &relent.Release{
		ReleaseInRepo: *releaseInRepo,
	}

	err = s.fillRelease(ctx, release, param)
	if err != nil {
		return nil, err
	}

	return release, nil
}

// SyncRelease sync releases from source to repo.
func (s *svc) SyncRelease(ctx context.Context) error {
	logger := log.FromContext(ctx)
	logger.Info("syncing releases from source ...")

	// query all releases from source
	relsInJira, err := s.source.ListReleases(ctx)
	if err != nil {
		return fmt.Errorf("list releases failed: %s", err)
	}

	perBatch := 20
	for index := 0; index < len(relsInJira); index += perBatch {
		end := index + perBatch
		if end > len(relsInJira) {
			end = len(relsInJira)
		}
		relsInJira := relsInJira[index:end]

		// query releases from repo
		filter := repo.ReleaseOpts.NameIn(
			container.TransformSlice(relsInJira, func(r *relent.ReleaseInRepo) string { return r.Name }),
		)
		relsInRepo, err := s.repo.ListReleases(ctx, filter)
		if err != nil {
			return fmt.Errorf("list releases from db failed: %s", err)
		}

		for i := range relsInJira {
			var (
				relInJira = relsInJira[i]
				relInRepo *relent.ReleaseInRepo
			)

			foundIndex := slices.IndexFunc(relsInRepo, func(relInRepo *relent.ReleaseInRepo) bool {
				return relInRepo.Name == relInJira.Name
			})
			if foundIndex != -1 {
				relInRepo = relsInRepo[foundIndex]
			}

			err := s.syncOneRelease(ctx, relInJira, relInRepo)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *svc) fillRelease(ctx context.Context, release *relent.Release, param relsvcapi.GetReleaseParam) error {
	// fill tickets
	if param.WithTickets {
		tickets, err := s.repo.ListTickets(ctx, repo.TicketOpts.ReleaseIn(release.Name))
		if err != nil {
			return err
		}

		for _, ticket := range tickets {
			release.Tickets = append(release.Tickets, &relent.ReleaseTicket{
				ReleaseTicketInRepo: *ticket,
			})
		}
	}

	return nil
}

func (s *svc) syncOneRelease(ctx context.Context, relInJira *relent.ReleaseInRepo, relInRepo *relent.ReleaseInRepo) error {
	logger := log.FromContextOrDef(ctx, s.logger)

	// insert a release if not exists in db
	if relInJira != nil && relInRepo == nil {
		err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
			return s.repo.CreateRelease(ctx, relInJira)
		})
		if err != nil {
			return fmt.Errorf("insert release %s failed: %w", relInJira.Name, err)
		}
		logger.Info("inserted a new release to repo", "name", relInJira.Name)
		return nil
	}

	// do nothing if release is deleted in jira
	if relInJira == nil && relInRepo != nil {
		return nil
	}

	// update a release if exists in db and jira.
	if relInJira != nil && relInRepo != nil {
		now := time.Now()

		// if release window is passed, update release status to finished in repo
		if now.After(relInJira.WindowEndAt) && relInRepo.Status != relent.ReleaseStatusFinished {
			err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
				return s.repo.PatchRelease(ctx, relInRepo.ID,
					func(current *relent.ReleaseInRepo) error {
						current.Status = relent.ReleaseStatusFinished
						current.ClosedAt = pointer.Pointer(time.Now())
						return nil
					})
			})
			if err != nil {
				return fmt.Errorf("close release %s failed: %w", relInRepo.Name, err)
			}
			logger.Info("closed a release in repo", "name", relInRepo.Name)
			return nil
		}

		// if release is not closed in jira, update release info.
		if relInJira.Status != relent.ReleaseStatusFinished {
			updatedFields := []string{}
			if !relInRepo.WindowStartAt.Equal(relInJira.WindowStartAt) {
				updatedFields = append(updatedFields, "window_start_at")
			}
			if !relInRepo.WindowEndAt.Equal(relInJira.WindowEndAt) {
				updatedFields = append(updatedFields, "window_end_at")
			}
			if relInRepo.WindowStartAt.Before(now) &&
				relInRepo.WindowEndAt.After(now) &&
				relInRepo.Status != relent.ReleaseStatusOpen {
				updatedFields = append(updatedFields, "status")
			}

			if len(updatedFields) > 0 {
				err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
					return s.repo.PatchRelease(ctx, relInRepo.ID,
						func(current *relent.ReleaseInRepo) error {
							// update release window
							current.WindowStartAt = relInJira.WindowStartAt
							current.WindowEndAt = relInJira.WindowEndAt
							// update status to 'open' if now is in release window.
							if current.WindowStartAt.Before(now) && current.WindowEndAt.After(now) {
								current.Status = relent.ReleaseStatusOpen
							}
							return nil
						})
				})
				if err != nil {
					return fmt.Errorf("update release %s failed: %w", relInRepo.Name, err)
				}
				logger.Info("updated a release in repo", "name", relInRepo.Name, "updated_fields", updatedFields)
			}

			return nil
		}

	}

	return nil
}
