package repository

import (
	"errors"
	"strings"
	"time"

	"hyapp-admin-server/internal/model"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

func (s *Store) ListRoleDataScopes(roleID uint) ([]model.DataScope, error) {
	var scopes []model.DataScope
	err := s.db.Where("role_id = ?", roleID).Order("scope_type ASC, scope_value ASC").Find(&scopes).Error
	return scopes, err
}

func (s *Store) DataAccessForUser(userID uint) (DataAccess, error) {
	if userID == 0 {
		return DataAccess{All: true}, nil
	}

	var user model.User
	if err := s.db.Preload("Roles").First(&user, userID).Error; err != nil {
		return DataAccess{}, err
	}
	roleIDs := make([]uint, 0, len(user.Roles))
	for _, role := range user.Roles {
		roleIDs = append(roleIDs, role.ID)
		if role.Code == "platform-admin" {
			return DataAccess{UserID: userID, All: true}, nil
		}
	}
	if len(roleIDs) == 0 {
		return DataAccess{UserID: userID, All: true}, nil
	}

	var scopes []model.DataScope
	if err := s.db.Where("role_id IN ?", roleIDs).Find(&scopes).Error; err != nil {
		return DataAccess{}, err
	}
	if len(scopes) == 0 {
		return DataAccess{UserID: userID, All: true}, nil
	}

	access := DataAccess{UserID: userID, Restricted: true}
	teamSet := map[string]struct{}{}
	for _, scope := range scopes {
		scopeType := strings.TrimSpace(scope.ScopeType)
		scopeValue := strings.TrimSpace(scope.ScopeValue)
		switch scopeType {
		case "all":
			return DataAccess{UserID: userID, All: true}, nil
		case "self":
			access.SelfOnly = true
		case "team":
			if scopeValue == "self" && strings.TrimSpace(user.Team) != "" {
				teamSet[user.Team] = struct{}{}
				continue
			}
			if scopeValue != "" {
				teamSet[scopeValue] = struct{}{}
			}
		}
	}
	for team := range teamSet {
		access.Teams = append(access.Teams, team)
	}
	return access, nil
}

func (s *Store) ReplaceRoleDataScopes(roleID uint, scopes []model.DataScope) error {
	if roleID == 0 {
		return errors.New("role id is required")
	}
	for i := range scopes {
		scopes[i].RoleID = roleID
		scopes[i].ScopeType = strings.TrimSpace(scopes[i].ScopeType)
		scopes[i].ScopeValue = strings.TrimSpace(scopes[i].ScopeValue)
		if scopes[i].ScopeType == "" || scopes[i].ScopeValue == "" {
			return errors.New("scope type and value are required")
		}
	}

	return s.db.Transaction(func(tx *gorm.DB) error {
		var role model.Role
		if err := tx.First(&role, roleID).Error; err != nil {
			return err
		}
		if err := tx.Where("role_id = ?", roleID).Delete(&model.DataScope{}).Error; err != nil {
			return err
		}
		if len(scopes) == 0 {
			return nil
		}
		return tx.Create(&scopes).Error
	})
}

func (s *Store) CreateJob(job *model.AdminJob) error {
	if strings.TrimSpace(job.Type) == "" {
		return errors.New("job type is required")
	}
	job.Type = strings.TrimSpace(job.Type)
	job.Status = model.JobStatusPending
	if job.MaxAttempts <= 0 {
		job.MaxAttempts = 3
	}
	return s.db.Create(job).Error
}

func (s *Store) ListJobs(options ListOptions) ([]model.AdminJob, int64, error) {
	page, pageSize := normalizePage(options.Page, options.PageSize)
	query := s.db.Model(&model.AdminJob{})
	if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
		like := "%" + keyword + "%"
		query = query.Where("type LIKE ? OR created_by_name LIKE ? OR error LIKE ?", like, like, like)
	}
	if status := strings.TrimSpace(options.Status); status != "" && status != "all" {
		query = query.Where("status = ?", status)
	}

	var total int64
	if err := query.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	var jobs []model.AdminJob
	err := query.Order("created_at_ms DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&jobs).Error
	return jobs, total, err
}

func (s *Store) ListActiveJobsByType(jobType string) ([]model.AdminJob, error) {
	var jobs []model.AdminJob
	err := s.db.Where("type = ? AND status IN ?", strings.TrimSpace(jobType), []string{model.JobStatusPending, model.JobStatusRunning}).
		Order("created_at_ms ASC, id ASC").
		Find(&jobs).Error
	return jobs, err
}

func (s *Store) LeaseNextJob(workerID string, lease time.Duration, maxAttempts int) (*model.AdminJob, error) {
	if strings.TrimSpace(workerID) == "" {
		return nil, errors.New("worker id is required")
	}
	if lease <= 0 {
		return nil, errors.New("lease must be greater than 0")
	}
	if maxAttempts <= 0 {
		maxAttempts = 3
	}

	nowMS := time.Now().UTC().UnixMilli()
	var leased model.AdminJob
	err := s.db.Transaction(func(tx *gorm.DB) error {
		var job model.AdminJob
		err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("(status = ? OR (status = ? AND locked_until_ms IS NOT NULL AND locked_until_ms < ?)) AND attempt < max_attempts", model.JobStatusPending, model.JobStatusRunning, nowMS).
			Order("created_at_ms ASC, id ASC").
			First(&job).Error
		if err != nil {
			return err
		}
		if job.MaxAttempts <= 0 {
			job.MaxAttempts = maxAttempts
		}
		updates := map[string]any{
			"status":          model.JobStatusRunning,
			"attempt":         job.Attempt + 1,
			"max_attempts":    job.MaxAttempts,
			"locked_by":       workerID,
			"locked_until_ms": nowMS + lease.Milliseconds(),
			"started_at_ms":   firstMS(job.StartedAtMS, nowMS),
			"error":           "",
		}
		if err := tx.Model(&model.AdminJob{}).Where("id = ?", job.ID).Updates(updates).Error; err != nil {
			return err
		}
		if err := tx.First(&leased, job.ID).Error; err != nil {
			return err
		}
		return nil
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, gorm.ErrRecordNotFound
	}
	if err != nil {
		return nil, err
	}
	return &leased, nil
}

func (s *Store) CompleteJobWithArtifact(id uint, workerID string, resultJSON string, artifactPath string) error {
	nowMS := time.Now().UTC().UnixMilli()
	result := s.db.Model(&model.AdminJob{}).
		Where("id = ? AND status = ? AND locked_by = ?", id, model.JobStatusRunning, strings.TrimSpace(workerID)).
		Updates(map[string]any{
			"status":          model.JobStatusSucceeded,
			"result_json":     resultJSON,
			"artifact_path":   artifactPath,
			"locked_by":       "",
			"locked_until_ms": nil,
			"finished_at_ms":  &nowMS,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("job lease is no longer owned by worker")
	}
	return nil
}

// RenewJobLease 延长仍属于 worker 的 running 任务；RowsAffected=0 说明任务已完成或租约已经被其他实例接管。
func (s *Store) RenewJobLease(id uint, workerID string, lease time.Duration) error {
	if strings.TrimSpace(workerID) == "" || lease <= 0 {
		return errors.New("worker id and positive lease are required")
	}
	nowMS := time.Now().UTC().UnixMilli()
	result := s.db.Model(&model.AdminJob{}).
		Where("id = ? AND status = ? AND locked_by = ?", id, model.JobStatusRunning, strings.TrimSpace(workerID)).
		Update("locked_until_ms", nowMS+lease.Milliseconds())
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		var job model.AdminJob
		if err := s.db.Select("status", "locked_by").First(&job, id).Error; err != nil {
			return err
		}
		// handler 完成提交与 heartbeat tick 可能恰好相邻；终态无需续租，不能把成功任务误报为失败。
		if job.Status != model.JobStatusRunning {
			return nil
		}
		return errors.New("job lease is no longer owned by worker")
	}
	return nil
}

type CountryCodeAdminReferenceCounts struct {
	Banners       int64 `json:"banners"`
	SplashScreens int64 `json:"splashScreens"`
}

func (s *Store) RenameCountryCodeAdminReferences(appCode string, oldCountryCode string, newCountryCode string, nowMS int64) (CountryCodeAdminReferenceCounts, error) {
	var counts CountryCodeAdminReferenceCounts
	err := s.db.Transaction(func(tx *gorm.DB) error {
		result := tx.Model(&model.AppBanner{}).
			Where("app_code = ? AND country_code = ?", strings.TrimSpace(appCode), strings.TrimSpace(oldCountryCode)).
			Updates(map[string]any{"country_code": strings.TrimSpace(newCountryCode), "updated_at_ms": nowMS})
		if result.Error != nil {
			return result.Error
		}
		counts.Banners = result.RowsAffected

		result = tx.Model(&model.AppSplashScreen{}).
			Where("app_code = ? AND country_code = ?", strings.TrimSpace(appCode), strings.TrimSpace(oldCountryCode)).
			Updates(map[string]any{"country_code": strings.TrimSpace(newCountryCode), "updated_at_ms": nowMS})
		if result.Error != nil {
			return result.Error
		}
		counts.SplashScreens = result.RowsAffected
		return nil
	})
	if err != nil {
		return CountryCodeAdminReferenceCounts{}, err
	}
	return counts, nil
}

func (s *Store) FailJobAttempt(id uint, workerID string, message string) error {
	var job model.AdminJob
	if err := s.db.Where("id = ? AND status = ? AND locked_by = ?", id, model.JobStatusRunning, strings.TrimSpace(workerID)).First(&job).Error; err != nil {
		return err
	}
	nowMS := time.Now().UTC().UnixMilli()
	status := model.JobStatusPending
	finishedAt := any(nil)
	if job.Attempt >= job.MaxAttempts {
		status = model.JobStatusFailed
		finishedAt = &nowMS
	}
	result := s.db.Model(&model.AdminJob{}).
		Where("id = ? AND status = ? AND locked_by = ?", id, model.JobStatusRunning, strings.TrimSpace(workerID)).
		Updates(map[string]any{
			"status":          status,
			"error":           message,
			"locked_by":       "",
			"locked_until_ms": nil,
			"finished_at_ms":  finishedAt,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("job lease is no longer owned by worker")
	}
	return nil
}

func (s *Store) ReleaseJobLease(id uint, workerID string) error {
	result := s.db.Model(&model.AdminJob{}).
		Where("id = ? AND status = ? AND locked_by = ?", id, model.JobStatusRunning, strings.TrimSpace(workerID)).
		Updates(map[string]any{
			"status":          model.JobStatusPending,
			"locked_by":       "",
			"locked_until_ms": nil,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("job lease is no longer owned by worker")
	}
	return nil
}

func (s *Store) CancelJob(id uint) error {
	nowMS := time.Now().UTC().UnixMilli()
	result := s.db.Model(&model.AdminJob{}).
		Where("id = ? AND status IN ?", id, []string{model.JobStatusPending, model.JobStatusRunning}).
		Updates(map[string]any{
			"status":          model.JobStatusCanceled,
			"locked_by":       "",
			"locked_until_ms": nil,
			"finished_at_ms":  &nowMS,
		})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("job cannot be canceled")
	}
	return nil
}

func (s *Store) FindJobByID(id uint) (*model.AdminJob, error) {
	var job model.AdminJob
	if err := s.db.First(&job, id).Error; err != nil {
		return nil, err
	}
	return &job, nil
}

func firstMS(value *int64, fallback int64) *int64 {
	if value != nil {
		return value
	}
	return &fallback
}
