package service

import (
	"context"
	"fmt"
	"strings"

	"github.com/pingcap/agent-git-service/internal/db"
)

const (
	RepositorySyncHealthSyncing          = "syncing"
	RepositorySyncHealthWaitingForGitHub = "waiting_for_github"
	RepositorySyncHealthNeedsAttention   = "needs_attention"
	RepositorySyncHealthHealthy          = "healthy"
	RepositorySyncHealthPaused           = "paused"
	RepositorySyncHealthNotStarted       = "not_started"
	RepositorySyncHealthClosed           = "closed"
)

// RepositorySyncOverview is the aggregate read model used by the global sync
// management page. It keeps the web client from issuing one runs request and
// one progress request for every visible binding.
type RepositorySyncOverview struct {
	Summary     RepositorySyncOverviewSummary
	Connections []RepositorySyncOverviewConnection
}

type RepositorySyncOverviewSummary struct {
	Connections      int
	Running          int
	WaitingForGitHub int
	NeedsAttention   int
	Healthy          int
	Paused           int
	NotStarted       int
	Closed           int
}

type RepositorySyncOverviewConnection struct {
	Binding   db.SyncBinding
	LatestRun *db.SyncRun
	ActiveRun *db.SyncRun
	Progress  *RepositorySyncRunProgress
	Health    string
}

type ListRepositorySyncActivityInput struct {
	Status  string
	AfterID uint
	Limit   int
}

type RepositorySyncActivityItem struct {
	Binding db.SyncBinding
	Run     db.SyncRun
}

type RepositorySyncActivityPage struct {
	Items       []RepositorySyncActivityItem
	NextAfterID uint
	HasMore     bool
}

// GetRepositorySyncOverview returns every sync binding for which the viewer is
// an AGS repository administrator, together with the latest run and batched
// RunItem progress for active runs.
func (s *Service) GetRepositorySyncOverview(ctx context.Context) (RepositorySyncOverview, error) {
	bindings, err := s.listRepositorySyncBindingsForViewer(ctx)
	if err != nil {
		return RepositorySyncOverview{}, err
	}

	result := RepositorySyncOverview{
		Connections: make([]RepositorySyncOverviewConnection, 0, len(bindings)),
	}
	if len(bindings) == 0 {
		return result, nil
	}

	lastRunIDs := make([]uint, 0, len(bindings))
	for _, binding := range bindings {
		if binding.LastRunID != nil {
			lastRunIDs = append(lastRunIDs, *binding.LastRunID)
		}
	}

	latestRuns := make(map[uint]db.SyncRun, len(lastRunIDs))
	if len(lastRunIDs) != 0 {
		var runs []db.SyncRun
		if err := s.DBForCtx(ctx).Where("id IN ?", lastRunIDs).Find(&runs).Error; err != nil {
			return RepositorySyncOverview{}, wrapErr(err)
		}
		for _, run := range runs {
			latestRuns[run.ID] = run
		}
	}

	activeRunIDs := make([]uint, 0, len(latestRuns))
	for _, run := range latestRuns {
		if isRepositorySyncRunActive(run.Status) {
			activeRunIDs = append(activeRunIDs, run.ID)
		}
	}
	progressByRun, err := s.loadRepositorySyncRunProgresses(ctx, activeRunIDs)
	if err != nil {
		return RepositorySyncOverview{}, err
	}

	for _, binding := range bindings {
		connection := RepositorySyncOverviewConnection{Binding: binding}
		if binding.LastRunID != nil {
			if latest, ok := latestRuns[*binding.LastRunID]; ok {
				latestCopy := latest
				connection.LatestRun = &latestCopy
				if isRepositorySyncRunActive(latest.Status) {
					activeCopy := latest
					connection.ActiveRun = &activeCopy
					if progress, found := progressByRun[latest.ID]; found {
						progressCopy := progress
						connection.Progress = &progressCopy
					}
				}
			}
		}
		connection.Health = repositorySyncConnectionHealth(binding, connection.LatestRun)
		result.Connections = append(result.Connections, connection)
		result.Summary.Connections++
		switch connection.Health {
		case RepositorySyncHealthSyncing:
			result.Summary.Running++
		case RepositorySyncHealthWaitingForGitHub:
			result.Summary.WaitingForGitHub++
		case RepositorySyncHealthNeedsAttention:
			result.Summary.NeedsAttention++
		case RepositorySyncHealthHealthy:
			result.Summary.Healthy++
		case RepositorySyncHealthPaused:
			result.Summary.Paused++
		case RepositorySyncHealthNotStarted:
			result.Summary.NotStarted++
		case RepositorySyncHealthClosed:
			result.Summary.Closed++
		}
	}
	return result, nil
}

