package release

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

	relent "github.com/tidbcloud/devops/entity/release"
	"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"
	"github.com/tidbcloud/devops/pkg/service/github"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"
	"github.com/tidbcloud/devops/pkg/service/release/source"
)

func (s *svc) GetTicket(ctx context.Context, name string, param relsvcapi.GetTicketParam) (*relent.ReleaseTicket, error) {
	ticketInRepo, err := s.repo.GetTicket(ctx, name)
	if err != nil {
		return nil, err
	}

	ticket := &relent.ReleaseTicket{
		ReleaseTicketInRepo: *ticketInRepo,
	}
	if param.WithGithubPR && len(ticket.GithubPRLocations) != 0 {
		prs, _, err := s.ghSvc.ListPullRequests(ctx, github.ListPullRequestsParam{
			PRLocations: ticket.GithubPRLocations,
			Page:        1,
			PerPage:     100, // give a large enough number
		})
		if err != nil {
			return nil, err
		}
		ticket.GithubPRs = prs
	}

	return ticket, nil
}

func (s *svc) CloseTickets(ctx context.Context, ticketNames []string) error {
	if len(ticketNames) == 0 {
		return nil
	}

	tickets, err := s.repo.ListTickets(ctx, repo.TicketOpts.NameIn(ticketNames))
	if err != nil {
		return fmt.Errorf("list tickets failed: %w", err)
	}

	for _, ticket := range tickets {
		if ticket.Status == relent.ReleaseTicketStatusDone {
			continue
		}

		// update ticket status in repo
		err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
			return s.repo.PatchTicket(ctx, ticket, func(current *relent.ReleaseTicketInRepo) error {
				ticket.Status = relent.ReleaseTicketStatusDone
				return nil
			})
		})
		if err != nil {
			return err
		}

		// for dm tickets, change dm ticket status
		if ticket.Type == relent.ReleaseTicketTypeDM {
			go func(tk *relent.ReleaseTicketInRepo) {
				err := s.source.UpdateTicketStatus(ctx, tk.Name, relent.ReleaseTicketStatusDone)
				if err != nil {
					s.logger.Error(err, "change ticket status in source failed", "ticket", tk.Name)
				}
			}(ticket)
		}

	}

	return nil
}

// UpdateTicket update ticket in repo.
// Only update part of fields and the info about DM tickets is updated in syncTickets.
func (s *svc) UpdateTicket(ctx context.Context, name string, param relsvcapi.UpdateTicketParam) error {
	ticketInRepo, err := s.repo.GetTicket(ctx, name)
	if err != nil {
		return err
	}

	err = s.dbp.Transaction(ctx, func(ctx context.Context) error {
		return s.repo.PatchTicket(ctx, ticketInRepo, func(current *relent.ReleaseTicketInRepo) error {
			if param.GithubPRLocations != nil {
				// save interested prs to db
				err := s.ghSvc.WatchPullRequest(ctx, *param.GithubPRLocations)
				if err != nil {
					return fmt.Errorf("watch prs failed: %w", err)
				}

				current.GithubPRLocations = *param.GithubPRLocations
			}
			return nil
		})
	})

	return err
}

func (s *svc) ListTickets(ctx context.Context, param relsvcapi.ListTicketsParam) ([]*relent.ReleaseTicket, int64, error) {
	param = param.Normalize()

	opts := []repoopt.Option{}
	if len(param.Names) != 0 {
		opts = append(opts, repo.TicketOpts.NameIn(param.Names))
	}
	if len(param.Statuses) != 0 {
		opts = append(opts, repo.TicketOpts.StatusIn(param.Statuses))
	}
	if len(param.Releases) != 0 {
		opts = append(opts, repo.TicketOpts.ReleaseIn(param.Releases...))
	}

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

	if param.Page != 0 {
		opts = append(opts, repo.TicketOpts.WithPager(param.Page, param.PerPage))
	}

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

	tickets := make([]*relent.ReleaseTicket, 0, len(ticketsInRepo))
	for _, ticketInRepo := range ticketsInRepo {
		ticket := &relent.ReleaseTicket{
			ReleaseTicketInRepo: *ticketInRepo,
		}
		tickets = append(tickets, ticket)
	}

	return tickets, total, nil
}

// SyncTickets list tickets from jira and sync them to repo.
func (s *svc) WatchTickets(ctx context.Context) error {
	logger := log.FromContext(ctx)

	logger.Info("syncing tickets from source ...")

	names, err := s.source.SearchTickets(ctx, source.SearchTicketOptions{
		UpdatedAtGTE: pointer.Pointer(time.Now().AddDate(0, 0, -s.conf.WarchTicketsUpdatedDaysAgo)),
	})
	if err != nil {
		return err
	}

	s.syncTicketCh <- names
	return nil
}

