package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/uptrace/opentelemetry-go-extra/otellogrus"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
	oteltrace "go.opentelemetry.io/otel/trace"
)

// Initialize a global logger
var logger = logrus.New()

// Initialize a global HTTP client with OpenTelemetry instrumentation
var httpClient *http.Client

// Initialize a global tracer
var tracer oteltrace.Tracer

// API endpoint templates
const (
	tagURLTemplate           = "%s/api/v1/repos/%s/tags/%s"
	tagsURLTemplate          = "%s/api/v1/repos/%s/tags"
	releasesURLTemplate      = "%s/api/v1/repos/%s/releases"
	releaseByTagURLTemplate  = "%s/api/v1/repos/%s/releases/tags/%s"
	releaseAssetsURLTemplate = "%s/api/v1/repos/%s/releases/%d/assets"
	releaseURLTemplate       = "%s/api/v1/repos/%s/releases/%d"
	latestReleaseURLTemplate = "%s/api/v1/repos/%s/releases/latest"
)

// Release represents a Forgejo release.
type Release struct {
	ID     int64  `json:"id"`
	Draft  bool   `json:"draft"`
	Tag    string `json:"tag_name"`
	Assets []struct {
		ID                 int64  `json:"id"`
		Name               string `json:"name"`
		BrowserDownloadURL string `json:"browser_download_url"`
	} `json:"assets"`
}

func main() {
	// Configure logger to output in JSON format
	logger.SetFormatter(&logrus.JSONFormatter{})
	logger.SetOutput(os.Stdout)
	logger.SetLevel(logrus.InfoLevel)

	// Parse command-line flags and environment variables.
	config, err := parseConfig()
	if err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Configuration error")
		os.Exit(1)
	}

	// Instrument logrus.
	logrus.AddHook(otellogrus.NewHook(otellogrus.WithLevels(
		logrus.PanicLevel,
		logrus.FatalLevel,
		logrus.ErrorLevel,
		logrus.WarnLevel,
	)))

	// Initialize OpenTelemetry
	ctx := context.Background()
	shutdown, err := initOpenTelemetry(ctx, config.TelemetryEndpoint)
	if err != nil {
		logger.WithContext(ctx).WithFields(logrus.Fields{
			"error": err.Error(),
		}).Fatal("Failed to initialize OpenTelemetry")
	}
	defer func() {
		if err := shutdown(ctx); err != nil {
			logger.WithFields(logrus.Fields{
				"error": err.Error(),
			}).Error("Failed to shutdown OpenTelemetry")
		}
	}()

	// Initialize tracer
	tracer = otel.Tracer("forgejo-release-manager")

	// Initialize the instrumented HTTP client
	httpClient = &http.Client{
		Transport: otelhttp.NewTransport(http.DefaultTransport),
		Timeout:   30 * time.Second,
	}

	logger.Error("Started OpenTelemetry client")

	// Execute the specified command.
	switch config.Command {
	case "create":
		err := createReleaseCommand(ctx, config)
		if err != nil {
			logger.WithFields(logrus.Fields{
				"command": config.Command,
				//"error":   err.Error(),
			}).Error("Release creation failed")
			os.Exit(1)
		}
		logger.WithFields(logrus.Fields{
			"command": config.Command,
			"tag":     config.Tag,
		}).Infof("Release '%s' created successfully.", config.Tag)
	case "download":
		err := downloadRelease(ctx, config)
		if err != nil {
			logger.WithFields(logrus.Fields{
				"command": config.Command,
				"error":   err.Error(),
			}).Error("Download failed")
			os.Exit(1)
		}
		logger.WithFields(logrus.Fields{
			"command": config.Command,
		}).Info("All assets downloaded successfully.")
	default:
		logger.WithFields(logrus.Fields{
			"command": config.Command,
		}).Error("Unknown command. Use 'create' or 'download'.")
		os.Exit(1)
	}
}

// Config holds the configuration parameters.
type Config struct {
	ForgejoURL        string
	TelemetryEndpoint string
	Repo              string
	Title             string
	ReleaseDir        string
	ReleaseNotes      string
	DownloadLatest    bool
	TmpDir            string
	Override          bool
	Retry             int
	Delay             int
	GPGPrivateKey     string
	GPGPassphrase     string
	Tag               string
	SHA               string
	Token             string
	Prerelease        bool
	Command           string
	Interactive       bool
}

