package actions

import (
	"bytes"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"code.gitea.io/gitea/modules/timeutil"

	stdCtx "context"

	actions_model "code.gitea.io/gitea/models/actions"
	"code.gitea.io/gitea/models/db"
	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/models/unit"
	"code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/actions"
	actions_module "code.gitea.io/gitea/modules/actions"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/container"
	"code.gitea.io/gitea/modules/context"
	"code.gitea.io/gitea/modules/git"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/routers/api/v1/utils"
	"code.gitea.io/gitea/routers/web/repo"
	actions_service "code.gitea.io/gitea/services/actions"
	"code.gitea.io/gitea/services/convert"
	hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions"
	hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
	hat_actions_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/actions"
	jobparser "github.com/nektos/act/pkg/jobparser"
	"github.com/nektos/act/pkg/model"
	"xorm.io/builder"
)

func DeleteActionsSecret(ctx *context.APIContext) {
	if err := hat_actions_service.DeleteSecretByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("secretname")); err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "DeleteSecretByName", err)
		} else if errors.Is(err, util.ErrNotExist) {
			ctx.NotFound(err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	ctx.Status(http.StatusNoContent)
}

func CreateActionsSecret(ctx *context.APIContext) {
	opt := web.GetForm(ctx).(*hat_api.CreateSecretOption)
	repoID := ctx.Repo.Repository.ID
	secretName := ctx.Params("secretname")

	v, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{
		RepoID: repoID,
		Name:   secretName,
	})
	if err != nil && !errors.Is(err, util.ErrNotExist) {
		ctx.InternalServerError(err)
		return
	}
	if v != nil && v.ID > 0 {
		ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("secret name %s already exists", secretName))
		return
	}
	if _, err := hat_actions_service.CreateSecret(ctx, 0, repoID, secretName, opt.Data); err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "CreateSecret", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}
	ctx.Status(http.StatusCreated)
}

