package server

import (
	"context"
	"errors"
	"log/slog"
	"time"

	"github.com/pingcap/agent-git-service/config"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/sync/githubapp"
	"github.com/pingcap/agent-git-service/internal/sync/provider"
	"github.com/pingcap/agent-git-service/internal/sync/provider/ags"
	githubprovider "github.com/pingcap/agent-git-service/internal/sync/provider/github"
	syncworker "github.com/pingcap/agent-git-service/internal/sync/worker"
	"github.com/pingcap/agent-git-service/internal/tenant"
	"github.com/pingcap/agent-git-service/internal/wikicatalog"
)

const (
	wikiCatalogGCInterval    = time.Hour
	wikiCatalogGCTimeout     = 5 * time.Minute
	wikiCatalogGCPendingTTL  = time.Hour
	wikiCatalogGCRefcountTTL = time.Hour
)

func startRuntimeWorkers(deps *bootstrapDeps) {
	if deps == nil {
		return
	}
	startWikiCatalogGCWorkers(deps.SvcDeps, deps.Registry)
	startTenantRuntimeWorkers(deps.SvcDeps, deps.Registry)
	configureRepositorySync(deps, false)
}

func configureRepositorySync(deps *bootstrapDeps, startWorkers bool) {
	if deps == nil || deps.SvcDeps == nil || deps.Registry == nil {
		return
	}
	configs := make(map[string]config.TenantRepositorySyncConfig, len(deps.Cfg.Tenants))
	for _, tenantConfig := range deps.Cfg.Tenants {
		configs[tenantConfig.ID] = tenantConfig.RepositorySync
	}
	for _, id := range deps.Registry.ids {
		workerConfig := configs[string(id)]
		if !workerConfig.Enabled {
			continue
		}
		resources, err := deps.Registry.ForTenant(id)
		if err != nil {
			slog.Error("repository sync worker not started", "tenant_id", id, "error", err)
			continue
		}
		resolver, err := githubapp.New(githubapp.Config{
			APIURL: workerConfig.GitHubAPIURL, WebURL: workerConfig.GitHubWebURL,
			AppSlug: workerConfig.GitHubAppSlug, AppID: workerConfig.GitHubAppID,
			PrivateKey: workerConfig.GitHubPrivateKey, ClientID: workerConfig.GitHubClientID,
			ClientSecret: workerConfig.GitHubClientSecret,
		})
		if err != nil {
			slog.Error("repository sync worker not started", "tenant_id", id, "error", err)
			continue
		}
		factory := &provider.Factory{AGS: &ags.Factory{Service: deps.SvcDeps, MaxConcurrency: workerConfig.MaxConcurrency}, GitHub: &githubprovider.Factory{Tokens: resolver, MaxConcurrency: workerConfig.MaxConcurrency, MaxAssetBytes: workerConfig.MaxAssetBytes}}
		deps.SvcDeps.SetRepositorySyncConnectionValidator(id, factory.GitHub)
		deps.SvcDeps.SetRepositorySyncFullAuditInterval(id, workerConfig.FullAuditInterval)
		if !startWorkers {
			continue
		}
		worker, err := syncworker.New(deps.SvcDeps, factory, syncworker.Options{PollInterval: workerConfig.PollInterval, LeaseTTL: workerConfig.LeaseTTL})
		if err != nil {
			slog.Error("repository sync worker not started", "tenant_id", id, "error", err)
			continue
		}
		ctx := tenant.WithInfo(deps.SvcDeps.ServerCtx(), resources.Info)
		deps.SvcDeps.Wg.Add(1)
		go func() {
			defer deps.SvcDeps.Wg.Done()
			if err := worker.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
				slog.ErrorContext(ctx, "repository sync worker stopped", "tenant_id", id, "error", err)
			}
		}()
	}
}

func startTenantRuntimeWorkers(svc *service.Service, registry *tenantRegistry) {
	if svc == nil || registry == nil {
		return
	}
	for _, id := range registry.ids {
		resources, err := registry.ForTenant(id)
		if err != nil {
			slog.Warn("wiki search projection worker not started", "tenant_id", id, "error", err)
			continue
		}
		ctx := tenant.WithInfo(svc.ServerCtx(), resources.Info)
		if err := svc.StartWikiSearchProjectionWorker(ctx); err != nil {
			slog.WarnContext(ctx, "wiki search projection worker not started", "tenant_id", id, "error", err)
		}
		if err := svc.StartWikiReferenceEffectsRecoveryWorker(ctx); err != nil {
			slog.WarnContext(ctx, "wiki reference recovery worker not started", "tenant_id", id, "error", err)
		}
		if err := svc.StartWikiReconcileWorker(ctx); err != nil {
			slog.ErrorContext(ctx, "start wiki reconcile worker", "error", err)
		}
		if err := svc.StartPullRequestMergeRecovery(ctx); err != nil {
			slog.WarnContext(ctx, "pull request merge recovery worker not started", "tenant_id", id, "error", err)
		}
	}
}

func startWikiCatalogGCWorkers(svc *service.Service, registry *tenantRegistry) {
	if svc == nil || registry == nil {
		return
	}
	for _, id := range registry.ids {
		resources, err := registry.ForTenant(id)
		if err != nil || resources.WikiCatalog == nil {
			continue
		}
		ctx := tenant.WithInfo(svc.ServerCtx(), resources.Info)
		svc.Wg.Add(1)
		go func(id tenant.ID, catalog *wikicatalog.Catalog, tenantCtx context.Context) {
			defer svc.Wg.Done()
			ticker := time.NewTicker(wikiCatalogGCInterval)
			defer ticker.Stop()
			for {
				select {
				case <-tenantCtx.Done():
					return
				case <-ticker.C:
					runWikiCatalogGC(tenantCtx, id, catalog)
				}
			}
		}(id, resources.WikiCatalog, ctx)
	}
}

func runWikiCatalogGC(parent context.Context, id tenant.ID, catalog *wikicatalog.Catalog) {
	if parent == nil || catalog == nil {
		return
	}
	ctx, cancel := context.WithTimeout(parent, wikiCatalogGCTimeout)
	defer cancel()

	stats, err := catalog.GCRun(ctx, time.Now().UTC(), wikiCatalogGCPendingTTL, wikiCatalogGCRefcountTTL)
	if err != nil {
		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
			slog.DebugContext(ctx, "wiki catalog gc stopped", "tenant_id", id, "error", err)
			return
		}
		slog.WarnContext(ctx, "wiki catalog gc failed", "tenant_id", id, "error", err)
		return
	}
	if stats.InlineRefsReclaimed == 0 && stats.PendingReclaimed == 0 && stats.BlobsReclaimed == 0 {
		slog.DebugContext(ctx, "wiki catalog gc completed",
			"tenant_id", id,
			"inline_refs_reclaimed", 0,
			"pending_reclaimed", 0,
			"blobs_reclaimed", 0,
		)
		return
	}
	slog.InfoContext(ctx, "wiki catalog gc reclaimed blobs",
		"tenant_id", id,
		"inline_refs_reclaimed", stats.InlineRefsReclaimed,
		"pending_reclaimed", stats.PendingReclaimed,
		"blobs_reclaimed", stats.BlobsReclaimed,
	)
}
