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

package actions

import (
	"crypto/md5"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"code.gitea.io/gitea/models/actions"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/util"
)

const (
	artifactXTfsFileLengthHeader     = "x-tfs-filelength"
	artifactXActionsResultsMD5Header = "x-actions-results-md5"
)

// The rules are from https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/path-and-artifact-name-validation.ts#L32
var invalidArtifactNameChars = strings.Join([]string{"\\", "/", "\"", ":", "<", ">", "|", "*", "?", "\r", "\n"}, "")

func validateArtifactName(ctx *ArtifactContext, artifactName string) bool {
	if strings.ContainsAny(artifactName, invalidArtifactNameChars) {
		log.Error("Error checking artifact name contains invalid character")
		ctx.Error(http.StatusBadRequest, "Error checking artifact name contains invalid character")
		return false
	}
	return true
}

func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
	task := ctx.ActionTask
	runID := ctx.ParamsInt64("run_id")
	if task.Job.RunID != runID {
		log.Error("Error runID not match")
		ctx.Error(http.StatusBadRequest, "run-id does not match")
		return nil, 0, false
	}
	return task, runID, true
}

func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
	task := ctx.ActionTask
	runID, err := strconv.ParseInt(rawRunID, 10, 64)
	if err != nil || task.Job.RunID != runID {
		log.Error("Error runID not match")
		ctx.Error(http.StatusBadRequest, "run-id does not match")
		return nil, 0, false
	}
	return task, runID, true
}

func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
	paramHash := ctx.Params("artifact_hash")
	// use artifact name to create upload url
	artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(artifactName)))
	if paramHash == artifactHash {
		return true
	}
	log.Warn("Invalid artifact hash: %s", paramHash)
	ctx.Error(http.StatusBadRequest, "Invalid artifact hash")
	return false
}

func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
	// itemPath is generated from upload-artifact action
	// it's formatted as {artifact_name}/{artfict_path_in_runner}
	// act_runner in host mode on Windows, itemPath is joined by Windows slash '\'
	itemPath := util.PathJoinRelX(ctx.Req.URL.Query().Get("itemPath"))
	artifactName := strings.Split(itemPath, "/")[0]
	artifactPath := strings.TrimPrefix(itemPath, artifactName+"/")
	if !validateArtifactHash(ctx, artifactName) {
		return "", "", false
	}
	if !validateArtifactName(ctx, artifactName) {
		return "", "", false
	}
	return artifactName, artifactPath, true
}

// getUploadFileSize returns the size of the file to be uploaded.
// The raw size is the size of the file as reported by the header X-TFS-FileLength.
func getUploadFileSize(ctx *ArtifactContext) (int64, int64, error) {
	contentLength := ctx.Req.ContentLength
	xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
	if xTfsLength > 0 {
		return xTfsLength, contentLength, nil
	}
	return contentLength, contentLength, nil
}
