//go:build fasthttp

package gitea

import (
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/valyala/fasthttp"
	"github.com/valyala/fastjson"

	"codeberg.org/codeberg/pages/server/cache"
)

const (
	giteaAPIRepos = "/api/v1/repos/"
)

type Client struct {
	giteaRoot      string
	giteaAPIToken  string
	infoTimeout    time.Duration
	contentTimeout time.Duration
	fastClient     *fasthttp.Client
	responseCache  cache.SetGetKey

	followSymlinks bool
	supportLFS     bool
}

func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, followSymlinks, supportLFS bool) (*Client, error) {
	rootURL, err := url.Parse(giteaRoot)
	giteaRoot = strings.Trim(rootURL.String(), "/")

	return &Client{
		giteaRoot:      giteaRoot,
		giteaAPIToken:  giteaAPIToken,
		infoTimeout:    5 * time.Second,
		contentTimeout: 10 * time.Second,
		fastClient:     getFastHTTPClient(),
		responseCache:  respCache,

		followSymlinks: followSymlinks,
		supportLFS:     supportLFS,
	}, err
}

func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) {
	resp, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource)
	if err != nil {
		return nil, err
	}
	return resp.Body(), nil
}

func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (*fasthttp.Response, error) {
	var apiURL string
	if client.supportLFS {
		apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "media", resource+"?ref="+url.QueryEscape(ref))
	} else {
		apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "raw", resource+"?ref="+url.QueryEscape(ref))
	}
	resp, err := client.do(client.contentTimeout, apiURL)
	if err != nil {
		return nil, err
	}

	switch resp.StatusCode() {
	case fasthttp.StatusOK:
		objType := string(resp.Header.Peek(giteaObjectTypeHeader))
		log.Trace().Msgf("server raw content object: %s", objType)
		if client.followSymlinks && objType == "symlink" {
			// TODO: limit to 1000 chars if we switched to std
			linkDest := strings.TrimSpace(string(resp.Body()))
			log.Debug().Msgf("follow symlink from '%s' to '%s'", resource, linkDest)
			return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
		}

		return resp, nil

	case fasthttp.StatusNotFound:
		return nil, ErrorNotFound

	default:
		return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode())
	}
}

func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) {
	cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName)

	if stamp, ok := client.responseCache.Get(cacheKey); ok && stamp != nil {
		return stamp.(*BranchTimestamp), nil
	}

	url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName, "branches", branchName)
	res, err := client.do(client.infoTimeout, url)
	if err != nil {
		return &BranchTimestamp{}, err
	}
	if res.StatusCode() != fasthttp.StatusOK {
		return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", res.StatusCode())
	}
	timestamp, err := time.Parse(time.RFC3339, fastjson.GetString(res.Body(), "commit", "timestamp"))
	if err != nil {
		return &BranchTimestamp{}, err
	}

	stamp := &BranchTimestamp{
		Branch:    branchName,
		Timestamp: timestamp,
	}

	client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout)
	return stamp, nil
}

func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) {
	cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName)

	if branch, ok := client.responseCache.Get(cacheKey); ok && branch != nil {
		return branch.(string), nil
	}

	url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName)
	res, err := client.do(client.infoTimeout, url)
	if err != nil {
		return "", err
	}
	if res.StatusCode() != fasthttp.StatusOK {
		return "", fmt.Errorf("unexpected status code '%d'", res.StatusCode())
	}

	branch := fastjson.GetString(res.Body(), "default_branch")
	client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout)
	return branch, nil
}

func (client *Client) do(timeout time.Duration, url string) (*fasthttp.Response, error) {
	req := fasthttp.AcquireRequest()

	req.SetRequestURI(url)
	req.Header.Set(fasthttp.HeaderAuthorization, "token "+client.giteaAPIToken)
	res := fasthttp.AcquireResponse()

	err := client.fastClient.DoTimeout(req, res, timeout)

	return res, err
}

// TODO: once golang v1.19 is min requirement, we can switch to 'JoinPath()' of 'net/url' package
func joinURL(baseURL string, paths ...string) string {
	p := make([]string, 0, len(paths))
	for i := range paths {
		path := strings.TrimSpace(paths[i])
		path = strings.Trim(path, "/")
		if len(path) != 0 {
			p = append(p, path)
		}
	}

	return baseURL + "/" + strings.Join(p, "/")
}

func getFastHTTPClient() *fasthttp.Client {
	return &fasthttp.Client{
		MaxConnDuration:    60 * time.Second,
		MaxConnWaitTimeout: 1000 * time.Millisecond,
		MaxConnsPerHost:    128 * 16, // TODO: adjust bottlenecks for best performance with Gitea!
	}
}