// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
	"context"
	"fmt"
	"slices"
	"time"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
)

// ActionRunAttempt represents a single execution attempt of an ActionRun.
type ActionRunAttempt struct {
	ID      int64
	RepoID  int64      `xorm:"index(repo_concurrency_status)"`
	RunID   int64      `xorm:"UNIQUE(run_attempt)"`
	Run     *ActionRun `xorm:"-"`
	Attempt int64      `xorm:"UNIQUE(run_attempt)"`

	TriggerUserID int64
	TriggerUser   *user_model.User `xorm:"-"`

	ConcurrencyGroup  string `xorm:"index(repo_concurrency_status) NOT NULL DEFAULT ''"`
	ConcurrencyCancel bool   `xorm:"NOT NULL DEFAULT FALSE"`

	Status  Status `xorm:"index(repo_concurrency_status)"`
	Started timeutil.TimeStamp
	Stopped timeutil.TimeStamp

	Created timeutil.TimeStamp `xorm:"created"`
	Updated timeutil.TimeStamp `xorm:"updated"`
}

func (*ActionRunAttempt) TableName() string {
	return "action_run_attempt"
}

func init() {
	db.RegisterModel(new(ActionRunAttempt))
}

func (attempt *ActionRunAttempt) Duration() time.Duration {
	return calculateDuration(attempt.Started, attempt.Stopped, attempt.Status, attempt.Updated)
}

func (attempt *ActionRunAttempt) LoadAttributes(ctx context.Context) (err error) {
	if attempt.Run == nil {
		run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
		if err != nil {
			return err
		}
		if err := run.LoadAttributes(ctx); err != nil {
			return err
		}
		attempt.Run = run
	}

	if attempt.TriggerUser == nil {
		attempt.TriggerUserID, attempt.TriggerUser, err = user_model.GetPossibleUserByID(ctx, attempt.TriggerUserID)
		if err != nil {
			return err
		}
	}

	return nil
}

func GetRunAttemptByRepoAndID(ctx context.Context, repoID, attemptID int64) (*ActionRunAttempt, error) {
	var attempt ActionRunAttempt
	has, err := db.GetEngine(ctx).Where("repo_id=? AND id=?", repoID, attemptID).Get(&attempt)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, fmt.Errorf("run attempt %d in repo %d: %w", attemptID, repoID, util.ErrNotExist)
	}
	return &attempt, nil
}

func GetRunAttemptByRunIDAndAttemptNum(ctx context.Context, runID, attemptNum int64) (*ActionRunAttempt, error) {
	var attempt ActionRunAttempt
	has, err := db.GetEngine(ctx).Where("run_id=? AND attempt=?", runID, attemptNum).Get(&attempt)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, fmt.Errorf("run attempt %d for run %d: %w", attemptNum, runID, util.ErrNotExist)
	}
	return &attempt, nil
}

// FindConcurrentRunAttempts returns attempts in the given concurrency group and status set.
// Results are unordered; callers must not depend on any particular row order.
func FindConcurrentRunAttempts(ctx context.Context, repoID int64, concurrencyGroup string, statuses []Status) ([]*ActionRunAttempt, error) {
	attempts := make([]*ActionRunAttempt, 0)
	sess := db.GetEngine(ctx).Where("repo_id=? AND concurrency_group=?", repoID, concurrencyGroup)
	if len(statuses) > 0 {
		sess = sess.In("status", statuses)
	}
	return attempts, sess.Find(&attempts)
}

func UpdateRunAttempt(ctx context.Context, attempt *ActionRunAttempt, cols ...string) error {
	if slices.Contains(cols, "status") && attempt.Started.IsZero() && attempt.Status.IsRunning() {
		attempt.Started = timeutil.TimeStampNow()
		cols = append(cols, "started")
	}

	sess := db.GetEngine(ctx).ID(attempt.ID)
	if len(cols) > 0 {
		sess.Cols(cols...)
	}
	if _, err := sess.Update(attempt); err != nil {
		return err
	}

	// Only status/timing changes on an attempt need to update the latest run.
	if len(cols) > 0 && !slices.Contains(cols, "status") && !slices.Contains(cols, "started") && !slices.Contains(cols, "stopped") {
		return nil
	}

	run, err := GetRunByRepoAndID(ctx, attempt.RepoID, attempt.RunID)
	if err != nil {
		return err
	}
	if run.LatestAttemptID != attempt.ID {
		log.Warn("run %d cannot be updated by an old attempt %d", run.LatestAttemptID, attempt.ID)
		return nil
	}

	run.Status = attempt.Status
	run.Started = attempt.Started
	run.Stopped = attempt.Stopped
	return UpdateRun(ctx, run, "status", "started", "stopped")
}