// parseConfig parses command-line flags and environment variables.
func parseConfig() (*Config, error) {
	var config Config

	config.TelemetryEndpoint = getEnv("TELEMETRY_ENDPOINT", "https://telemetry.252.no") // OpenTelemetry endpoint URL

	flag.StringVar(&config.ForgejoURL, "forgejo-url", getEnv("FORGEJO_URL", "https://code.252.no"), "Forgejo instance URL")
	flag.StringVar(&config.Repo, "repo", getEnv("REPO", ""), "Repository in the format owner/repo-name (required)")
	flag.StringVar(&config.Title, "title", getEnv("TITLE", ""), "Release title (defaults to tag)")
	flag.StringVar(&config.ReleaseDir, "release-dir", getEnv("RELEASE_DIR", ""), "Directory containing release assets")
	flag.StringVar(&config.ReleaseNotes, "release-notes", getEnv("RELEASE_NOTES", ""), "File containing markdown-formatted release notes (required)")
	flag.BoolVar(&config.DownloadLatest, "download-latest", getEnvBool("DOWNLOAD_LATEST", false), "Download the latest release")
	flag.StringVar(&config.TmpDir, "tmp-dir", getEnv("TMP_DIR", ""), "Temporary directory")
	flag.BoolVar(&config.Override, "override", getEnvBool("OVERRIDE", false), "Override existing release and tag")
	flag.IntVar(&config.Retry, "retry", getEnvInt("RETRY", 1), "Number of retries when downloading")
	flag.IntVar(&config.Delay, "delay", getEnvInt("DELAY", 10), "Delay between retries in seconds")
	flag.StringVar(&config.GPGPrivateKey, "gpg-private-key", getEnv("GPG_PRIVATE_KEY", ""), "Path to GPG private key")
	flag.StringVar(&config.GPGPassphrase, "gpg-passphrase", getEnv("GPG_PASSPHRASE", ""), "Path to GPG passphrase file")
	flag.StringVar(&config.Tag, "tag", getEnv("TAG", ""), "Release tag (required for create)")
	flag.StringVar(&config.SHA, "sha", getEnv("SHA", ""), "Commit SHA for the tag (optional)")
	flag.StringVar(&config.Token, "forgejo-token", getEnv("FORGEJO_TOKEN", ""), "Forgejo API token (required)")
	flag.BoolVar(&config.Prerelease, "prerelease", getEnvBool("PRERELEASE", false), "Mark the release as a prerelease")
	flag.BoolVar(&config.Interactive, "interactive", getEnvBool("INTERACTIVE", false), "Interactive mode presents a guide")

	// Check if the program is run without arguments (interactive mode).
	if config.Interactive {
		interactiveSetup(&config)
		return &config, nil
	}

	flag.Usage = func() {
		fmt.Printf("Usage: %s <create|download> [options]\n", os.Args[0])
		fmt.Println("\nCommands:")
		fmt.Println("  create    Create a new release with uploaded assets")
		fmt.Println("  download  Download assets from an existing release")
		fmt.Println("\nOptions:")
		flag.PrintDefaults()
		fmt.Println("\nExamples:")
		fmt.Printf("  %s create --repo=owner/repo --tag=v1.0.0 --forgejo-token=your-token --release-dir=path/to/assets\n", os.Args[0])
		fmt.Printf("  %s download --repo=owner/repo --tag=v1.0.0 --forgejo-token=your-token --release-dir=path/to/download\n", os.Args[0])
	}

	flag.Parse()

	// Get the command (create or download).
	if len(flag.Args()) < 1 {
		flag.Usage()
		os.Exit(1)
	}
	config.Command = flag.Args()[0]

	// Convert ReleaseDir to an absolute path
	absReleaseDir, err := filepath.Abs(config.ReleaseDir)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve absolute path for release directory: %w", err)
	}
	config.ReleaseDir = absReleaseDir

	// Validate required parameters.
	if config.Repo == "" {
		return nil, errors.New("repository (--repo) is required")
	}
	if config.Token == "" {
		return nil, errors.New("API token (--forgejo-token) is required")
	}
	if config.Command == "create" && config.Tag == "" {
		return nil, errors.New("tag (--tag) is required for creating a release")
	}
	if config.Command == "download" && !config.DownloadLatest && config.Tag == "" {
		return nil, errors.New("tag (--tag) is required unless --download-latest is true")
	}
	if config.ReleaseNotes == "" {
		return nil, errors.New("release notes (--release-notes) is required")
	}
	return &config, nil
}

