package jira

import (
	"context"
	"fmt"
	"strings"

	jiraent "github.com/tidbcloud/devops/entity/jira"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	jirasvcapi "github.com/tidbcloud/devops/pkg/service/jira/api"
	"github.com/tidbcloud/devops/pkg/service/jira/issueloader"
	"github.com/tidbcloud/devops/pkg/service/jira/repo"
)

func (s *svc) ListUnifiedIssues(ctx context.Context, param jirasvcapi.ListUnifiedIssuesParam) ([]*jiraent.UnifiedIssue, int64, error) {
	options := s.buildIssueOptionsFromParam(param.IssueInRepoFilter)

	count, err := s.repo.CountIssues(ctx, options...)
	if err != nil {
		return nil, 0, err
	}

	if param.Page > 0 && param.PerPage > 0 {
		options = append(options, repo.IssueOption.WithPager(int(param.Page), int(param.PerPage)))
	}

	issuesInRepo, err := s.repo.ListIssues(ctx, options...)
	if err != nil {
		return nil, 0, err
	}

	// transform to unified issues
	issues := container.TransformSlice(issuesInRepo, func(issue *jiraent.IssueInRepo) *jiraent.UnifiedIssue {
		return &jiraent.UnifiedIssue{
			Key:         issue.Key,
			IssueInRepo: issue,
		}
	})

	// inject issue in Jira
	if param.WithJira {
		keys := container.TransformSlice(issues, func(issue *jiraent.UnifiedIssue) string {
			return issue.Key
		})

		jql := fmt.Sprintf("key in (%s)", strings.Join(keys, ","))
		jiraIssues, _, err := s.issueLoader.LoadIssueInJiraByJQL(ctx, jql, &issueloader.LoadByJQLOptions{
			Fields: issueloader.IssueFieldOptions{
				WithComments: param.WithJiraComments,
			},
		})
		if err != nil {
			return nil, 0, fmt.Errorf("load issues from jira: %w", err)
		}

		for _, issue := range issues {
			for _, jiraIssue := range jiraIssues {
				if issue.Key == jiraIssue.Key {
					issue.IssueInJira = jiraIssue
					break
				}
			}
		}
	}

	// inject triage
	if param.WithTriage {
		keys := []string{}
		for _, issue := range issues {
			if issue.IssueInRepo.HaveTriage {
				keys = append(keys, issue.Key)
			}
		}

		triages, err := s.repo.ListIssueTriages(ctx, repo.IssueTriageOption.KeyIn(keys))
		if err != nil {
			return nil, 0, fmt.Errorf("list issue triages: %w", err)
		}

		triageMap := container.SliceToMap(triages, func(triage *jiraent.IssueTriage) string {
			return triage.Key
		})

		for _, issue := range issues {
			if triage, ok := triageMap[issue.Key]; ok {
				issue.IssueTriage = triage
			}
		}
	}

	// inject timeline summary
	if param.WithTimelineSummary {
		for _, issue := range issues {
			if issue.IssueInRepo.HaveTimelineSummary {
				summary, err := s.repo.GetIssueTimelineSummary(ctx, issue.Key)
				if err != nil {
					return nil, 0, fmt.Errorf("get issue timeline summary: %w", err)
				}
				issue.IssueTimelineSummary = summary
			}
		}
	}

	return issues, count, nil
}

func (s *svc) GetUnifiedIssue(ctx context.Context, key string, param jirasvcapi.GetUnifiedIssueParam) (*jiraent.UnifiedIssue, error) {
	issueInRepo, err := s.repo.GetIssue(ctx, key)
	if err != nil {
		return nil, err
	}

	unifiedIssue := &jiraent.UnifiedIssue{
		Key:         issueInRepo.Key,
		IssueInRepo: issueInRepo,
	}

	if param.WithTriage && issueInRepo.HaveTriage {
		triage, err := s.repo.GetIssueTriage(ctx, key)
		if err != nil {
			return nil, fmt.Errorf("get issue triage: %w", err)
		}
		unifiedIssue.IssueTriage = triage
	}

	if param.WithTimelineSummary && issueInRepo.HaveTimelineSummary {
		summary, err := s.repo.GetIssueTimelineSummary(ctx, key)
		if err != nil {
			return nil, fmt.Errorf("get issue timeline summary: %w", err)
		}
		unifiedIssue.IssueTimelineSummary = summary
	}

	return unifiedIssue, nil
}

