// Package gitea is a minimal read-only client for the Gitea generic-package API. It lets the hub
// discover which artifact versions currently exist (olders get pruned) and read each file's sha256
// WITHOUT downloading the artifact — so the operator picks a version in the UI and the hub derives and
// vouches the checksum itself, instead of the operator pasting a 64-hex sha by hand.
package gitea

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"
)

// Client talks to the Gitea packages API with basic auth (the registry username + token the hub
// already holds for the controller image pull).
type Client struct {
	baseURL string // e.g. https://gitea.dooplex.hu (no trailing slash)
	owner   string // package owner, e.g. admin
	user    string
	token   string
	http    *http.Client
}

// New builds a client. baseURL/owner/user/token come from the hub's registry config.
func New(baseURL, owner, user, token string) *Client {
	return &Client{
		baseURL: strings.TrimRight(baseURL, "/"),
		owner:   owner,
		user:    user,
		token:   token,
		http:    &http.Client{Timeout: 10 * time.Second},
	}
}

type pkgEntry struct {
	Name    string `json:"name"`
	Version string `json:"version"`
	Type    string `json:"type"`
}

// ListVersions returns the versions of a generic package, newest-semver first. Non-semver versions
// (if any) sort after the semver ones so nothing is silently dropped.
func (c *Client) ListVersions(ctx context.Context, pkgName string) ([]string, error) {
	u := fmt.Sprintf("%s/api/v1/packages/%s?type=generic&q=%s&limit=100",
		c.baseURL, c.owner, pkgName)
	var pkgs []pkgEntry
	if err := c.getJSON(ctx, u, &pkgs); err != nil {
		return nil, err
	}
	var vers []string
	for _, p := range pkgs {
		if p.Name == pkgName && p.Version != "" {
			vers = append(vers, p.Version)
		}
	}
	sort.SliceStable(vers, func(i, j int) bool { return compareSemver(vers[i], vers[j]) > 0 })
	return vers, nil
}

type pkgFile struct {
	Name   string `json:"name"`
	SHA256 string `json:"sha256"`
}

// FileSHA256 returns the sha256 of a file inside a generic package version. When the version holds
// multiple files it prefers an exact match to preferredFile (e.g. "golden.tar.zst"); otherwise it uses
// the first file. This is a cheap metadata call — the artifact bytes are never downloaded.
func (c *Client) FileSHA256(ctx context.Context, pkgName, version, preferredFile string) (string, error) {
	u := fmt.Sprintf("%s/api/v1/packages/%s/generic/%s/%s/files",
		c.baseURL, c.owner, pkgName, version)
	var files []pkgFile
	if err := c.getJSON(ctx, u, &files); err != nil {
		return "", err
	}
	if len(files) == 0 {
		return "", fmt.Errorf("no files listed for %s/%s", pkgName, version)
	}
	if preferredFile != "" {
		for _, f := range files {
			if f.Name == preferredFile && f.SHA256 != "" {
				return f.SHA256, nil
			}
		}
	}
	if files[0].SHA256 == "" {
		return "", fmt.Errorf("no sha256 for %s/%s file %q", pkgName, version, files[0].Name)
	}
	return files[0].SHA256, nil
}

func (c *Client) getJSON(ctx context.Context, url string, out interface{}) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.SetBasicAuth(c.user, c.token)
	req.Header.Set("Accept", "application/json")
	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("gitea GET %s: HTTP %d", url, resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

// compareSemver returns >0 if a>b, 0 if equal, <0 if a<b. Falls back to a lexical compare on a
// non-"X.Y.Z" input (mirrors web.compareVersions; kept local to avoid an import cycle).
func compareSemver(a, b string) int {
	a = strings.TrimPrefix(a, "v")
	b = strings.TrimPrefix(b, "v")
	ap := strings.SplitN(a, ".", 3)
	bp := strings.SplitN(b, ".", 3)
	if len(ap) != 3 || len(bp) != 3 {
		return strings.Compare(a, b)
	}
	for i := 0; i < 3; i++ {
		ai, e1 := strconv.Atoi(ap[i])
		bi, e2 := strconv.Atoi(bp[i])
		if e1 != nil || e2 != nil {
			return strings.Compare(a, b)
		}
		if ai != bi {
			return ai - bi
		}
	}
	return 0
}