// interactiveSetup prompts the user for configuration in interactive mode.
func interactiveSetup(config *Config) {
	reader := bufio.NewReader(os.Stdin)
	logger.Info("Interactive Mode: Please provide the following details.")

	config.Command = promptChoice(reader, "Choose a command (create/download): ", []string{"create", "download"}, "create")
	config.ForgejoURL = prompt(reader, "Forgejo instance URL", getEnv("FORGEJO_URL", "https://code.252.no"))
	config.Repo = prompt(reader, "Repository (owner/name)", "")
	config.Token = prompt(reader, "Forgejo API token", "")
	if config.Command == "create" {
		config.ReleaseDir = prompt(reader, "Directory containing release assets", getEnv("RELEASE_DIR", "dist/"))
		config.Tag = prompt(reader, "Release tag", "")
		config.Title = prompt(reader, "Release title", config.Tag)
		config.ReleaseNotes = prompt(reader, "Release notes path", config.ReleaseNotes)
		config.SHA = prompt(reader, "Commit SHA for the tag (leave blank if tag exists)", "")
		config.Prerelease = promptBool(reader, "Is this a prerelease?", false)
		config.Override = promptBool(reader, "Override existing release and tag if they exist?", false)
		config.GPGPrivateKey = prompt(reader, "Path to GPG private key (optional)", "")
		config.GPGPassphrase = prompt(reader, "Path to GPG passphrase file (optional)", "")
	} else if config.Command == "download" {
		config.ReleaseDir = prompt(reader, "Directory to download assets into", getEnv("RELEASE_DIR", "dist/"))
		config.DownloadLatest = promptBool(reader, "Download the latest release?", false)
		if !config.DownloadLatest {
			config.Tag = prompt(reader, "Release tag to download", "")
		}
		config.Retry = promptInt(reader, "Number of retries if release is not available", 1)
		config.Delay = promptInt(reader, "Delay between retries in seconds", 10)
	}
}

// Helper functions for interactive prompts.
func prompt(reader *bufio.Reader, label, defaultValue string) string {
	if defaultValue != "" {
		fmt.Printf("%s [%s]: ", label, defaultValue)
	} else {
		fmt.Printf("%s: ", label)
	}
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)
	if input == "" {
		return defaultValue
	}
	return input
}

func promptBool(reader *bufio.Reader, label string, defaultValue bool) bool {
	defaultStr := "no"
	if defaultValue {
		defaultStr = "yes"
	}
	for {
		fmt.Printf("%s [%s]: ", label, defaultStr)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(strings.ToLower(input))
		if input == "" {
			return defaultValue
		}
		if input == "yes" || input == "y" {
			return true
		}
		if input == "no" || input == "n" {
			return false
		}
		fmt.Println("Please enter 'yes' or 'no'.")
	}
}

func promptInt(reader *bufio.Reader, label string, defaultValue int) int {
	for {
		fmt.Printf("%s [%d]: ", label, defaultValue)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(input)
		if input == "" {
			return defaultValue
		}
		value, err := strconv.Atoi(input)
		if err == nil {
			return value
		}
		fmt.Println("Please enter a valid integer.")
	}
}

func promptChoice(reader *bufio.Reader, label string, choices []string, defaultValue string) string {
	choiceMap := make(map[string]bool)
	for _, choice := range choices {
		choiceMap[choice] = true
	}
	for {
		fmt.Printf("%s [%s]: ", label, defaultValue)
		input, _ := reader.ReadString('\n')
		input = strings.TrimSpace(strings.ToLower(input))
		if input == "" {
			return defaultValue
		}
		if _, valid := choiceMap[input]; valid {
			return input
		}
		fmt.Printf("Invalid choice. Valid options are: %s\n", strings.Join(choices, ", "))
	}
}

// initOpenTelemetry initializes OpenTelemetry with an OTLP HTTP exporter.
func initOpenTelemetry(ctx context.Context, endpoint string) (func(context.Context) error, error) {
	// Create OTLP HTTP exporter
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpoint(strings.TrimPrefix(endpoint, "https://")),
		otlptracehttp.WithURLPath("/v1/traces"), // Adjust path if necessary
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP HTTP exporter: %w", err)
	}

	// Create Tracer Provider
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("forgejo-release-manager"),
		)),
	)

	// Set the global Tracer Provider
	otel.SetTracerProvider(tp)

	return tp.Shutdown, nil
}