func (s *svc) SyncIssueSnapshotByKey(ctx context.Context, key string) (*jiraent.IssueInRepo, error) {
	snapshot, err := s.issueLoader.LoadIssueInRepoByKey(ctx, key)
	if err != nil {
		return nil, err
	}

	var issueInRepo *jiraent.IssueInRepo
	err = s.db.Transaction(ctx, func(txCtx context.Context) error {
		issueInRepo, err = s.upsertIssueSnapshot(txCtx, snapshot)
		return err
	})
	if err != nil {
		return nil, err
	}

	return issueInRepo, nil
}

func (s *svc) SyncIssueSnapshotByJQL(ctx context.Context, jql string, param jirasvcapi.SyncIssueSnapshotByJQLParam) (*jirasvcapi.StoreIssueInRepoByJQLResult, error) {
	snapshots, result, err := s.issueLoader.LoadIssueInRepoByJQL(ctx, jql, &issueloader.LoadByJQLOptions{
		MaxCount: param.MaxCount,
		FailFast: param.FailFast,
		PerPage:  param.PerPage,
	})
	if err != nil {
		return nil, err
	}

	err = s.db.Transaction(ctx, func(txCtx context.Context) error {
		for _, snapshot := range snapshots {
			_, err = s.upsertIssueSnapshot(txCtx, snapshot)
			if err != nil {
				return err
			}
		}
		return nil
	})

	resp := &jirasvcapi.StoreIssueInRepoByJQLResult{
		Total:         result.Total,
		SuccessIssues: result.SuccessIssues,
		FailureIssues: result.FailureIssues,
	}

	return resp, err
}

func (s *svc) UpsertIssueTriage(ctx context.Context, triage *jiraent.IssueTriage) (*jiraent.IssueTriage, error) {
	issue, err := s.SyncIssueSnapshotByKey(ctx, triage.Key)
	if err != nil {
		return nil, fmt.Errorf("sync issue failed: %w", err)
	}

	var createdTriage *jiraent.IssueTriage
	err = s.db.Transaction(ctx, func(txCtx context.Context) error {
		createdTriage, err = s.repo.UpsertIssueTriage(txCtx, triage)
		if err != nil {
			return fmt.Errorf("upsert issue triage failed: %w", err)
		}

		if !issue.HaveTriage {
			issue.HaveTriage = true
			_, err = s.repo.UpdateIssue(txCtx, issue)
			if err != nil {
				return fmt.Errorf("update issue failed: %w", err)
			}
		}

		return nil
	})

	return createdTriage, err
}

func (s *svc) SetIssueTimelineSummary(ctx context.Context, key string, summary *jiraent.IssueTimelineSummary) error {
	issue, err := s.SyncIssueSnapshotByKey(ctx, key)
	if err != nil {
		return fmt.Errorf("sync issue failed: %w", err)
	}

	err = s.db.Transaction(ctx, func(txCtx context.Context) error {
		if !issue.HaveTimelineSummary {
			issue.HaveTimelineSummary = true
			_, err = s.repo.UpdateIssue(txCtx, issue)
			if err != nil {
				return fmt.Errorf("update issue failed: %w", err)
			}
		}

		return s.repo.SetIssueTimelineSummary(txCtx, key, summary)
	})

	return err
}