func UpdateActionsSecret(ctx *context.APIContext) {
	opt := web.GetForm(ctx).(*hat_api.UpdateSecretOption)

	_, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{
		RepoID: ctx.Repo.Repository.ID,
		Name:   ctx.Params("secretname"),
	})
	if err != nil {
		if errors.Is(err, util.ErrNotExist) {
			ctx.Error(http.StatusNotFound, "GetSecret", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	if opt.Name == "" {
		opt.Name = ctx.Params("secretname")
	}

	_, err = hat_actions_service.UpdateSecret(ctx, 0, ctx.Repo.Repository.ID, opt.Name, opt.Data)
	if err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "UpdateSecret", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	ctx.Status(http.StatusNoContent)

}

func ListActionsSecrets(ctx *context.APIContext) {
	opts := &hat_actions_model.FindSecretsOptions{
		RepoID:      ctx.Repo.Repository.ID,
		ListOptions: utils.GetListOptions(ctx),
	}

	count, err := hat_actions_model.CountSecrets(ctx, opts)
	if err != nil {
		ctx.InternalServerError(err)
		return
	}

	secrets, err := hat_actions_model.FindSecrets(ctx, *opts)
	if err != nil {
		ctx.InternalServerError(err)
		return
	}

	apiSecrets := make([]*api.Secret, len(secrets))
	for k, v := range secrets {
		apiSecrets[k] = &api.Secret{
			Name:    v.Name,
			Created: v.CreatedUnix.AsTime(),
		}
	}

	ctx.SetTotalCountHeader(count)
	ctx.JSON(http.StatusOK, apiSecrets)
}

func GetVariable(ctx *context.APIContext) {
	v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{
		RepoID: ctx.Repo.Repository.ID,
		Name:   ctx.Params("variablename"),
	})
	if err != nil {
		if errors.Is(err, util.ErrNotExist) {
			ctx.NotFound(err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	variable := &hat_api.ActionVariable{
		OwnerID: v.OwnerID,
		RepoID:  v.RepoID,
		Name:    v.Name,
		Data:    v.Data,
	}

	ctx.JSON(http.StatusOK, variable)
}

func DeleteVariable(ctx *context.APIContext) {
	if err := hat_actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
		} else if errors.Is(err, util.ErrNotExist) {
			ctx.NotFound(err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	ctx.Status(http.StatusNoContent)
}

func CreateVariable(ctx *context.APIContext) {
	opt := web.GetForm(ctx).(*hat_api.CreateVariableOption)
	repoID := ctx.Repo.Repository.ID
	variableName := ctx.Params("variablename")

	v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{
		RepoID: repoID,
		Name:   variableName,
	})
	if err != nil && !errors.Is(err, util.ErrNotExist) {
		ctx.InternalServerError(err)
		return
	}
	if v != nil && v.ID > 0 {
		ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
		return
	}

	if _, err := hat_actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "CreateVariable", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	ctx.Status(http.StatusCreated)
}

func UpdateVariable(ctx *context.APIContext) {
	opt := web.GetForm(ctx).(*hat_api.UpdateVariableOption)

	v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{
		RepoID: ctx.Repo.Repository.ID,
		Name:   ctx.Params("variablename"),
	})
	if err != nil {
		if errors.Is(err, util.ErrNotExist) {
			ctx.Error(http.StatusNotFound, "GetVariable", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	if opt.Name == "" {
		opt.Name = ctx.Params("variablename")
	}

	v.Name = opt.Name
	v.Data = opt.Value

	if _, err := hat_actions_service.UpdateVariableNameData(ctx, v); err != nil {
		if errors.Is(err, util.ErrInvalidArgument) {
			ctx.Error(http.StatusBadRequest, "UpdateVariableNameData", err)
		} else {
			ctx.InternalServerError(err)
		}
		return
	}

	ctx.Status(http.StatusNoContent)
}

func ListVariables(ctx *context.APIContext) {
	opts := hat_actions_model.FindVariablesOpts{
		RepoID:      ctx.Repo.Repository.ID,
		ListOptions: utils.GetListOptions(ctx),
	}
	vars, err := hat_actions_model.FindVariables(ctx, opts)

	if err != nil {
		ctx.InternalServerError(err)
		return
	}

	count, err := hat_actions_model.CountVariables(ctx, opts)

	if err != nil {
		ctx.InternalServerError(err)
		return
	}

	variables := make([]*hat_api.ActionVariable, len(vars))
	for i, v := range vars {
		variables[i] = &hat_api.ActionVariable{
			OwnerID: v.OwnerID,
			RepoID:  v.RepoID,
			Name:    v.Name,
			Data:    v.Data,
		}
	}

	ctx.SetTotalCountHeader(count)
	ctx.JSON(http.StatusOK, variables)
}

func Run(ctx *context.APIContext) {
	workflow := ctx.FormString("workflow")
	ref := ctx.FormString("ref")

	if ref == "" {
		ref = ctx.Repo.Repository.DefaultBranch
	}

	commit, err := ctx.Repo.GitRepo.GetCommit(ref)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetCommit", err.Error())
		return
	}

	entries, err := ListWorkflowsByWorkflow(commit, workflow)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "ListWorkflowsByWorkflow", err.Error())
		return
	}

	workflows := make([]*actions_module.DetectedWorkflow, 0, len(entries))
	for _, entry := range entries {
		content, err := actions_module.GetContentFromEntry(entry)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetContentFromEntry", err.Error())
			return
		}
		dwf := &actions_module.DetectedWorkflow{
			EntryName: entry.Name(),
			Content:   content,
		}
		workflows = append(workflows, dwf)
	}

	for _, dwf := range workflows {
		run := &actions_model.ActionRun{
			Title:         strings.SplitN(commit.CommitMessage, "\n", 2)[0],
			RepoID:        ctx.Repo.Repository.ID,
			OwnerID:       ctx.Repo.Repository.OwnerID,
			WorkflowID:    dwf.EntryName,
			TriggerUserID: ctx.Doer.ID,
			Ref:           ref,
			CommitSHA:     commit.ID.String(),
			Status:        actions_model.StatusWaiting,
		}

		jobs, err := jobparser.Parse(dwf.Content)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "jobparser.Parse", err.Error())
			return
		}

		if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
			ctx.Error(http.StatusInternalServerError, "InsertRun", err.Error())
			return
		}

		alljobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "FindRunJobs", err.Error())
			return
		}
		actions_service.CreateCommitStatus(ctx, alljobs...)
	}

	ctx.Status(http.StatusNoContent)
}