// ListRepositorySyncActivity returns recent runs across every binding the
// viewer can administer. Results use a descending ID cursor so new runs do not
// shift later pages while the user is browsing history.
func (s *Service) ListRepositorySyncActivity(ctx context.Context, in ListRepositorySyncActivityInput) (RepositorySyncActivityPage, error) {
	bindings, err := s.listRepositorySyncBindingsForViewer(ctx)
	if err != nil {
		return RepositorySyncActivityPage{}, err
	}
	if in.Limit <= 0 || in.Limit > 100 {
		in.Limit = 30
	}
	if len(bindings) == 0 {
		return RepositorySyncActivityPage{Items: []RepositorySyncActivityItem{}}, nil
	}

	bindingIDs := make([]uint, 0, len(bindings))
	bindingByID := make(map[uint]db.SyncBinding, len(bindings))
	for _, binding := range bindings {
		bindingIDs = append(bindingIDs, binding.ID)
		bindingByID[binding.ID] = binding
	}

	query := s.DBForCtx(ctx).Model(&db.SyncRun{}).
		Where("binding_id IN ?", bindingIDs).
		Order("id DESC")
	if in.AfterID != 0 {
		query = query.Where("id < ?", in.AfterID)
	}
	statusMode, statuses, err := normalizedRepositorySyncActivityStatus(in.Status)
	if err != nil {
		return RepositorySyncActivityPage{}, err
	}
	if statusMode == "exact" {
		query = query.Where("status = ?", statuses[0])
	} else if statusMode == "group" {
		query = query.Where("status IN ?", statuses)
	}

	var runs []db.SyncRun
	if err := query.Limit(in.Limit + 1).Find(&runs).Error; err != nil {
		return RepositorySyncActivityPage{}, wrapErr(err)
	}
	hasMore := len(runs) > in.Limit
	if hasMore {
		runs = runs[:in.Limit]
	}
	items := make([]RepositorySyncActivityItem, 0, len(runs))
	for _, run := range runs {
		binding, ok := bindingByID[run.BindingID]
		if !ok {
			continue
		}
		items = append(items, RepositorySyncActivityItem{Binding: binding, Run: run})
	}
	var nextAfterID uint
	if hasMore && len(runs) != 0 {
		nextAfterID = runs[len(runs)-1].ID
	}
	return RepositorySyncActivityPage{Items: items, NextAfterID: nextAfterID, HasMore: hasMore}, nil
}

// listRepositorySyncBindingsForViewer resolves authorization once for the
// aggregate endpoints, then uses immutable AGS endpoint IDs to fetch bindings
// in bulk. Detail endpoints continue to use per-binding authorization.
func (s *Service) listRepositorySyncBindingsForViewer(ctx context.Context) ([]db.SyncBinding, error) {
	viewer, ok := UserFromContext(ctx)
	if !ok || viewer.ID == 0 {
		return nil, ErrUnauthorized
	}
	repositories, err := s.ListViewerRepos(ctx)
	if err != nil {
		return nil, err
	}
	adminRepositoryIDs := make([]uint, 0, len(repositories))
	for _, repository := range repositories {
		if repository.Permission.AtLeast(RepoPermissionAdmin) {
			adminRepositoryIDs = append(adminRepositoryIDs, repository.Repository.ID)
		}
	}
	if len(adminRepositoryIDs) == 0 {
		return []db.SyncBinding{}, nil
	}

	var bindingIDs []uint
	if err := s.DBForCtx(ctx).Model(&db.SyncBindingEndpoint{}).
		Where("provider = ? AND ags_repository_id IN ?", SyncProviderAGS, adminRepositoryIDs).
		Order("binding_id DESC").
		Pluck("binding_id", &bindingIDs).Error; err != nil {
		return nil, wrapErr(err)
	}
	if len(bindingIDs) == 0 {
		return []db.SyncBinding{}, nil
	}
	var bindings []db.SyncBinding
	if err := s.DBForCtx(ctx).Preload("Endpoints").
		Where("id IN ?", bindingIDs).
		Order("id DESC").
		Find(&bindings).Error; err != nil {
		return nil, wrapErr(err)
	}
	return bindings, nil
}

type repositorySyncProgressAggregateRow struct {
	RunID      uint
	ObjectType string
	State      string
	Count      int64
}