func (s *svc) ListIssuesFromJira(ctx context.Context, jql string, param jirasvcapi.ListIssuesFromJiraParam) (*jirasvcapi.ListIssuesFromJiraResult, error) {
	issues, result, err := s.issueLoader.LoadIssueInJiraByJQLPaged(ctx, jql, &issueloader.LoadByJQLPagedOptions{
		Fields: issueloader.IssueFieldOptions{
			WithComments: param.WithComments,
		},
		PerPage:   param.PageSize,
		PageToken: param.PageToken,
	})
	if err != nil {
		return nil, err
	}

	return &jirasvcapi.ListIssuesFromJiraResult{
		Issues:        issues,
		IsLast:        result.IsLast,
		NextPageToken: result.NextPageToken,
	}, nil
}

func (s *svc) GetIssueFromJira(ctx context.Context, key string, param jirasvcapi.GetIssueFromJiraParam) (*jiraent.IssueInJira, error) {
	return s.issueLoader.LoadIssueInJiraByKey(ctx, key, issueloader.IssueFieldOptions{
		WithComments: param.WithComments,
	})
}

func (s *svc) buildIssueOptionsFromParam(param jirasvcapi.IssueInRepoFilter) []repoopt.Option {
	// build query options
	options := []repoopt.Option{}

	// issue filters
	if param.HaveTriage != nil {
		options = append(options, repo.IssueOption.HaveTriage(*param.HaveTriage))
	}
	if param.HaveTimelineSummary != nil {
		options = append(options, repo.IssueOption.HaveTimelineSummary(*param.HaveTimelineSummary))
	}
	if param.KeyIn != nil {
		options = append(options, repo.IssueOption.KeyIn(*param.KeyIn))
	}

	// snapshot filters
	snapshotFilters := []repoopt.Option{}
	if param.StatusIn != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.StatusIn(*param.StatusIn))
	}
	if param.CreatedAfter != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.CreatedAfter(*param.CreatedAfter))
	}
	if param.CreatedBefore != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.CreatedBefore(*param.CreatedBefore))
	}
	if param.PriorityIn != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.PriorityIn(*param.PriorityIn))
	}
	if param.ProjectKeyIn != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.ProjectKeyIn(*param.ProjectKeyIn))
	}
	if param.ComponentIn != nil {
		snapshotFilters = append(snapshotFilters, repo.IssueSnapshotOption.ComponentIn(*param.ComponentIn))
	}
	if len(snapshotFilters) > 0 {
		options = append(options, repo.IssueOption.FilterSnapshot(snapshotFilters...))
	}

	// customer info filters
	customerFilters := []repoopt.Option{}
	if param.CustomerEnvIs != nil {
		customerFilters = append(customerFilters, repo.IssueCustomerInfoOption.EnvironmentIs(*param.CustomerEnvIs))
	}
	if param.ClusterIDIn != nil {
		customerFilters = append(customerFilters, repo.IssueCustomerInfoOption.ClusterIDIn(*param.ClusterIDIn))
	}
	if param.TenantIDIn != nil {
		customerFilters = append(customerFilters, repo.IssueCustomerInfoOption.TenantIDIn(*param.TenantIDIn))
	}
	if len(customerFilters) > 0 {
		options = append(options, repo.IssueOption.FilterCustomerInfo(customerFilters...))
	}

	return options
}

func (s *svc) upsertIssueSnapshot(ctx context.Context, snapshot *jiraent.IssueSnapshot) (*jiraent.IssueInRepo, error) {
	issue, err := s.repo.GetIssue(ctx, snapshot.Key)
	if err != nil && !errs.IsNotFoundInDB(err) {
		return nil, err
	}

	if errs.IsNotFoundInDB(err) {
		// create a new issue
		issue = &jiraent.IssueInRepo{
			Key: snapshot.Key,

			Snapshot: snapshot,
		}

		return s.repo.CreateIssue(ctx, issue)
	}

	issue.Snapshot = snapshot
	return s.repo.UpdateIssue(ctx, issue)
}