// createReleaseCommand handles the creation of a release.
func createReleaseCommand(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "createReleaseCommand")
	defer span.End()

	// Create temporary directory if not provided.
	if config.TmpDir == "" {
		var err error
		config.TmpDir, err = os.MkdirTemp("", "forgejo-release")
		if err != nil {
			return fmt.Errorf("failed to create temp directory: %w", err)
		}
		defer os.RemoveAll(config.TmpDir)
		logger.WithFields(logrus.Fields{
			"tmp_dir": config.TmpDir,
		}).Info("Temporary directory created")
	}

	// Override existing release and tag if requested.
	if config.Override {
		if err := deleteRelease(ctx, config); err != nil {
			return err
		}
		if err := deleteTag(ctx, config); err != nil {
			return err
		}
	}

	// Ensure the tag exists.
	if err := ensureTag(ctx, config); err != nil {
		return fmt.Errorf("failed to ensure tag: %w", err)
	}

	// Sign release assets if GPG key is provided.
	if config.GPGPrivateKey != "" {
		if err := signReleaseAssets(ctx, config); err != nil {
			return fmt.Errorf("failed to sign assets: %w", err)
		}
	}

	// Create and publish the release.
	if err := createAndPublishRelease(ctx, config); err != nil {
		return err
	}

	return nil
}

// downloadRelease handles the download of a release.
func downloadRelease(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "downloadRelease")
	defer span.End()

	// Create temporary directory if not provided.
	if config.TmpDir == "" {
		var err error
		config.TmpDir, err = os.MkdirTemp("", "forgejo-release")
		if err != nil {
			return fmt.Errorf("failed to create temp directory: %w", err)
		}
		defer os.RemoveAll(config.TmpDir)
		logger.WithFields(logrus.Fields{
			"tmp_dir": config.TmpDir,
		}).Info("Temporary directory created")
	}

	// Retrieve the release information.
	release, err := getRelease(ctx, config)
	if err != nil {
		return err
	}

	// Download the assets.
	if err := downloadAssets(ctx, config, release); err != nil {
		return fmt.Errorf("failed to download assets: %w", err)
	}

	return nil
}

// ensureTag checks if the tag exists and creates it if it doesn't.
func ensureTag(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "ensureTag")
	defer span.End()

	tagSHA, err := getTagSHA(ctx, config)
	if err != nil {
		if errors.Is(err, errTagNotFound) {
			// Tag does not exist; create it if SHA is provided.
			if config.SHA == "" {
				return errors.New("tag does not exist and SHA is not provided to create it")
			}
			if err := createTag(ctx, config); err != nil {
				return fmt.Errorf("failed to create tag: %w", err)
			}
			logger.WithFields(logrus.Fields{
				"tag": config.Tag,
			}).Info("Tag created successfully")
		} else {
			return err
		}
	} else {
		// Tag exists; verify SHA if provided.
		if config.SHA != "" && config.SHA != tagSHA {
			return errors.New("tag SHA does not match the provided SHA")
		}
		// Use the tag's SHA if not provided.
		if config.SHA == "" {
			config.SHA = tagSHA
		}
		logger.WithFields(logrus.Fields{
			"tag": config.Tag,
		}).Info("Tag already exists")
	}
	return nil
}

// getTagSHA retrieves the SHA associated with the tag.
func getTagSHA(ctx context.Context, config *Config) (string, error) {
	ctx, span := tracer.Start(ctx, "getTagSHA")
	defer span.End()

	apiURL := fmt.Sprintf(tagURLTemplate, config.ForgejoURL, config.Repo, config.Tag)
	resp, err := apiRequest(ctx, "GET", apiURL, nil, config.Token)
	if err != nil {
		return "", fmt.Errorf("failed to get tag: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusOK {
		var result struct {
			Commit struct {
				SHA string `json:"sha"`
			} `json:"commit"`
		}
		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
			return "", fmt.Errorf("failed to decode tag response: %w", err)
		}
		return result.Commit.SHA, nil
	} else if resp.StatusCode == http.StatusNotFound {
		return "", errTagNotFound
	}

	bodyBytes, _ := io.ReadAll(resp.Body)
	logger.WithFields(logrus.Fields{
		"method": resp.Request.Method,
		"url":    resp.Request.URL.String(),
		"status": resp.StatusCode,
		"body":   string(bodyBytes),
	}).Error("Failed to get tag")
	return "", fmt.Errorf("failed to get tag: %s", string(bodyBytes))
}

var errTagNotFound = errors.New("tag not found")

// createTag creates a new tag in the repository.
func createTag(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "createTag")
	defer span.End()

	payload := map[string]string{
		"tag_name": config.Tag,
		"target":   config.SHA,
	}
	payloadBytes, _ := json.Marshal(payload)
	apiURL := fmt.Sprintf(tagsURLTemplate, config.ForgejoURL, config.Repo)
	resp, err := apiRequest(ctx, "POST", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
	if err != nil {
		return fmt.Errorf("failed to create tag: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to create tag")
		return fmt.Errorf("failed to create tag: %s", string(bodyBytes))
	}
	return nil
}