func (s *Service) loadRepositorySyncRunProgresses(ctx context.Context, runIDs []uint) (map[uint]RepositorySyncRunProgress, error) {
	result := make(map[uint]RepositorySyncRunProgress, len(runIDs))
	if len(runIDs) == 0 {
		return result, nil
	}
	var rows []repositorySyncProgressAggregateRow
	if err := s.DBForCtx(ctx).Model(&db.SyncRunItem{}).
		Select("run_id, object_type, state, COUNT(*) AS count").
		Where("run_id IN ?", runIDs).
		Group("run_id, object_type, state").
		Scan(&rows).Error; err != nil {
		return nil, wrapErr(err)
	}
	for _, runID := range runIDs {
		result[runID] = RepositorySyncRunProgress{ByObjectType: map[string]RepositorySyncRunProgressCounts{}}
	}
	for _, row := range rows {
		progress := result[row.RunID]
		progress.Total += row.Count
		byType := progress.ByObjectType[row.ObjectType]
		byType.Total += row.Count
		addRepositorySyncProgressCount(&progress, &byType, row.State, row.Count)
		progress.ByObjectType[row.ObjectType] = byType
		result[row.RunID] = progress
	}
	for runID, progress := range result {
		progress.Processed = progress.Completed + progress.Blocked + progress.Failed + progress.Superseded
		progress.Remaining = progress.Total - progress.Processed
		for objectType, counts := range progress.ByObjectType {
			counts.Processed = counts.Completed + counts.Blocked + counts.Failed + counts.Superseded
			counts.Remaining = counts.Total - counts.Processed
			progress.ByObjectType[objectType] = counts
		}
		result[runID] = progress
	}
	return result, nil
}

func addRepositorySyncProgressCount(progress *RepositorySyncRunProgress, byType *RepositorySyncRunProgressCounts, state string, count int64) {
	switch state {
	case SyncRunItemStatePlanned:
		progress.Planned += count
		byType.Planned += count
	case SyncRunItemStateApplying:
		progress.Applying += count
		byType.Applying += count
	case SyncRunItemStateVerifying:
		progress.Verifying += count
		byType.Verifying += count
	case SyncRunItemStateCompleted:
		progress.Completed += count
		byType.Completed += count
	case SyncRunItemStateBlocked:
		progress.Blocked += count
		byType.Blocked += count
	case SyncRunItemStateFailed:
		progress.Failed += count
		byType.Failed += count
	case SyncRunItemStateSuperseded:
		progress.Superseded += count
		byType.Superseded += count
	}
}

func repositorySyncConnectionHealth(binding db.SyncBinding, latest *db.SyncRun) string {
	switch binding.Status {
	case SyncBindingStatusClosed:
		return RepositorySyncHealthClosed
	case SyncBindingStatusPaused:
		return RepositorySyncHealthPaused
	}
	if latest != nil {
		if latest.RetryAfterAt != nil && latest.Status == SyncRunStatusQueued {
			return RepositorySyncHealthWaitingForGitHub
		}
		if isRepositorySyncRunActive(latest.Status) {
			return RepositorySyncHealthSyncing
		}
		if repositorySyncRunNeedsAttention(*latest) {
			return RepositorySyncHealthNeedsAttention
		}
	}
	if binding.LastConvergedRunID == nil {
		return RepositorySyncHealthNotStarted
	}
	return RepositorySyncHealthHealthy
}

func isRepositorySyncRunActive(status string) bool {
	return status == SyncRunStatusQueued || status == SyncRunStatusRunning
}

func repositorySyncRunNeedsAttention(run db.SyncRun) bool {
	if run.ErrorCode != "" || run.ErrorSummary != "" {
		return true
	}
	switch run.Status {
	case SyncRunStatusCompletedWithPendingChanges, SyncRunStatusRequiresAttention,
		SyncRunStatusBlocked, SyncRunStatusFailed, SyncRunStatusCancelled:
		return true
	default:
		return false
	}
}

func normalizedRepositorySyncActivityStatus(status string) (string, []string, error) {
	status = strings.ToLower(strings.TrimSpace(status))
	switch status {
	case "", "all":
		return "", nil, nil
	case "active", "running":
		return "group", syncActiveRunStatuses(), nil
	case "attention", "needs_attention":
		return "group", []string{
			SyncRunStatusCompletedWithPendingChanges, SyncRunStatusRequiresAttention,
			SyncRunStatusBlocked, SyncRunStatusFailed, SyncRunStatusCancelled,
		}, nil
	case SyncRunStatusCompleted, SyncRunStatusQueued,
		SyncRunStatusCompletedWithPendingChanges, SyncRunStatusRequiresAttention,
		SyncRunStatusBlocked, SyncRunStatusFailed, SyncRunStatusCancelled,
		SyncRunStatusSuperseded:
		return "exact", []string{status}, nil
	default:
		return "", nil, fmt.Errorf("%w: unsupported activity status %q", ErrValidation, status)
	}
}