func ListWorkflowsByWorkflow(commit *git.Commit, workflow string) (git.Entries, error) {
	tree, err := commit.SubTree(".gitea/workflows")
	if _, ok := err.(git.ErrNotExist); ok {
		tree, err = commit.SubTree(".github/workflows")
	}
	if _, ok := err.(git.ErrNotExist); ok {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	entries, err := tree.ListEntriesRecursiveFast()
	if err != nil {
		return nil, err
	}

	ret := make(git.Entries, 0, len(entries))
	for _, entry := range entries {
		if workflow == entry.Name() && (strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml")) {
			ret = append(ret, entry)
		}
	}
	return ret, nil
}

type Workflow struct {
	Name   string
	ErrMsg string
}

type DetailStatus struct {
	RunID  int64
	Status []actions_model.Status
}

type ResponseAction struct {
	Workflows                    []Workflow
	CurWorkflow                  string
	ActionsConfig                *repo_model.ActionsConfig
	AllowDisableOrEnableWorkflow bool
	CurWorkflowDisabled          bool
	CurActor                     int64
	CurStatus                    int
	IsFiltered                   bool
	Runs                         actions_model.RunList
	Actors                       []*user.User
	StatusInfoList               []actions_model.StatusInfo
	DetailStatusList             []DetailStatus
}

func ListActions(ctx *context.APIContext) {

	workflow := ctx.FormString("workflow")
	actorID := ctx.FormInt64("actor")
	status := ctx.FormInt("status")

	var workflows []Workflow
	if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
		ctx.Error(http.StatusInternalServerError, "IsEmpty", err.Error())
		return
	} else if !empty {
		commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err.Error())
			return
		}
		entries, err := actions.ListWorkflows(commit)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "ListWorkflows", err.Error())
			return
		}

		opts := actions_model.FindRunnerOptions{
			RepoID:        ctx.Repo.Repository.ID,
			WithAvailable: true,
		}
		runners, err := actions_model.FindRunners(ctx, opts)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "FindRunners", err.Error())
		}

		allRunnerLabels := make(container.Set[string])
		for _, r := range runners {
			allRunnerLabels.AddMultiple(r.AgentLabels...)
		}

		workflows = make([]Workflow, 0, len(entries))
		for _, entry := range entries {
			workflow := Workflow{Name: entry.Name()}
			content, err := actions.GetContentFromEntry(entry)
			if err != nil {
				ctx.Error(http.StatusInternalServerError, "GetContentFromEntry", err.Error())
				return
			}
			wf, err := model.ReadWorkflow(bytes.NewReader(content))
			if err != nil {
				workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
				workflows = append(workflows, workflow)
				continue
			}
			// Check whether have matching runner
			for _, j := range wf.Jobs {
				runsOnList := j.RunsOn()
				for _, ro := range runsOnList {
					if strings.Contains(ro, "${{") {
						// Skip if it contains expressions.
						// The expressions could be very complex and could not be evaluated here,
						// so just skip it, it's OK since it's just a tooltip message.
						continue
					}
					if !allRunnerLabels.Contains(ro) {
						workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_runner_helper", ro)
						break
					}
				}
				if workflow.ErrMsg != "" {
					break
				}
			}
			workflows = append(workflows, workflow)
		}
	}
	responseAction := ResponseAction{}
	responseAction.Workflows = workflows
	page := ctx.FormInt("page")
	if page <= 0 {
		page = 1
	}

	responseAction.CurWorkflow = workflow

	actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
	responseAction.ActionsConfig = actionsConfig

	if len(workflow) > 0 && ctx.Repo.IsAdmin() {
		responseAction.AllowDisableOrEnableWorkflow = true
		responseAction.CurWorkflowDisabled = actionsConfig.IsWorkflowDisabled(workflow)
	}

	responseAction.CurActor = actorID
	responseAction.CurStatus = status
	if actorID > 0 && status > int(actions_model.StatusUnknown) {
		responseAction.IsFiltered = true
	}

	opts := actions_model.FindRunOptions{
		ListOptions: db.ListOptions{
			Page:     page,
			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
		},
		RepoID:        ctx.Repo.Repository.ID,
		WorkflowID:    workflow,
		TriggerUserID: actorID,
	}

	if actions_model.Status(status) != actions_model.StatusUnknown {
		opts.Status = []actions_model.Status{actions_model.Status(status)}
	}

	runs, total, err := actions_model.FindRuns(ctx, opts)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "FindRuns", err.Error())
		return
	}

	for _, run := range runs {
		var detailStatuses []actions_model.Status
		jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "actions_model.GetRunJobsByRunID", err.Error())
			return
		}
		for _, j := range jobs {
			detailStatuses = append(detailStatuses, j.Status)
		}
		run.Repo = ctx.Repo.Repository
		responseAction.DetailStatusList = append(responseAction.DetailStatusList, DetailStatus{
			RunID:  run.ID,
			Status: detailStatuses,
		})
	}

	if err := runs.LoadTriggerUser(ctx); err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadTriggerUser", err.Error())
		return
	}

	responseAction.Runs = runs

	actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetActors", err.Error())
		return
	}
	responseAction.Actors = repo.MakeSelfOnTop(ctx.Doer, actors)
	responseAction.StatusInfoList = actions_model.GetStatusInfoList(ctx)
	ctx.SetLinkHeader(int(total), ctx.FormInt("limit"))
	ctx.SetTotalCountHeader(total)
	ctx.JSON(http.StatusOK, responseAction)
}

