package main

import (
	"bytes"
	"context"
	"crypto/subtle"
	"fmt"
	"log"
	"net/http"
	"time"

	nanoid "github.com/matoous/go-nanoid"
	"github.com/valyala/fasthttp"
)

var (
	mimes = map[imageType]string{
		imageTypeJPEG: "image/jpeg",
		imageTypePNG:  "image/png",
		imageTypeWEBP: "image/webp",
	}

	authHeaderMust []byte

	healthPath = []byte("/health")

	serverMutex mutex

	errInvalidMethod = newError(422, "Invalid request method", "Method doesn't allowed")
	errInvalidSecret = newError(403, "Invalid secret", "Forbidden")
)

func startServer() *fasthttp.Server {
	serverMutex = newMutex(conf.Concurrency)

	s := &fasthttp.Server{
		Name:        "imgproxy",
		Handler:     serveHTTP,
		Concurrency: conf.MaxClients,
		ReadTimeout: time.Duration(conf.ReadTimeout) * time.Second,
	}

	go func() {
		log.Printf("Starting server at %s\n", conf.Bind)
		if err := s.ListenAndServe(conf.Bind); err != nil {
			log.Fatalln(err)
		}
	}()

	return s
}

func shutdownServer(s *fasthttp.Server) {
	log.Println("Shutting down the server...")
	s.Shutdown()
}

func logResponse(status int, msg string) {
	var color int

	if status >= 500 {
		color = 31
	} else if status >= 400 {
		color = 33
	} else {
		color = 32
	}

	log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg)
}

func writeCORS(rctx *fasthttp.RequestCtx) {
	if len(conf.AllowOrigin) > 0 {
		rctx.Request.Header.Set("Access-Control-Allow-Origin", conf.AllowOrigin)
		rctx.Request.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONs")
	}
}

func respondWithImage(ctx context.Context, reqID string, rctx *fasthttp.RequestCtx, data []byte) {
	rctx.SetStatusCode(200)

	po := getProcessingOptions(ctx)

	rctx.SetContentType(mimes[po.Format])
	rctx.Response.Header.Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
	rctx.Response.Header.Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))

	if conf.GZipCompression > 0 && rctx.Request.Header.HasAcceptEncodingBytes([]byte("gzip")) {
		rctx.Response.Header.Set("Content-Encoding", "gzip")
		gzipData(data, rctx)
	} else {
		rctx.SetBody(data)
	}

	logResponse(200, fmt.Sprintf("[%s] Processed in %s: %s; %+v", reqID, getTimerSince(ctx), getImageURL(ctx), po))
}

func respondWithError(reqID string, rctx *fasthttp.RequestCtx, err imgproxyError) {
	logResponse(err.StatusCode, fmt.Sprintf("[%s] %s", reqID, err.Message))

	rctx.SetStatusCode(err.StatusCode)
	rctx.SetBodyString(err.PublicMessage)
}

func respondWithOptions(reqID string, rctx *fasthttp.RequestCtx) {
	logResponse(200, fmt.Sprintf("[%s] Respond with options", reqID))
	rctx.SetStatusCode(200)
}

func prepareAuthHeaderMust() []byte {
	if len(authHeaderMust) == 0 {
		authHeaderMust = []byte(fmt.Sprintf("Bearer %s", conf.Secret))
	}

	return authHeaderMust
}

func checkSecret(rctx *fasthttp.RequestCtx) bool {
	if len(conf.Secret) == 0 {
		return true
	}

	return subtle.ConstantTimeCompare(
		rctx.Request.Header.Peek("Authorization"),
		prepareAuthHeaderMust(),
	) == 1
}

func serveHTTP(rctx *fasthttp.RequestCtx) {
	reqID, _ := nanoid.Nanoid()

	defer func() {
		if r := recover(); r != nil {
			if err, ok := r.(imgproxyError); ok {
				respondWithError(reqID, rctx, err)
			} else {
				respondWithError(reqID, rctx, newUnexpectedError(r.(error), 4))
			}
		}
	}()

	log.Printf("[%s] %s: %s\n", reqID, rctx.Method(), rctx.Path())

	writeCORS(rctx)

	if rctx.Request.Header.IsOptions() {
		respondWithOptions(reqID, rctx)
		return
	}

	if !rctx.IsGet() {
		panic(errInvalidMethod)
	}

	if !checkSecret(rctx) {
		panic(errInvalidSecret)
	}

	serverMutex.Lock()
	defer serverMutex.Unock()

	if bytes.Equal(rctx.Path(), healthPath) {
		rctx.SetStatusCode(200)
		rctx.SetBodyString("imgproxy is running")
		return
	}

	ctx, timeoutCancel := startTimer(time.Duration(conf.WriteTimeout) * time.Second)
	defer timeoutCancel()

	ctx, err := parsePath(ctx, rctx)
	if err != nil {
		panic(newError(404, err.Error(), "Invalid image url"))
	}

	ctx, downloadcancel, err := downloadImage(ctx)
	defer downloadcancel()
	if err != nil {
		panic(newError(404, err.Error(), "Image is unreachable"))
	}

	checkTimeout(ctx)

	if conf.ETagEnabled {
		eTag := calcETag(ctx)
		rctx.Response.Header.SetBytesV("ETag", eTag)

		if bytes.Equal(eTag, rctx.Request.Header.Peek("If-None-Match")) {
			panic(errNotModified)
		}
	}

	checkTimeout(ctx)

	imageData, err := processImage(ctx)
	if err != nil {
		panic(newError(500, err.Error(), "Error occurred while processing image"))
	}

	checkTimeout(ctx)

	respondWithImage(ctx, reqID, rctx, imageData)
}