// createAndPublishRelease creates a new release and uploads assets.
func createAndPublishRelease(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "createAndPublishRelease")
	defer span.End()

	// Create the release.
	releaseID, err := createRelease(ctx, config)
	if err != nil {
		return err
	}

	// Upload assets.
	if err := uploadAssets(ctx, config, releaseID); err != nil {
		return err
	}

	// Publish the release.
	if err := updateReleaseDraftState(ctx, config, releaseID, false); err != nil {
		return err
	}

	return nil
}

// ErrorResponse represents the structure of error responses from the server.
type ErrorResponse struct {
	Message string `json:"message"`
	Code    int    `json:"code,omitempty"`
	URL     string `json:"url,omitempty"`
}

func handleErrorResponse(resp *http.Response) error {
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"httpMethod": resp.Request.Method,
			"httpUrl":    resp.Request.URL.String(),
			"httpStatus": resp.StatusCode,
			"httpError":  err.Error(),
		}).Error("Failed to read response body")
		return fmt.Errorf("failed to read response body: %w", err)
	}

	// Attempt to deserialize the response body as JSON.
	var errorResp ErrorResponse
	if err := json.Unmarshal(bodyBytes, &errorResp); err == nil && errorResp.Message != "" {
		// JSON deserialization succeeded and message is present.
		logger.WithFields(logrus.Fields{
			"method":         resp.Request.Method,
			"url":            resp.Request.URL.String(),
			"status":         resp.StatusCode,
			"server_message": errorResp.Message,
			"server_code":    errorResp.Code,
			"server_url":     errorResp.URL,
		}).Error("Server returned an error")
		if errorResp.Code != 0 {
			return fmt.Errorf("server error: %s (code: %d)", errorResp.Message, errorResp.Code)
		}
		return fmt.Errorf("server error: %s", errorResp.Message)
	}

	// JSON deserialization failed or message is missing; treat response as plain text.
	logger.WithFields(logrus.Fields{
		"method":          resp.Request.Method,
		"url":             resp.Request.URL.String(),
		"status":          resp.StatusCode,
		"server_response": string(bodyBytes),
	}).Error("Invalid response format from server")
	return fmt.Errorf("invalid response from server: %s", string(bodyBytes))
}

// createRelease creates a new release and returns its ID.
func createRelease(ctx context.Context, config *Config) (int64, error) {
	ctx, span := tracer.Start(ctx, "createRelease")
	defer span.End()

	isPrerelease := config.Prerelease || strings.Contains(strings.ToLower(config.Tag), "-rc")
	title := config.Title
	if title == "" {
		title = config.Tag
	}

	releaseNotes, err := os.ReadFile(config.ReleaseNotes)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"release_notes": config.ReleaseNotes,
			"error":         err.Error(),
		}).Error("Failed to open release notes")
		return 0, fmt.Errorf("failed to open release notes")
	}

	// TOOD(tom): first check if the release exists before trying to create it
	//            or! we can check for a status=409
	payload := map[string]interface{}{
		"tag_name":   config.Tag,
		"name":       title,
		"prerelease": isPrerelease,
		"draft":      true,
		"body":       string(releaseNotes),
	}
	payloadBytes, _ := json.Marshal(payload)

	apiURL := fmt.Sprintf(releasesURLTemplate, config.ForgejoURL, config.Repo)
	resp, err := apiRequest(ctx, "POST", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
	if err != nil {
		return 0, fmt.Errorf("%w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusConflict {
		logger.WithFields(logrus.Fields{
			"tag_name": config.Tag,
			"name":     title,
		}).Warn("Release already exists")
		return 0, fmt.Errorf("%w", err)

	} else if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		return 0, handleErrorResponse(resp)
	}

	var release Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Invalid content when decode response")
		return 0, fmt.Errorf("invalid content when decode response")
	}

	logger.WithFields(logrus.Fields{
		"release_id": release.ID,
		"tag":        release.Tag,
	}).Info("Release created successfully")
	return release.ID, nil
}

// uploadAssets uploads the assets to the release.
func uploadAssets(ctx context.Context, config *Config, releaseID int64) error {
	ctx, span := tracer.Start(ctx, "uploadAssets")
	defer span.End()

	entries, err := os.ReadDir(config.ReleaseDir)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"release_dir": config.ReleaseDir,
			"error":       err.Error(),
		}).Error("Failed to read release directory")
		return fmt.Errorf("unable to read release directory: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		filePath := filepath.Join(config.ReleaseDir, entry.Name())
		if err := uploadAsset(ctx, config, releaseID, filePath); err != nil {
			return fmt.Errorf("unable to upload asset %s: %w", entry.Name(), err)
		}
		logger.WithFields(logrus.Fields{
			"asset": entry.Name(),
		}).Info("Asset uploaded successfully")
	}
	return nil
}

