package github

import (
	"context"
	"errors"
	"fmt"
	"sync"

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

	"slices"

	"gorm.io/gorm"
)

func (s *svc) WatchPullRequest(ctx context.Context, locations []ent.PRLocation) error {
	// find prs not exist in db
	prsInDB, err := s.repo.ListPullRequest(ctx, repo.PROption.LocationIn(locations...))
	if err != nil {
		return err
	}

	missedPRLocs := make([]ent.PRLocation, 0, len(locations))
	for _, loc := range locations {
		index := slices.IndexFunc(prsInDB, func(prInDB *ent.PullRequestInDB) bool {
			return prInDB.Location == loc
		})
		if index == -1 {
			missedPRLocs = append(missedPRLocs, loc)
		}
	}

	if len(missedPRLocs) == 0 {
		return nil
	}

	// collect prs from github
	mu := sync.Mutex{}
	errs := []error{}
	missPRs := make([]*githubcli.PullRequest, 0, len(missedPRLocs))
	parallel := workqueue.NewParallelizer(missedPRLocs, 10, 1)
	parallel.Do(ctx, func(ctx context.Context, job []ent.PRLocation) {
		for _, loc := range job {
			owner, repo, number := loc.Parse()

			prInGh, err := s.cli.PullRequest.Get(ctx, owner, repo, int(number))
			if err != nil {
				mu.Lock()
				errs = append(errs, fmt.Errorf("get pr %s failed: %w", loc, err))
				mu.Unlock()
				return
			}

			mu.Lock()
			missPRs = append(missPRs, prInGh)
			mu.Unlock()
		}
	})
	if len(errs) > 0 {
		return errors.Join(errs...)
	}

	s.Logger(ctx).Info("add new prs to watch", "locs", missedPRLocs)

	// save prs to db
	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		for _, prInGh := range missPRs {
			pr := ConvertPullRequest(*prInGh)
			_, err := s.repo.CreatePullRequest(ctx, &pr.PullRequestInDB)
			if err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return err
	}

	return nil
}

func (s *svc) ListPullRequests(ctx context.Context, param ListPullRequestsParam) ([]*ent.PullRequest, int64, error) {
	param = param.Normalize()

	opts := []repoopt.Option{}
	if param.RepoLocation != nil {
		opts = append(opts, repo.PROption.RepoIs(*param.RepoLocation))
	}
	if len(param.PRLocations) > 0 {
		opts = append(opts, repo.PROption.LocationIn(param.PRLocations...))
	}

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

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

	prs := make([]*ent.PullRequest, 0, len(prsInDB))
	for _, prInDB := range prsInDB {
		prs = append(prs, &ent.PullRequest{
			PullRequestInDB: *prInDB,
		})
	}
	return prs, total, nil
}

func (s *svc) GetPullRequestByLoc(ctx context.Context, loc ent.PRLocation) (*ent.PullRequest, error) {
	prsInDB, err := s.repo.ListPullRequest(ctx, repo.PROption.LocationIn(loc))
	if err != nil {
		return nil, err
	}
	if len(prsInDB) == 0 {
		return nil, gorm.ErrRecordNotFound
	}

	prInDB := prsInDB[0]
	return &ent.PullRequest{
		PullRequestInDB: *prInDB,
	}, nil
}

func (s *svc) SyncPullRequests(ctx context.Context) error {
	logger := s.Logger(ctx)
	logger.Info("syncing pull requests ...")

	// query watched prs from db
	page := 0
	perPage := 20
	for {
		page++

		prsInDB, err := s.repo.ListPullRequest(ctx,
			repo.PROption.StateIn("OPEN"), // only sync open prs now
			repo.PROption.WithPager(page, perPage),
		)
		if err != nil {
			return err
		}
		if len(prsInDB) == 0 {
			break
		}

		// query prs from github
		mu := sync.Mutex{}
		prsInGh := map[ent.PRLocation]*githubcli.PullRequest{}
		parallel := workqueue.NewParallelizer(prsInDB, 10, 1)
		parallel.Do(ctx, func(ctx context.Context, job []*ent.PullRequestInDB) {
			for _, prInDB := range job {
				loc := prInDB.Location

				prInGh, err := s.cli.PullRequest.Get(ctx, prInDB.Owner, prInDB.Repo, int(prInDB.Number))
				if err != nil {
					logger.Info("fail to get pr from github so skip to sync", "err", err.Error())
					continue
				}

				mu.Lock()
				prsInGh[loc] = prInGh
				mu.Unlock()
			}
		})

		// sync prs
		for _, prInDB := range prsInDB {
			prInGh := prsInGh[prInDB.Location]
			err := s.syncOnePullRequest(ctx, prInGh, prInDB)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *svc) syncOnePullRequest(ctx context.Context, prInGh *githubcli.PullRequest, prInDB *ent.PullRequestInDB) error {
	if prInGh == nil || prInDB == nil {
		return nil // nothing we can do
	}

	logger := s.Logger(ctx)

	updatedFields := []string{}
	if prInGh.Title != prInDB.Title {
		updatedFields = append(updatedFields, "title")
	}
	if string(prInGh.State) != prInDB.State {
		updatedFields = append(updatedFields, "state")
	}
	if prInGh.UpdatedAt.Unix() != prInDB.UpdatedAt {
		updatedFields = append(updatedFields, "updated_at")
	}

	if len(updatedFields) > 0 {
		err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
			return s.repo.PatchPullRequest(ctx, prInDB, func(current *ent.PullRequestInDB) error {
				current.Title = prInGh.Title
				current.State = string(prInGh.State)
				current.UpdatedAt = prInGh.UpdatedAt.Unix()
				return nil
			})
		})
		if err != nil {
			return fmt.Errorf("update a repo %s in db failed: %w", prInDB.Location, err)
		}
		logger.Info("update a repo in db success", "loc", prInDB.Location, "id", prInDB.ID, "updated_fields", updatedFields)
	}

	return nil
}