func (s *svc) SyncTicketsAsync(ctx context.Context, names []string) error {
	s.syncTicketCh <- names
	return nil
}

func (s *svc) SyncTickets(ctx context.Context, names []string) error {
	logger := log.FromContextOrDef(ctx, s.logger)
	ticketsInJira := make([]*relent.ReleaseTicketInRepo, 0, len(names))

	// query tickets from jira
	for _, name := range names {
		ticketInJira, err := s.source.GetTicket(ctx, name)
		if err != nil {
			logger.Error(err, "ignore an issue cause error", "issue", name)
			continue
		}
		ticketsInJira = append(ticketsInJira, &ticketInJira)
	}

	// sync tickets
	perBatch := 50
	for index := 0; index < len(ticketsInJira); index += perBatch {
		end := index + perBatch
		if end > len(ticketsInJira) {
			end = len(ticketsInJira)
		}
		ticketsInJira := ticketsInJira[index:end]

		// query tickets from repo
		filter := repo.TicketOpts.NameIn(
			container.TransformSlice(ticketsInJira, func(t *relent.ReleaseTicketInRepo) string { return t.Name }),
		)
		ticketsInRepo, err := s.repo.ListTickets(ctx, filter)
		if err != nil {
			return fmt.Errorf("list tickets from db failed: %s", err)
		}

		// sync per ticket
		for i := range ticketsInJira {
			var (
				ticketInJira = ticketsInJira[i]
				ticketInRepo *relent.ReleaseTicketInRepo
			)

			foundIndex := slices.IndexFunc(ticketsInRepo, func(ticketInRepo *relent.ReleaseTicketInRepo) bool {
				return ticketInRepo.Name == ticketInJira.Name
			})
			if foundIndex != -1 {
				ticketInRepo = ticketsInRepo[foundIndex]
			}

			err := s.syncOneTicket(ctx, ticketInJira, ticketInRepo)
			if err != nil {
				return err
			}
		}

	}

	return nil
}

func (s *svc) syncOneTicket(ctx context.Context, ticketInJira, ticketInRepo *relent.ReleaseTicketInRepo) error {
	logger := log.FromContextOrDef(ctx, s.logger)

	// insert a new ticket if not exists in repo
	if ticketInJira != nil && ticketInRepo == nil {
		err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
			_, err := s.repo.CreateTicket(ctx, ticketInJira)
			return err
		})
		if err != nil {
			return fmt.Errorf("insert ticket %s failed: %s", ticketInJira.Name, err)
		}
		logger.Info("insert ticket", "ticket", ticketInJira.Name)
		return nil
	}

	// do nothing if exists in repo but not getted in jira
	// Maybe the ticket is too old and has not been queried so that `ticketInJira` is nil.
	if ticketInJira == nil && ticketInRepo != nil {
		logger.Info("ignore a ticket not found in jira", "ticket", ticketInRepo.Name)
	}

	// update ticket if exists in repo and jira
	if ticketInJira != nil && ticketInRepo != nil {
		updatedFields := []string{}
		if ticketInJira.Summary != ticketInRepo.Summary {
			updatedFields = append(updatedFields, "summary")
		}
		if ticketInJira.Status != ticketInRepo.Status {
			updatedFields = append(updatedFields, "status")
		}
		if ticketInJira.Type != ticketInRepo.Type {
			updatedFields = append(updatedFields, "type")
		}
		if ticketInJira.AssigneeEmail != ticketInRepo.AssigneeEmail {
			updatedFields = append(updatedFields, "assignee_email")
		}
		if pointer.Deref(ticketInJira.ReleaseName, "unknown") !=
			pointer.Deref(ticketInRepo.ReleaseName, "unknown") {
			updatedFields = append(updatedFields, "release_name")
		}

		if len(updatedFields) != 0 {
			err := s.dbp.Transaction(ctx, func(ctx context.Context) error {
				return s.repo.PatchTicket(ctx, ticketInRepo, func(current *relent.ReleaseTicketInRepo) error {
					current.Summary = ticketInJira.Summary
					current.Status = ticketInJira.Status
					current.Type = ticketInJira.Type
					current.AssigneeEmail = ticketInJira.AssigneeEmail
					current.ReleaseName = ticketInJira.ReleaseName
					return nil
				})
			})
			if err != nil {
				return fmt.Errorf("update ticket %s failed: %s", ticketInJira.Name, err)
			}
			logger.Info("updated a ticket", "ticket", ticketInJira.Name, "fields", updatedFields)
		}
	}

	return nil
}