// uploadAsset uploads a single asset to the release.
func uploadAsset(ctx context.Context, config *Config, releaseID int64, filePath string) error {
	ctx, span := tracer.Start(ctx, "uploadAsset", oteltrace.WithAttributes())
	defer span.End()

	file, err := os.Open(filePath)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filePath,
			"error":     err.Error(),
		}).Error("Failed to open asset file")
		return fmt.Errorf("failed to open asset file: %w", err)
	}
	defer file.Close()

	fileInfo, err := file.Stat()
	if err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filePath,
			"error":     err.Error(),
		}).Error("Failed to get file info")
		return fmt.Errorf("failed to get file info: %w", err)
	}

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("attachment", fileInfo.Name())
	if err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filePath,
			"error":     err.Error(),
		}).Error("Failed to create form file")
		return fmt.Errorf("failed to create form file: %w", err)
	}
	if _, err := io.Copy(part, file); err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filePath,
			"error":     err.Error(),
		}).Error("Failed to copy file content")
		return fmt.Errorf("failed to copy file content: %w", err)
	}
	if err := writer.Close(); err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Failed to close multipart writer")
		return fmt.Errorf("failed to close multipart writer: %w", err)
	}

	apiURL := fmt.Sprintf(releaseAssetsURLTemplate, config.ForgejoURL, config.Repo, releaseID)
	req, err := http.NewRequestWithContext(ctx, "POST", apiURL, body)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": "POST",
			"url":    apiURL,
			"error":  err.Error(),
		}).Error("Failed to create upload request")
		return fmt.Errorf("failed to create upload request: %w", err)
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())
	req.Header.Set("Authorization", fmt.Sprintf("token %s", config.Token))

	// Use the global instrumented HTTP client
	resp, err := httpClient.Do(req)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": "POST",
			"url":    apiURL,
			"error":  err.Error(),
		}).Error("Failed to upload asset")
		return fmt.Errorf("failed to upload asset: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": "POST",
			"url":    apiURL,
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to upload asset")
		return fmt.Errorf("failed to upload asset: %s", string(bodyBytes))
	}

	return nil
}

// updateReleaseDraftState updates the draft state of the release.
func updateReleaseDraftState(ctx context.Context, config *Config, releaseID int64, state bool) error {
	ctx, span := tracer.Start(ctx, "updateReleaseDraftState")
	defer span.End()

	payload := map[string]bool{
		"draft": state,
	}
	payloadBytes, _ := json.Marshal(payload)
	apiURL := fmt.Sprintf(releaseURLTemplate, config.ForgejoURL, config.Repo, releaseID)
	resp, err := apiRequest(ctx, "PATCH", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
	if err != nil {
		return fmt.Errorf("failed to update release draft state: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to update release draft state")
		return fmt.Errorf("failed to update release draft state: %s", string(bodyBytes))
	}
	return nil
}

// signReleaseAssets signs the release assets using GPG.
func signReleaseAssets(ctx context.Context, config *Config) error {
	//ctx, span := tracer.Start(ctx, "signReleaseAssets")
	//defer span.End()

	gpgHome, err := os.MkdirTemp("", "gpg")
	if err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Failed to create GPG home directory")
		return fmt.Errorf("failed to create GPG home directory: %w", err)
	}
	defer os.RemoveAll(gpgHome)

	// Import GPG private key.
	gpgArgs := []string{"--batch", "--yes", "--import", config.GPGPrivateKey}
	if config.GPGPassphrase != "" {
		gpgArgs = append([]string{"--passphrase-file", config.GPGPassphrase}, gpgArgs...)
	}
	cmd := exec.Command("gpg", gpgArgs...)
	cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
	output, err := cmd.CombinedOutput()
	if err != nil {
		logger.WithFields(logrus.Fields{
			"command": strings.Join(cmd.Args, " "),
			"error":   err.Error(),
			"output":  string(output),
		}).Error("Failed to import GPG key")
		return fmt.Errorf("failed to import GPG key: %w, output: %s", err, string(output))
	}

	// Sign each asset.
	entries, err := os.ReadDir(config.ReleaseDir)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"release_dir": config.ReleaseDir,
			"error":       err.Error(),
		}).Error("Failed to read release directory for signing")
		return fmt.Errorf("failed to read release directory: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() || strings.HasSuffix(entry.Name(), ".sha256") {
			continue
		}
		filePath := filepath.Join(config.ReleaseDir, entry.Name())
		sigPath := filePath + ".asc"

		cmdArgs := []string{"--armor", "--detach-sign", "--output", sigPath, filePath}
		if config.GPGPassphrase != "" {
			cmdArgs = append([]string{"--batch", "--yes", "--passphrase-file", config.GPGPassphrase}, cmdArgs...)
		}
		cmd = exec.Command("gpg", cmdArgs...)
		cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
		output, err := cmd.CombinedOutput()
		if err != nil {
			logger.WithFields(logrus.Fields{
				"command": strings.Join(cmd.Args, " "),
				"file":    filePath,
				"error":   err.Error(),
				"output":  string(output),
			}).Error("Failed to sign asset")
			return fmt.Errorf("failed to sign asset %s: %w, output: %s", entry.Name(), err, string(output))
		}
		logger.WithFields(logrus.Fields{
			"file": sigPath,
		}).Info("Asset signed successfully")
	}

	return nil
}