type ViewRequest struct {
	LogCursors []struct {
		Step     int   `json:"step"`
		Cursor   int64 `json:"cursor"`
		Expanded bool  `json:"expanded"`
	} `json:"logCursors"`
}

type ViewResponse struct {
	State struct {
		Run struct {
			Link       string     `json:"link"`
			Title      string     `json:"title"`
			Status     string     `json:"status"`
			CanCancel  bool       `json:"canCancel"`
			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
			CanRerun   bool       `json:"canRerun"`
			Done       bool       `json:"done"`
			Jobs       []*ViewJob `json:"jobs"`
			Commit     ViewCommit `json:"commit"`
		} `json:"run"`
		CurrentJob struct {
			Title  string         `json:"title"`
			Detail string         `json:"detail"`
			Steps  []*ViewJobStep `json:"steps"`
		} `json:"currentJob"`
	} `json:"state"`
	Logs struct {
		StepsLog []*ViewStepLog `json:"stepsLog"`
	} `json:"logs"`
}

type ViewJob struct {
	ID       int64  `json:"id"`
	Name     string `json:"name"`
	Status   string `json:"status"`
	CanRerun bool   `json:"canRerun"`
	Duration string `json:"duration"`
}

type ViewCommit struct {
	LocaleCommit   string     `json:"localeCommit"`
	LocalePushedBy string     `json:"localePushedBy"`
	ShortSha       string     `json:"shortSHA"`
	Link           string     `json:"link"`
	Pusher         ViewUser   `json:"pusher"`
	Branch         ViewBranch `json:"branch"`
}

type ViewUser struct {
	DisplayName string `json:"displayName"`
	Link        string `json:"link"`
}

type ViewBranch struct {
	Name string `json:"name"`
	Link string `json:"link"`
}

type ViewJobStep struct {
	Summary  string `json:"summary"`
	Duration string `json:"duration"`
	Status   string `json:"status"`
}

type ViewStepLog struct {
	Step    int                `json:"step"`
	Cursor  int64              `json:"cursor"`
	Lines   []*ViewStepLogLine `json:"lines"`
	Started int64              `json:"started"`
}

type ViewStepLogLine struct {
	Index     int64   `json:"index"`
	Message   string  `json:"message"`
	Timestamp float64 `json:"timestamp"`
}

