package gitea

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"code.gitea.io/sdk/gitea"
	"github.com/rs/zerolog/log"

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

var ErrorNotFound = errors.New("not found")

const (
	branchTimestampCacheKeyPrefix = "branchTime"
	defaultBranchCacheKeyPrefix   = "defaultBranch"
	giteaObjectTypeHeader         = "X-Gitea-Object-Type"
)

type Client struct {
	sdkClient     *gitea.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)
	if err != nil {
		return nil, err
	}
	giteaRoot = strings.Trim(rootURL.String(), "/")

	stdClient := http.Client{Timeout: 10 * time.Second}

	sdk, err := gitea.NewClient(giteaRoot, gitea.SetHTTPClient(&stdClient), gitea.SetToken(giteaAPIToken))
	return &Client{
		sdkClient:      sdk,
		responseCache:  respCache,
		followSymlinks: followSymlinks,
		supportLFS:     supportLFS,
	}, err
}

func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) {
	reader, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	return io.ReadAll(reader)
}

func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, *http.Response, error) {
	// if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() {
	// 	cachedResponse = cachedValue.(gitea.FileResponse)
	reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
	if resp != nil {
		switch resp.StatusCode {
		case http.StatusOK:

			// add caching

			// Write the response body to the original request
			// var cacheBodyWriter bytes.Buffer
			// if res != nil {
			// 	if res.Header.ContentLength() > fileCacheSizeLimit {
			// 		// fasthttp else will set "Content-Length: 0"
			// 		ctx.Response().SetBodyStream(&strings.Reader{}, -1)
			//
			// 		err = res.BodyWriteTo(ctx.Response.BodyWriter())
			// 	} else {
			// 		// TODO: cache is half-empty if request is cancelled - does the ctx.Err() below do the trick?
			// 		err = res.BodyWriteTo(io.MultiWriter(ctx.Response().BodyWriter(), &cacheBodyWriter))
			// 	}
			// } else {
			// 	_, err = ctx.Write(cachedResponse.Body)
			// }

			// if res != nil && res.Header.ContentLength() <= fileCacheSizeLimit && ctx.Err() == nil {
			// 	cachedResponse.Exists = true
			// 	cachedResponse.MimeType = mimeType
			// 	cachedResponse.Body = cacheBodyWriter.Bytes()
			// 	_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), cachedResponse, fileCacheTimeout)
			// }
			// store ETag in resp !!!!

			objType := resp.Header.Get(giteaObjectTypeHeader)
			log.Trace().Msgf("server raw content object: %s", objType)
			if client.followSymlinks && objType == "symlink" {
				// limit to 1000 chars
				defer reader.Close()
				linkDestBytes, err := io.ReadAll(io.LimitReader(reader, 10000))
				if err != nil {
					return nil, nil, err
				}
				linkDest := strings.TrimSpace(string(linkDestBytes))

				log.Debug().Msgf("follow symlink from '%s' to '%s'", resource, linkDest)
				return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
			}

			return reader, resp.Response, err
		case http.StatusNotFound:

			// add not exist caching
			// _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
			// 	Exists: false,
			// }, fileCacheTimeout)

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

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
	}

	branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName)
	if err != nil {
		if resp != nil && resp.StatusCode == http.StatusNotFound {
			return &BranchTimestamp{}, ErrorNotFound
		}
		return &BranchTimestamp{}, err
	}
	if resp.StatusCode != http.StatusOK {
		return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
	}

	stamp := &BranchTimestamp{
		Branch:    branch.Name,
		Timestamp: branch.Commit.Timestamp,
	}

	if err := client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout); err != nil {
		log.Error().Err(err).Msgf("error on store of repo branch timestamp [%s/%s@%s]", repoOwner, repoName, branchName)
	}
	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
	}

	repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName)
	if err != nil {
		return "", err
	}
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
	}

	branch := repo.DefaultBranch
	if err := client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout); err != nil {
		log.Error().Err(err).Msgf("error on store of repo default branch [%s/%s]", repoOwner, repoName)
	}
	return branch, nil
}