// deleteRelease deletes an existing release by tag.
func deleteRelease(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "deleteRelease")
	defer span.End()

	apiURL := fmt.Sprintf(releaseByTagURLTemplate, config.ForgejoURL, config.Repo, config.Tag)
	resp, err := apiRequest(ctx, "DELETE", apiURL, nil, config.Token)
	if err != nil {
		return fmt.Errorf("failed to delete release: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to delete release")
		return fmt.Errorf("failed to delete release: %s", string(bodyBytes))
	}

	logger.WithFields(logrus.Fields{
		"tag": config.Tag,
	}).Info("Release deleted successfully")
	return nil
}

// deleteTag deletes an existing tag.
func deleteTag(ctx context.Context, config *Config) error {
	ctx, span := tracer.Start(ctx, "deleteTag")
	defer span.End()

	apiURL := fmt.Sprintf("%s/api/v1/repos/%s/tags/%s", config.ForgejoURL, config.Repo, config.Tag)
	resp, err := apiRequest(ctx, "DELETE", apiURL, nil, config.Token)
	if err != nil {
		return fmt.Errorf("failed to delete tag: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to delete tag")
		return fmt.Errorf("failed to delete tag: %s", string(bodyBytes))
	}

	logger.WithFields(logrus.Fields{
		"tag": config.Tag,
	}).Info("Tag deleted successfully")
	return nil
}

// getRelease retrieves the release information.
func getRelease(ctx context.Context, config *Config) (*Release, error) {
	ctx, span := tracer.Start(ctx, "getRelease")
	defer span.End()

	var release *Release
	var err error

	for i := 0; i < config.Retry; i++ {
		if config.DownloadLatest {
			release, err = getLatestRelease(ctx, config)
		} else {
			release, err = getReleaseByTag(ctx, config)
		}

		if err == nil && !release.Draft {
			return release, nil
		}

		logger.WithFields(logrus.Fields{
			"retry":    i + 1,
			"delay":    config.Delay,
			"command":  config.Command,
			"download": config.DownloadLatest,
			"tag":      config.Tag,
		}).Infof("Release not ready yet, retrying in %d seconds...", config.Delay)
		time.Sleep(time.Duration(config.Delay) * time.Second)
	}

	return nil, errors.New("release not available after retries")
}

// getLatestRelease retrieves the latest release.
func getLatestRelease(ctx context.Context, config *Config) (*Release, error) {
	ctx, span := tracer.Start(ctx, "getLatestRelease")
	defer span.End()

	apiURL := fmt.Sprintf(latestReleaseURLTemplate, config.ForgejoURL, config.Repo)
	resp, err := apiRequest(ctx, "GET", apiURL, nil, config.Token)
	if err != nil {
		return nil, fmt.Errorf("failed to get latest release: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to get latest release")
		return nil, fmt.Errorf("failed to get latest release: %s", string(bodyBytes))
	}

	var release Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Failed to decode latest release")
		return nil, fmt.Errorf("failed to decode latest release: %w", err)
	}
	return &release, nil
}