func ListJobs(ctx *context.APIContext) {
	req := web.GetForm(ctx).(*ViewRequest)

	runIndex := ctx.ParamsInt64("run")
	jobIndex := ctx.ParamsInt64("job")

	current, jobs := getRunJobs(ctx, runIndex, jobIndex)
	if ctx.Written() {
		return
	}
	run := current.Run
	if err := run.LoadAttributes(ctx); err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err.Error())
		return
	}

	resp := &ViewResponse{}
	resp.State.Run.Title = run.Title
	resp.State.Run.Link = run.Link()
	resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
	resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
	resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
	resp.State.Run.Done = run.Status.IsDone()
	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
	resp.State.Run.Status = run.Status.String()
	for _, v := range jobs {
		resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
			ID:       v.ID,
			Name:     v.Name,
			Status:   v.Status.String(),
			CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
			Duration: v.Duration().String(),
		})
	}

	pusher := ViewUser{
		DisplayName: run.TriggerUser.GetDisplayName(),
		Link:        run.TriggerUser.HomeLink(),
	}
	branch := ViewBranch{
		Name: run.PrettyRef(),
		Link: run.RefLink(),
	}
	resp.State.Run.Commit = ViewCommit{
		LocaleCommit:   ctx.Tr("actions.runs.commit"),
		LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
		ShortSha:       base.ShortSha(run.CommitSHA),
		Link:           fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
		Pusher:         pusher,
		Branch:         branch,
	}

	var task *actions_model.ActionTask
	if current.TaskID > 0 {
		var err error
		task, err = actions_model.GetTaskByID(ctx, current.TaskID)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetTaskByID", err.Error())
			return
		}
		task.Job = current
		if err := task.LoadAttributes(ctx); err != nil {
			ctx.Error(http.StatusInternalServerError, "LoadAttributes", err.Error())
			return
		}
	}

	resp.State.CurrentJob.Title = current.Name
	resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
	if run.NeedApproval {
		resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
	}
	resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
	resp.Logs.StepsLog = make([]*ViewStepLog, 0)          // marshal to '[]' instead fo 'null' in json
	if task != nil {
		steps := actions.FullSteps(task)

		for _, v := range steps {
			resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
				Summary:  v.Name,
				Duration: v.Duration().String(),
				Status:   v.Status.String(),
			})
		}

		for _, cursor := range req.LogCursors {
			if !cursor.Expanded {
				continue
			}

			step := steps[cursor.Step]

			logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json

			index := step.LogIndex + cursor.Cursor
			validCursor := cursor.Cursor >= 0 &&
				// !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
				// So return the same cursor and empty lines to let the frontend retry.
				cursor.Cursor < step.LogLength &&
				// !(index < task.LogIndexes[index]) when task data is older than step data.
				// It can be fixed by making sure write/read tasks and steps in the same transaction,
				// but it's easier to just treat it as fetching the next line before it's ready.
				index < int64(len(task.LogIndexes))

			if validCursor {
				length := step.LogLength - cursor.Cursor
				offset := task.LogIndexes[index]
				var err error
				logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
				if err != nil {
					ctx.Error(http.StatusInternalServerError, "ReadLogs", err.Error())
					return
				}

				for i, row := range logRows {
					logLines = append(logLines, &ViewStepLogLine{
						Index:     cursor.Cursor + int64(i) + 1, // start at 1
						Message:   row.Content,
						Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
					})
				}
			}

			resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
				Step:    cursor.Step,
				Cursor:  cursor.Cursor + int64(len(logLines)),
				Lines:   logLines,
				Started: int64(step.Started),
			})
		}
	}

	ctx.JSON(http.StatusOK, resp)
}

func Rerun(ctx *context.APIContext) {
	runIndex := ctx.ParamsInt64("run")
	jobIndex := ctx.ParamsInt64("job")

	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetRunByIndex", err.Error())
		return
	}

	// can not rerun job when workflow is disabled
	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
	cfg := cfgUnit.ActionsConfig()
	if cfg.IsWorkflowDisabled(run.WorkflowID) {
		ctx.Error(http.StatusInternalServerError, "IsWorkflowDisabled", ctx.Locale.Tr("actions.workflow.disabled"))
		return
	}

	job, jobs := getRunJobs(ctx, runIndex, jobIndex)
	if ctx.Written() {
		return
	}

	if jobIndex != 0 {
		jobs = []*actions_model.ActionRunJob{job}
	}

	// if err := hat_actions_model.InsertRun(ctx, run, jobs); err != nil {
	// 	ctx.Error(http.StatusInternalServerError, "InsertRun", err.Error())
	// 	return
	// }

	// for _, j := range jobs {
	if err := newRerunJob(ctx, run, jobs); err != nil {
		ctx.Error(http.StatusInternalServerError, "rerunJob", err.Error())
		return
	}
	// }

	ctx.Status(http.StatusNoContent)
}

