// Dagger pipeline for multi-platform builds.
// Usage:
//
//	go run ci/dagger.go [--release v1.0.0]
//
// If --release is provided, uploads all built binaries in dist/ to the specified Gitea release.
// Requires .env file with GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO.
// Requires: go install dagger.io/dagger@latest && go get github.com/joho/godotenv && go mod tidy
package main

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

	"dagger.io/dagger"
	"github.com/joho/godotenv"
)

func main() {
	releaseTag := flag.String("release", "", "(optional) Release tag to upload binaries to Gitea")
	flag.Parse()

	ctx := context.Background()
	client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
	if err != nil {
		panic(err)
	}
	defer client.Close()

	platforms := []struct {
		OS   string
		Arch string
	}{
		{"darwin", "arm64"},
		{"darwin", "amd64"},
		{"windows", "amd64"},
		{"linux", "amd64"},
	}

	src := client.Host().Directory(".")

	var builtBinaries []string

	for _, p := range platforms {
		binName := fmt.Sprintf("valhallir-deconvolver-%s-%s", p.OS, p.Arch)
		if p.OS == "windows" {
			binName += ".exe"
		}

		ctr := client.Container().From("golang:1.24.5").
			WithMountedDirectory("/src", src).
			WithWorkdir("/src").
			WithEnvVariable("GOOS", p.OS).
			WithEnvVariable("GOARCH", p.Arch).
			WithExec([]string{"go", "build", "-o", binName, "."})

		// Export the binary from the container to the host's dist/ directory
		outPath := fmt.Sprintf("dist/%s", binName)
		_, err := ctr.File(binName).Export(ctx, outPath)
		if err != nil {
			panic(fmt.Sprintf("export failed for %s/%s: %v", p.OS, p.Arch, err))
		}
		fmt.Printf("Built and exported %s\n", outPath)
		builtBinaries = append(builtBinaries, outPath)
	}

	if *releaseTag != "" {
		fmt.Printf("\nUploading binaries to Gitea release: %s\n", *releaseTag)
		// Load .env
		err := godotenv.Load("ci/.env")
		if err != nil {
			panic("Error loading .env file: " + err.Error())
		}
		giteaToken := os.Getenv("GITEA_TOKEN")
		giteaURL := os.Getenv("GITEA_URL")
		giteaOwner := os.Getenv("GITEA_OWNER")
		giteaRepo := os.Getenv("GITEA_REPO")
		if giteaToken == "" || giteaURL == "" || giteaOwner == "" || giteaRepo == "" {
			panic("GITEA_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO must be set in .env")
		}

		// 1. Get or create the release
		releaseID, err := getOrCreateRelease(giteaURL, giteaOwner, giteaRepo, *releaseTag, giteaToken)
		if err != nil {
			panic("Failed to get or create release: " + err.Error())
		}

		// 2. Upload each binary as an asset
		for _, bin := range builtBinaries {
			fmt.Printf("Uploading %s...\n", bin)
			err := uploadAsset(giteaURL, giteaOwner, giteaRepo, releaseID, bin, giteaToken)
			if err != nil {
				panic(fmt.Sprintf("Failed to upload %s: %v", bin, err))
			}
			fmt.Printf("Uploaded %s\n", bin)
		}
		fmt.Println("All binaries uploaded to Gitea release.")

		// 3. Tag the release locally and push the tag
		if err := tagAndPush(*releaseTag); err != nil {
			panic("Failed to tag and push: " + err.Error())
		}
		fmt.Printf("Tagged and pushed %s\n", *releaseTag)
	}
}

func getOrCreateRelease(url, owner, repo, tag, token string) (int, error) {
	// Try to get the release by tag
	api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/tags/%s", strings.TrimRight(url, "/"), owner, repo, tag)
	req, _ := http.NewRequest("GET", api, nil)
	req.Header.Set("Authorization", "token "+token)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == 200 {
		// Parse JSON to get ID
		type releaseResp struct{ ID int }
		var r releaseResp
		io.ReadAll(resp.Body) // ignore body for now, parse below
		dec := json.NewDecoder(resp.Body)
		dec.Decode(&r)
		return r.ID, nil
	}
	// If not found, create it
	if resp.StatusCode == 404 {
		api = fmt.Sprintf("%s/api/v1/repos/%s/%s/releases", strings.TrimRight(url, "/"), owner, repo)
		body := strings.NewReader(fmt.Sprintf(`{"tag_name":"%s","name":"%s"}`, tag, tag))
		req, _ = http.NewRequest("POST", api, body)
		req.Header.Set("Authorization", "token "+token)
		req.Header.Set("Content-Type", "application/json")
		resp, err = http.DefaultClient.Do(req)
		if err != nil {
			return 0, err
		}
		defer resp.Body.Close()
		if resp.StatusCode == 201 {
			type releaseResp struct{ ID int }
			var r releaseResp
			dec := json.NewDecoder(resp.Body)
			dec.Decode(&r)
			return r.ID, nil
		}
		return 0, fmt.Errorf("failed to create release: %s", resp.Status)
	}
	return 0, fmt.Errorf("failed to get release: %s", resp.Status)
}

func uploadAsset(url, owner, repo string, releaseID int, filePath, token string) error {
	api := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%d/assets?name=%s", strings.TrimRight(url, "/"), owner, repo, releaseID, filepath.Base(filePath))
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	f, err := w.CreateFormFile("attachment", filepath.Base(filePath))
	if err != nil {
		return err
	}
	_, err = io.Copy(f, file)
	if err != nil {
		return err
	}
	w.Close()

	req, _ := http.NewRequest("POST", api, &b)
	req.Header.Set("Authorization", "token "+token)
	req.Header.Set("Content-Type", w.FormDataContentType())
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 201 {
		return fmt.Errorf("upload failed: %s", resp.Status)
	}
	return nil
}

func tagAndPush(tag string) error {
	// Check if tag exists
	cmd := exec.Command("git", "tag", "--list", tag)
	out, err := cmd.Output()
	if err != nil {
		return err
	}
	if strings.TrimSpace(string(out)) == tag {
		// Tag already exists
		return nil
	}
	// Create tag
	cmd = exec.Command("git", "tag", tag)
	if err := cmd.Run(); err != nil {
		return err
	}
	// Push tag
	cmd = exec.Command("git", "push", "origin", tag)
	if err := cmd.Run(); err != nil {
		return err
	}
	return nil
}