// getReleaseByTag retrieves a release by its tag.
func getReleaseByTag(ctx context.Context, config *Config) (*Release, error) {
	ctx, span := tracer.Start(ctx, "getReleaseByTag")
	defer span.End()

	apiURL := fmt.Sprintf("%s/api/v1/repos/%s/releases/tags/%s", config.ForgejoURL, config.Repo, config.Tag)
	resp, err := apiRequest(ctx, "GET", apiURL, nil, config.Token)
	if err != nil {
		return nil, fmt.Errorf("failed to get release by tag: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": resp.Request.Method,
			"url":    resp.Request.URL.String(),
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to get release by tag")
		return nil, fmt.Errorf("failed to get release by tag: %s", string(bodyBytes))
	}

	var release Release
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		logger.WithFields(logrus.Fields{
			"error": err.Error(),
		}).Error("Failed to decode release by tag")
		return nil, fmt.Errorf("failed to decode release by tag: %w", err)
	}
	return &release, nil
}

// downloadAssets downloads all assets from the release.
func downloadAssets(ctx context.Context, config *Config, release *Release) error {
	ctx, span := tracer.Start(ctx, "downloadAssets")
	defer span.End()

	if err := os.MkdirAll(config.ReleaseDir, 0755); err != nil {
		logger.WithFields(logrus.Fields{
			"release_dir": config.ReleaseDir,
			"error":       err.Error(),
		}).Error("Failed to create release directory")
		return fmt.Errorf("failed to create release directory: %w", err)
	}

	for _, asset := range release.Assets {
		assetPath := filepath.Join(config.ReleaseDir, asset.Name)
		if err := downloadAsset(ctx, config, asset.BrowserDownloadURL, assetPath); err != nil {
			return fmt.Errorf("failed to download asset %s: %w", asset.Name, err)
		}
		logger.WithFields(logrus.Fields{
			"asset": asset.Name,
		}).Info("Downloaded asset successfully")
	}

	return nil
}

// downloadAsset downloads a single asset.
func downloadAsset(ctx context.Context, config *Config, url, filepath string) error {
	ctx, span := tracer.Start(ctx, "downloadAsset")
	defer span.End()

	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": "GET",
			"url":    url,
			"error":  err.Error(),
		}).Error("Failed to create download request")
		return fmt.Errorf("failed to create download request: %w", err)
	}
	req.Header.Set("Authorization", fmt.Sprintf("token %s", config.Token))

	// Use the global instrumented HTTP client
	resp, err := httpClient.Do(req)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": "GET",
			"url":    url,
			"error":  err.Error(),
		}).Error("Failed to download asset")
		return fmt.Errorf("failed to download asset: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		logger.WithFields(logrus.Fields{
			"method": "GET",
			"url":    url,
			"status": resp.StatusCode,
			"body":   string(bodyBytes),
		}).Error("Failed to download asset")
		return fmt.Errorf("failed to download asset: %s", string(bodyBytes))
	}

	outFile, err := os.Create(filepath)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filepath,
			"error":     err.Error(),
		}).Error("Failed to create file")
		return fmt.Errorf("failed to create file: %w", err)
	}
	defer outFile.Close()

	if _, err := io.Copy(outFile, resp.Body); err != nil {
		logger.WithFields(logrus.Fields{
			"file_path": filepath,
			"error":     err.Error(),
		}).Error("Failed to write asset to file")
		return fmt.Errorf("failed to write asset to file: %w", err)
	}

	return nil
}

// apiRequest sends an HTTP request to the Forgejo API with context.
func apiRequest(ctx context.Context, method, url string, body io.Reader, token string) (*http.Response, error) {
	ctx, span := tracer.Start(ctx, "apiRequest")
	defer span.End()

	req, err := http.NewRequestWithContext(ctx, method, url, body)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": method,
			"url":    url,
			"error":  err.Error(),
		}).Error("Failed to create API request")
		return nil, fmt.Errorf("failed to create API request: %w", err)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Authorization", fmt.Sprintf("token %s", token))

	// Use the global instrumented HTTP client
	resp, err := httpClient.Do(req)
	if err != nil {
		logger.WithFields(logrus.Fields{
			"method": method,
			"url":    url,
			"error":  err.Error(),
		}).Error("API request failed")
		return nil, fmt.Errorf("API request failed: %w", err)
	}

	return resp, nil
}

// Helper functions to get environment variables with defaults.
func getEnv(key, defaultValue string) string {
	if value, exists := os.LookupEnv(key); exists {
		return value
	}
	return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
	if valueStr, exists := os.LookupEnv(key); exists {
		if value, err := strconv.Atoi(valueStr); err == nil {
			return value
		}
	}
	return defaultValue
}

func getEnvBool(key string, defaultValue bool) bool {
	if valueStr, exists := os.LookupEnv(key); exists {
		if value, err := strconv.ParseBool(valueStr); err == nil {
			return value
		}
	}
	return defaultValue
}
