package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

func split(paths string) ([]string, error) {
	rv := []string{}

	s := bufio.NewScanner(bytes.NewBufferString(paths))
	for s.Scan() {
		l := strings.TrimSpace(s.Text())
		if l != "" {
			rv = append(rv, l)
		}
	}

	if err := s.Err(); err != nil {
		return nil, err
	}
	return rv, nil
}

type file struct {
	path string
	data []byte
}

func read(paths []string) ([]*file, error) {
	// this method wastes some memory, but since we need to delete existing
	// version before starting to upload, it is better to ensure that all the
	// files are readable beforehand

	realpaths := []string{}
	for _, path := range paths {
		st, err := os.Stat(path)
		if err != nil {
			return nil, err
		}

		if st.IsDir() {
			if len(paths) > 1 {
				return nil, fmt.Errorf("read: when using directory, there must be only one path argument")
			}

			ee, err := os.ReadDir(path)
			if err != nil {
				return nil, err
			}
			for _, e := range ee {
				fn := filepath.Join(path, e.Name())
				if !e.Type().IsRegular() {
					return nil, fmt.Errorf("read: unsupported file type for %q: %s", fn, e.Type())
				}
				realpaths = append(realpaths, fn)
			}
		} else if st.Mode().IsRegular() {
			realpaths = append(realpaths, path)
		} else {
			return nil, fmt.Errorf("read: unsupported file type for %q: %s", path, st.Mode())
		}
	}

	rv := []*file{}
	for _, p := range realpaths {
		data, err := os.ReadFile(p)
		if err != nil {
			return nil, err
		}
		rv = append(rv, &file{
			path: p,
			data: data,
		})
	}
	return rv, nil
}

var (
	serverUrl string
	token     string
)

func request(method string, path string, body io.Reader) (*http.Response, error) {
	if serverUrl == "" {
		s, ok := os.LookupEnv("FORGEJO_SERVER_URL")
		if !ok {
			return nil, errors.New("request: missing FORGEJO_SERVER_URL")
		}
		serverUrl = strings.TrimRight(s, "/")
	}

	if token == "" {
		t, ok := os.LookupEnv("PACKAGES_TOKEN")
		if !ok {
			return nil, errors.New("request: missing PACKAGES_TOKEN")
		}
		token = t
	}

	req, err := http.NewRequest(method, serverUrl+"/"+strings.TrimLeft(path, "/"), body)
	if err != nil {
		return nil, err
	}
	req.Header.Add("authorization", "token "+token)

	return http.DefaultClient.Do(req)
}

func delete(user string, name string, version string) (bool, error) {
	log.Printf("checking if version %q exists ...", version)
	resp, err := request("GET", "/api/v1/packages/"+user+"/generic/"+name+"/"+version, nil)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	v := struct {
		Repository *struct {
			Id int `json:"id"`
		} `json:"repository"`
	}{}
	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
		return false, err
	}

	if resp.StatusCode == 200 {
		log.Printf("deleting version %q ...", version)
		resp, err := request("DELETE", "/api/v1/packages/"+user+"/generic/"+name+"/"+version, nil)
		if err != nil {
			return false, err
		}
		defer resp.Body.Close()
	}
	return v.Repository != nil, nil
}

func upload(user string, name string, version string, files []*file) error {
	// try to upload all before exiting
	errs := []error{}
	for _, f := range files {
		log.Printf("uploading %q for version %q ...", f.path, version)
		resp, err := request("PUT", "/api/packages/"+user+"/generic/"+name+"/"+version+"/"+filepath.Base(f.path), bytes.NewBuffer(f.data))
		if err != nil {
			errs = append(errs, err)
			continue
		}
		resp.Body.Close()
	}
	if len(errs) > 0 {
		return errors.Join(errs...)
	}
	return nil
}

func link(user string, name string, repo string) error {
	log.Printf("linking package to repository ...")
	resp, err := request("POST", "/api/v1/packages/"+user+"/generic/"+name+"/-/link/"+repo, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	allowed := []int{201, 400}
	if !slices.Contains(allowed, resp.StatusCode) {
		return fmt.Errorf("link: failed, %d not in %v", resp.StatusCode, allowed)
	}
	return nil
}

func check(err any) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %s\n", err)
		os.Exit(1)
	}
}

func main() {
	if len(os.Args) != 5 {
		check("invalid number of arguments")
	}

	p := strings.Split(os.Args[1], "/")
	if len(p) != 2 {
		check("invalid owner/repository")
	}

	user := p[0]
	repo := p[1]
	name := os.Args[2]
	version := os.Args[3]
	paths := os.Args[4]

	lpaths, err := split(paths)
	check(err)

	files, err := read(lpaths)
	check(err)

	wasLinked, err := delete(user, name, version)
	check(err)

	check(upload(user, name, version, files))

	if !wasLinked {
		check(link(user, name, repo))
	}
}