func newRerunJob(ctx *context.APIContext, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) error {
	if err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
		err := hat_actions_model.InsertRun(ctx, run, jobs)
		return err
	}); err != nil {
		return err
	}

	actions_service.CreateCommitStatus(ctx, jobs...)
	return nil
}

func rerunJob(ctx *context.APIContext, job *actions_model.ActionRunJob) error {
	status := job.Status
	if !status.IsDone() {
		return nil
	}

	job.TaskID = 0
	job.Status = actions_model.StatusWaiting
	job.Started = 0
	job.Stopped = 0

	if err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
		_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
		return err
	}); err != nil {
		return err
	}

	actions_service.CreateCommitStatus(ctx, job)
	return nil
}

func Logs(ctx *context.APIContext) {
	runIndex := ctx.ParamsInt64("run")
	jobIndex := ctx.ParamsInt64("job")

	job, _ := getRunJobs(ctx, runIndex, jobIndex)
	if ctx.Written() {
		return
	}
	if job.TaskID == 0 {
		ctx.Error(http.StatusNotFound, "", "job is not started")
		return
	}

	err := job.LoadRun(ctx)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadRun", err.Error())
		return
	}

	task, err := actions_model.GetTaskByID(ctx, job.TaskID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetTaskByID", err.Error())
		return
	}
	if task.LogExpired {
		ctx.Error(http.StatusNotFound, "", "logs have been cleaned up")
		return
	}

	reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "OpenLogs", err.Error())
		return
	}
	defer reader.Close()

	workflowName := job.Run.WorkflowID
	if p := strings.Index(workflowName, "."); p > 0 {
		workflowName = workflowName[0:p]
	}
	ctx.ServeContent(reader, &context.ServeHeaderOptions{
		Filename:           fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
		ContentLength:      &task.LogSize,
		ContentType:        "text/plain",
		ContentTypeCharset: "utf-8",
		Disposition:        "attachment",
	})
}

func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
	run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
	if err != nil {
		if errors.Is(err, util.ErrNotExist) {
			ctx.Error(http.StatusNotFound, "GetRunByIndex", err.Error())
			return nil, nil
		}
		ctx.Error(http.StatusInternalServerError, "GetRunByIndex", err.Error())
		return nil, nil
	}
	run.Repo = ctx.Repo.Repository

	jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetRunJobsByRunID", err.Error())
		return nil, nil
	}
	if len(jobs) == 0 {
		ctx.Error(http.StatusNotFound, "", err.Error())
		return nil, nil
	}

	for _, v := range jobs {
		v.Run = run
	}

	if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
		return jobs[jobIndex], jobs
	}
	return jobs[0], jobs
}

func DisableWorkflowFile(ctx *context.APIContext) {
	disableOrEnableWorkflowFile(ctx, false)
}

func EnableWorkflowFile(ctx *context.APIContext) {
	disableOrEnableWorkflowFile(ctx, true)
}

func disableOrEnableWorkflowFile(ctx *context.APIContext, isEnable bool) {
	workflow := ctx.FormString("workflow")
	if len(workflow) == 0 {
		ctx.ServerError("workflow", nil)
		return
	}

	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
	cfg := cfgUnit.ActionsConfig()

	if isEnable {
		cfg.EnableWorkflow(workflow)
	} else {
		cfg.DisableWorkflow(workflow)
	}

	if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
		ctx.ServerError("UpdateRepoUnit", err)
		return
	}

	ctx.Status(http.StatusNoContent)
}

func Cancel(ctx *context.APIContext) {
	runIndex := ctx.ParamsInt64("run")

	_, jobs := getRunJobs(ctx, runIndex, -1)
	if ctx.Written() {
		return
	}

	if err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
		for _, job := range jobs {
			status := job.Status
			if status.IsDone() {
				continue
			}
			if job.TaskID == 0 {
				job.Status = actions_model.StatusCancelled
				job.Stopped = timeutil.TimeStampNow()
				n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
				if err != nil {
					return err
				}
				if n == 0 {
					return fmt.Errorf("job has changed, try again")
				}
				continue
			}
			if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		ctx.Error(http.StatusInternalServerError, "runs Cancel", err.Error())
		return
	}

	actions_service.CreateCommitStatus(ctx, jobs...)

	ctx.JSON(http.StatusOK, struct{}{})
}
