package service

import (
	"context"
	"database/sql"
	"log/slog"
	"time"

	"gorm.io/gorm/clause"

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

const businessActionGitPush = "git_push"

func businessMetricsDay(t time.Time) string {
	if t.IsZero() {
		t = time.Now()
	}
	return t.UTC().Format("2006-01-02")
}

func businessUserKind(u db.User) string {
	if u.UserKind != "" {
		return u.UserKind
	}
	return db.UserKindHuman
}

func (s *Service) recordUserCreatedMetric(u db.User) {
	metrics.IncUserCreated(businessUserKind(u))
}

func (s *Service) recordOrgCreatedMetric(ctx context.Context) {
	metrics.IncOrgCreated()
	if user, ok := UserFromContext(ctx); ok {
		s.recordActiveBusinessUse(ctx, user.ID, 0, time.Now())
	}
}

func (s *Service) recordRepoCreatedMetric(ctx context.Context, repoID uint) {
	metrics.IncRepoCreated()
	userID := uint(0)
	if user, ok := UserFromContext(ctx); ok {
		userID = user.ID
	}
	s.recordActiveBusinessUse(ctx, userID, repoID, time.Now())
}

// RecordGitRead records a successful Git upload-pack request.
func (s *Service) RecordGitRead(ctx context.Context, repoID uint) {
	metrics.IncGitRead()
	userID := uint(0)
	if user, ok := UserFromContext(ctx); ok {
		userID = user.ID
	}
	s.recordActiveBusinessUse(ctx, userID, repoID, time.Now())
}

func (s *Service) recordGitPushMetric(ctx context.Context, repoID uint, pushedAt time.Time) {
	if pushedAt.IsZero() {
		pushedAt = time.Now()
	}
	metrics.IncGitPush()

	var repo db.Repository
	if err := s.DBForCtx(ctx).Select("id", "created_at").First(&repo, "id = ?", repoID).Error; err != nil {
		slog.WarnContext(ctx, "record git push metrics: load repo failed", "repo_id", repoID, "error", err)
	}

	userID := uint(0)
	if user, ok := UserFromContext(ctx); ok && user.ID != 0 {
		userID = user.ID
		s.recordFirstUserAction(ctx, user, businessActionGitPush, pushedAt)
	}
	if repo.ID != 0 {
		s.recordFirstRepoAction(ctx, repo, businessActionGitPush, pushedAt)
	}
	s.recordActiveBusinessUse(ctx, userID, repoID, pushedAt)
}

// RecordSearch records a successful search request.
func (s *Service) RecordSearch(ctx context.Context) {
	if state, ok := requestBusinessMetricsStateFromContext(ctx); ok {
		state.mu.Lock()
		if state.searchRecorded {
			state.mu.Unlock()
			return
		}
		state.searchRecorded = true
		state.mu.Unlock()
	}
	metrics.IncSearch()
	if user, ok := UserFromContext(ctx); ok {
		s.recordActiveBusinessUse(ctx, user.ID, 0, time.Now())
	}
}

func (s *Service) recordStarCreatedMetric(ctx context.Context, userID, repoID uint) {
	metrics.IncStarCreated()
	s.recordActiveBusinessUse(ctx, userID, repoID, time.Now())
}

func (s *Service) recordReleasePublishedMetric(ctx context.Context, repoID uint) {
	metrics.IncReleasePublished()
	userID := uint(0)
	if user, ok := UserFromContext(ctx); ok {
		userID = user.ID
	}
	s.recordActiveBusinessUse(ctx, userID, repoID, time.Now())
}

func (s *Service) recordIssueOpenedMetric(ctx context.Context, issue db.Issue) {
	metrics.IncIssueOpened()
	s.recordActiveBusinessUse(ctx, issue.AuthorID, issue.RepositoryID, issue.CreatedAt)
}

func (s *Service) recordIssueClosedMetric(ctx context.Context, issue db.Issue, closedAt time.Time) {
	if closedAt.IsZero() {
		closedAt = time.Now()
	}
	metrics.IncIssueClosed()
	if !issue.CreatedAt.IsZero() {
		metrics.ObserveIssueCloseDuration(closedAt.Sub(issue.CreatedAt))
	}
	userID := issue.AuthorID
	if user, ok := UserFromContext(ctx); ok && user.ID != 0 {
		userID = user.ID
	}
	s.recordActiveBusinessUse(ctx, userID, issue.RepositoryID, closedAt)
}

func (s *Service) recordIssueReopenedMetric(ctx context.Context, issue db.Issue) {
	metrics.IncIssueReopened()
	userID := issue.AuthorID
	if user, ok := UserFromContext(ctx); ok && user.ID != 0 {
		userID = user.ID
	}
	s.recordActiveBusinessUse(ctx, userID, issue.RepositoryID, time.Now())
}

func (s *Service) recordIssueCommentCreatedMetric(ctx context.Context, comment db.IssueComment) {
	metrics.IncIssueCommentCreated()
	s.recordActiveBusinessUse(ctx, comment.AuthorID, comment.RepositoryID, comment.CreatedAt)
}

func (s *Service) recordActiveBusinessUse(ctx context.Context, userID, repoID uint, at time.Time) {
	if at.IsZero() {
		at = time.Now()
	}
	day := businessMetricsDay(at)
	if userID != 0 {
		s.recordDailyActiveUser(ctx, day, userID)
	}
	if repoID != 0 {
		s.recordDailyActiveRepo(ctx, day, repoID)
	}
}

func (s *Service) recordDailyActiveUser(ctx context.Context, day string, userID uint) {
	row := db.BusinessUserActivityDaily{Day: day, UserID: userID}
	if err := s.DBForCtx(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
		slog.WarnContext(ctx, "record daily active user failed", "day", day, "user_id", userID, "error", err)
		return
	}
	s.refreshDailyActiveUserGauge(ctx, day)
}

func (s *Service) recordDailyActiveRepo(ctx context.Context, day string, repoID uint) {
	row := db.BusinessRepoActivityDaily{Day: day, RepositoryID: repoID}
	if err := s.DBForCtx(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
		slog.WarnContext(ctx, "record daily active repo failed", "day", day, "repo_id", repoID, "error", err)
		return
	}
	s.refreshDailyActiveRepoGauge(ctx, day)
}

func (s *Service) refreshDailyActiveUserGauge(ctx context.Context, day string) {
	if !s.usesDefaultMetricsDB(ctx) {
		return
	}
	var count int64
	if err := s.DBForCtx(ctx).Model(&db.BusinessUserActivityDaily{}).Where("day = ?", day).Count(&count).Error; err != nil {
		slog.WarnContext(ctx, "refresh daily active users gauge failed", "day", day, "error", err)
		return
	}
	metrics.SetActiveUsersDaily(float64(count))
}

func (s *Service) refreshDailyActiveRepoGauge(ctx context.Context, day string) {
	if !s.usesDefaultMetricsDB(ctx) {
		return
	}
	var count int64
	if err := s.DBForCtx(ctx).Model(&db.BusinessRepoActivityDaily{}).Where("day = ?", day).Count(&count).Error; err != nil {
		slog.WarnContext(ctx, "refresh daily active repos gauge failed", "day", day, "error", err)
		return
	}
	metrics.SetActiveReposDaily(float64(count))
}

func (s *Service) usesDefaultMetricsDB(ctx context.Context) bool {
	scopedDB, ok := DBFromContext(ctx)
	if !ok || scopedDB == nil {
		return true
	}
	defaultSQL, err := s.sqlDBHandle(s.DB)
	if err != nil {
		return false
	}
	scopedSQL, err := s.sqlDBHandle(scopedDB)
	if err != nil {
		return false
	}
	return defaultSQL == scopedSQL
}

func (s *Service) sqlDBHandle(dbh interface{ DB() (*sql.DB, error) }) (*sql.DB, error) {
	return dbh.DB()
}

func (s *Service) recordFirstUserAction(ctx context.Context, user db.User, action string, occurredAt time.Time) {
	if user.ID == 0 || action == "" {
		return
	}
	if occurredAt.IsZero() {
		occurredAt = time.Now()
	}
	if user.CreatedAt.IsZero() {
		if err := s.DBForCtx(ctx).Select("id", "created_at", "user_kind").First(&user, "id = ?", user.ID).Error; err != nil {
			slog.WarnContext(ctx, "record first user action: load user failed", "user_id", user.ID, "action", action, "error", err)
		}
	}
	row := db.BusinessFirstUserAction{UserID: user.ID, Action: action, OccurredAt: occurredAt.UTC()}
	result := s.DBForCtx(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&row)
	if result.Error != nil {
		slog.WarnContext(ctx, "record first user action failed", "user_id", user.ID, "action", action, "error", result.Error)
		return
	}
	if result.RowsAffected == 0 {
		return
	}
	if action == businessActionGitPush {
		metrics.IncFirstGitPushUser()
		if !user.CreatedAt.IsZero() {
			metrics.ObserveUserActivationDuration(occurredAt.Sub(user.CreatedAt))
		}
	}
}

func (s *Service) recordFirstRepoAction(ctx context.Context, repo db.Repository, action string, occurredAt time.Time) {
	if repo.ID == 0 || action == "" {
		return
	}
	if occurredAt.IsZero() {
		occurredAt = time.Now()
	}
	row := db.BusinessFirstRepoAction{RepositoryID: repo.ID, Action: action, OccurredAt: occurredAt.UTC()}
	result := s.DBForCtx(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&row)
	if result.Error != nil {
		slog.WarnContext(ctx, "record first repo action failed", "repo_id", repo.ID, "action", action, "error", result.Error)
		return
	}
	if result.RowsAffected == 0 {
		return
	}
	if action == businessActionGitPush && !repo.CreatedAt.IsZero() {
		metrics.ObserveRepoActivationDuration(occurredAt.Sub(repo.CreatedAt))
	}
}
