package gitea

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"

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

const (
	// defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long.
	defaultBranchCacheTimeout = 15 * time.Minute

	// branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter
	// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be
	// picked up faster, while still allowing the content to be cached longer if nothing changes.
	branchExistenceCacheTimeout = 5 * time.Minute

	// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
	// on your available memory.
	// TODO: move as option into cache interface
	fileCacheTimeout = 5 * time.Minute

	// ownerExistenceCacheTimeout specifies the timeout for the existence of a repo/org
	ownerExistenceCacheTimeout = 5 * time.Minute

	// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
	fileCacheSizeLimit = int64(1000 * 1000)
)

type FileResponse struct {
	Exists    bool   `json:"exists"`
	IsSymlink bool   `json:"isSymlink"`
	ETag      string `json:"eTag"`
	MimeType  string `json:"mimeType"` // uncompressed MIME type
	RawMime   string `json:"rawMime"`  // raw MIME type (if compressed, type of compression)
	Body      []byte `json:"-"`        // saved separately
}

func (f FileResponse) IsEmpty() bool {
	return len(f.Body) == 0
}

func (f FileResponse) createHttpResponse(cacheKey string, decompress bool) (header http.Header, statusCode int) {
	header = make(http.Header)

	if f.Exists {
		statusCode = http.StatusOK
	} else {
		statusCode = http.StatusNotFound
	}

	if f.IsSymlink {
		header.Set(giteaObjectTypeHeader, objTypeSymlink)
	}
	header.Set(ETagHeader, f.ETag)

	if decompress {
		header.Set(ContentTypeHeader, f.MimeType)
	} else {
		header.Set(ContentTypeHeader, f.RawMime)
	}

	header.Set(ContentLengthHeader, fmt.Sprintf("%d", len(f.Body)))
	header.Set(PagesCacheIndicatorHeader, "true")

	log.Trace().Msgf("fileCache for %q used", cacheKey)
	return header, statusCode
}

type BranchTimestamp struct {
	NotFound  bool      `json:"notFound"`
	Branch    string    `json:"branch,omitempty"`
	Timestamp time.Time `json:"timestamp,omitempty"`
}

type writeCacheReader struct {
	originalReader io.ReadCloser
	buffer         *bytes.Buffer
	fileResponse   *FileResponse
	cacheKey       string
	cache          cache.ICache
	hasError       bool
	doNotCache     bool
	complete       bool
	log            zerolog.Logger
}

func (t *writeCacheReader) Read(p []byte) (n int, err error) {
	t.log.Trace().Msgf("[cache] read %q", t.cacheKey)
	n, err = t.originalReader.Read(p)
	if err == io.EOF {
		t.complete = true
	}
	if err != nil && err != io.EOF {
		t.log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
		t.hasError = true
	} else if n > 0 {
		if t.buffer.Len()+n > int(fileCacheSizeLimit) {
			t.doNotCache = true
			t.buffer.Reset()
		} else {
			_, _ = t.buffer.Write(p[:n])
		}
	}
	return
}

func (t *writeCacheReader) Close() error {
	doWrite := !t.hasError && !t.doNotCache && t.complete
	fc := *t.fileResponse
	fc.Body = t.buffer.Bytes()
	if doWrite {
		jsonToCache, err := json.Marshal(fc)
		if err != nil {
			t.log.Trace().Err(err).Msgf("[cache] marshaling json for %q has returned an error", t.cacheKey+"|Metadata")
		}
		err = t.cache.Set(t.cacheKey+"|Metadata", jsonToCache, fileCacheTimeout)
		if err != nil {
			t.log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey+"|Metadata")
		}
		err = t.cache.Set(t.cacheKey+"|Body", fc.Body, fileCacheTimeout)
		if err != nil {
			t.log.Trace().Err(err).Msgf("[cache] writer for %q has returned an error", t.cacheKey+"|Body")
		}
	}
	t.log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, doWrite)
	return t.originalReader.Close()
}

func (f FileResponse) CreateCacheReader(ctx *context.Context, r io.ReadCloser, cache cache.ICache, cacheKey string) io.ReadCloser {
	log := log.With().Str("ReqId", ctx.ReqId).Logger()
	if r == nil || cache == nil || cacheKey == "" {
		log.Error().Msg("could not create CacheReader")
		return nil
	}

	return &writeCacheReader{
		originalReader: r,
		buffer:         bytes.NewBuffer(make([]byte, 0)),
		fileResponse:   &f,
		cache:          cache,
		cacheKey:       cacheKey,
		log:            log,
	}
}