package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	minio "github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

type Config struct {
	S3   S3Config `json:"s3"`
	Bind string   `json:"bind"`
}

type S3Config struct {
	Endpoint        string `json:"endpoint"`
	AccessKeyID     string `json:"access_key_id"`
	SecretAccessKey string `json:"secret_access_key"`
	Bucket          string `json:"bucket"`
}

var (
	config = Config{
		Bind: ":8080",
	}

	minioClient *minio.Client
)

func loadconfig(filename string) {
	f, err := os.Open(filename)
	if err != nil {
		log.Fatal("error reading", filename, ":", err)
	}
	defer f.Close()

	if err := json.NewDecoder(f).Decode(&config); err != nil {
		log.Fatal("error parsing", filename, ":", err)
	}
}

func main() {
	loadconfig("caching-proxy.json")
	initializeMinioClient()

	log.Println("starting listener on", config.Bind)
	if err := http.ListenAndServe(config.Bind, proxyhandler{}); err != nil {
		log.Fatal("error starting http listener:", err)
	}
}

func initializeMinioClient() {
	var err error
	minioClient, err = minio.New(config.S3.Endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(config.S3.AccessKeyID, config.S3.SecretAccessKey, ""),
		Secure: true,
	})
	if err != nil {
		log.Fatalln("error creating minio client:", err)
	}
}

type proxyhandler struct{}

func (proxyhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	requestPath := strings.SplitN(r.URL.Path, "/", 3)
	if len(requestPath) != 3 || requestPath[2] == "" {
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("not found"))
		log.Println("invalid request, 404", r.URL.Path)
		return
	}

	get(requestPath[1]+"/"+requestPath[2], w)
}

func get(path string, w http.ResponseWriter) {
	source, err := minioClient.GetObject(context.Background(), config.S3.Bucket, path, minio.GetObjectOptions{})
	if err != nil {
		log.Println("error getting cached object for", path, ":", err)
		w.WriteHeader(500)
		return
	}

	stat, err := source.Stat()
	if err != nil {
		resp := minio.ToErrorResponse(err)
		if resp.StatusCode == http.StatusNotFound {
			download(path, w)
			return
		}

		log.Println("error getting cached file", path, ":", resp.Code)
		w.WriteHeader(500)
		return
	}

	w.Header().Add("Content-Length", fmt.Sprint(stat.Size))
	w.Header().Add("Content-Type", stat.ContentType)

	log.Println("cache hit: ", path)

	if _, err := io.Copy(w, source); err != nil {
		log.Println("error serving cached file:", err)
		return
	}
}

func download(path string, w http.ResponseWriter) {
	resp, err := http.Get("https://" + path)
	if err != nil {
		log.Println("error getting", path, ":", err)
		return
	}
	defer resp.Body.Close()

	w.Header().Add("Content-Length", fmt.Sprint(resp.ContentLength))
	w.WriteHeader(resp.StatusCode)

	if resp.StatusCode > 299 {
		log.Println("upstream non-ok:", resp.Status, ":", path)
		io.Copy(w, resp.Body)
		return
	}

	reader := io.TeeReader(resp.Body, infallableWriter{Next: w})

	_, err = minioClient.PutObject(resp.Request.Context(), config.S3.Bucket, path, reader, resp.ContentLength, minio.PutObjectOptions{})
	if err != nil {
		log.Println("error PUTting", path, ":", err)
	}

	log.Println("cache miss:", path)
}

type infallableWriter struct {
	Next io.Writer
}

func (i infallableWriter) Write(data []byte) (int, error) {
	n, err := i.Next.Write(data)
	if err != nil {
		return len(data), nil
	}

	return n, nil
}