package static

import (
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/artifacthub/hub/internal/handlers/helpers"
	"github.com/artifacthub/hub/internal/hub"
	"github.com/artifacthub/hub/internal/img"
	"github.com/go-chi/chi/v5"
	svg "github.com/h2non/go-is-svg"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
)

const (
	cspPolicy = `
	default-src 'none';
	connect-src 'self' https://play.openpolicyagent.org https://www.google-analytics.com https://kubernetesjsonschema.dev https://raw.githubusercontent.com/yannh/kubernetes-json-schema/ https://cncf.github.io/banners/banners.yml;
	font-src 'self';
	img-src 'self' data: https:;
	manifest-src 'self';
	script-src 'self' https://www.google-analytics.com;
	style-src 'self' 'unsafe-inline';
	frame-ancestors 'none';
	`

	indexCacheMaxAge = 5 * time.Minute

	// DocsCacheMaxAge is the cache max age used when serving the docs.
	DocsCacheMaxAge = 15 * time.Minute

	// StaticCacheMaxAge is the cache max age used when serving static assets.
	StaticCacheMaxAge = 365 * 24 * time.Hour
)

// Handlers represents a group of http handlers in charge of handling
// static files operations.
type Handlers struct {
	cfg        *viper.Viper
	imageStore img.Store
	logger     zerolog.Logger
	indexTmpl  *template.Template

	mu          sync.RWMutex
	imagesCache map[string][]byte
}

// NewHandlers creates a new Handlers instance.
func NewHandlers(cfg *viper.Viper, imageStore img.Store) *Handlers {
	h := &Handlers{
		cfg:         cfg,
		imageStore:  imageStore,
		imagesCache: make(map[string][]byte),
		logger:      log.With().Str("handlers", "static").Logger(),
	}
	h.setupIndexTemplate()
	return h
}

// setupIndexTemplate parses the index.html template for later use.
func (h *Handlers) setupIndexTemplate() {
	path := path.Join(h.cfg.GetString("server.webBuildPath"), "index.html")
	text, err := os.ReadFile(path)
	if err != nil {
		log.Panic().Err(err).Msg("error reading index.html template")
	}
	h.indexTmpl = template.Must(template.New("").Parse(string(text)))
}

// Image is an http handler that serves images stored in the database.
func (h *Handlers) Image(w http.ResponseWriter, r *http.Request) {
	// Extract image id and version
	image := chi.URLParam(r, "image")
	parts := strings.Split(image, "@")
	var imageID, version string
	if len(parts) == 2 {
		imageID = parts[0]
		version = parts[1]
	} else {
		imageID = image
	}

	// Check if image version data is cached
	h.mu.RLock()
	data, ok := h.imagesCache[image]
	h.mu.RUnlock()
	if !ok {
		// Get image data from database
		var err error
		data, err = h.imageStore.GetImage(r.Context(), imageID, version)
		if err != nil {
			if errors.Is(err, hub.ErrNotFound) {
				w.WriteHeader(http.StatusNotFound)
			} else {
				h.logger.Error().Err(err).Str("method", "Image").Str("imageID", imageID).Send()
				w.WriteHeader(http.StatusInternalServerError)
			}
			return
		}

		// Save image data in cache
		h.mu.Lock()
		h.imagesCache[image] = data
		h.mu.Unlock()
	}

	// Set headers and write image data to response writer
	w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(StaticCacheMaxAge))
	w.Header().Set("Content-Length", strconv.Itoa(len(data)))
	if svg.Is(data) {
		w.Header().Set("Content-Type", "image/svg+xml")
	} else {
		w.Header().Set("Content-Type", http.DetectContentType(data))
	}
	_, _ = w.Write(data)
}

// Index is an http handler that serves the index.html file.
func (h *Handlers) Index(w http.ResponseWriter, r *http.Request) {
	// Set headers
	w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(indexCacheMaxAge))
	w.Header().Set("Content-Security-Policy", cspPolicy)

	// Execute index template
	title, _ := r.Context().Value(hub.IndexMetaTitleKey).(string)
	if title == "" {
		title = h.cfg.GetString("theme.siteName")
	}
	description, _ := r.Context().Value(hub.IndexMetaDescriptionKey).(string)
	if description == "" {
		description = "Find, install and publish Cloud Native packages"
	}
	openGraphImage := h.cfg.GetString("theme.images.openGraphImage")
	if !strings.HasPrefix(openGraphImage, "http") {
		openGraphImage = h.cfg.GetString("server.baseURL") + openGraphImage
	}
	data := map[string]interface{}{
		"allowPrivateRepositories": h.cfg.GetBool("server.allowPrivateRepositories"),
		"allowUserSignUp":          h.cfg.GetBool("server.allowUserSignUp"),
		"appleTouchIcon192":        h.cfg.GetString("theme.images.appleTouchIcon192"),
		"appleTouchIcon512":        h.cfg.GetString("theme.images.appleTouchIcon512"),
		"bannersURL":               h.cfg.GetString("server.bannersURL"),
		"description":              description,
		"gaTrackingID":             h.cfg.GetString("analytics.gaTrackingID"),
		"githubAuth":               h.cfg.IsSet("server.oauth.github"),
		"googleAuth":               h.cfg.IsSet("server.oauth.google"),
		"motd":                     h.cfg.GetString("server.motd"),
		"motdSeverity":             h.cfg.GetString("server.motdSeverity"),
		"oidcAuth":                 h.cfg.IsSet("server.oauth.oidc"),
		"openGraphImage":           openGraphImage,
		"primaryColor":             h.cfg.GetString("theme.colors.primary"),
		"reportURL":                h.cfg.GetString("theme.reportURL"),
		"secondaryColor":           h.cfg.GetString("theme.colors.secondary"),
		"shortcutIcon":             h.cfg.GetString("theme.images.shortcutIcon"),
		"siteName":                 h.cfg.GetString("theme.siteName"),
		"title":                    title,
		"websiteLogo":              h.cfg.GetString("theme.images.websiteLogo"),
	}
	sampleQueriesJSON, err := getSampleQueriesJSON(h.cfg)
	if err != nil {
		h.logger.Error().Err(err).Msg("error reading provided sample queries to JSON, they won't be used")
	} else {
		data["sampleQueries"] = sampleQueriesJSON
	}
	if err := h.indexTmpl.Execute(w, data); err != nil {
		h.logger.Error().Err(err).Msg("error executing index template")
		http.Error(w, "", http.StatusInternalServerError)
	}
}

// SaveImage is an http handler that stores the provided image returning its id.
func (h *Handlers) SaveImage(w http.ResponseWriter, r *http.Request) {
	data, err := io.ReadAll(r.Body)
	if err != nil {
		h.logger.Error().Err(err).Str("method", "SaveImage").Msg("error reading body data")
		helpers.RenderErrorJSON(w, err)
		return
	}
	imageID, err := h.imageStore.SaveImage(r.Context(), data)
	if err != nil {
		h.logger.Error().Err(err).Str("method", "SaveImage").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	dataJSON := []byte(fmt.Sprintf(`{"image_id": "%s"}`, imageID))
	helpers.RenderJSON(w, dataJSON, 0, http.StatusOK)
}

// FileServer sets up a http.FileServer handler to serve static files.
func FileServer(r chi.Router, public, static string, cacheMaxAge time.Duration) {
	if strings.ContainsAny(public, "{}*") {
		panic("FileServer does not permit URL parameters")
	}

	if public != "/" && public[len(public)-1] != '/' {
		r.Get(public, http.RedirectHandler(public+"/", http.StatusMovedPermanently).ServeHTTP)
		public += "/"
	}

	fsHandler := http.StripPrefix(public, http.FileServer(http.Dir(static)))

	r.Get(public+"*", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		file := strings.Replace(r.RequestURI, public, "/", 1)
		if _, err := os.Stat(static + file); os.IsNotExist(err) {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.Header().Set("Cache-Control", helpers.BuildCacheControlHeader(cacheMaxAge))
		fsHandler.ServeHTTP(w, r)
	}))
}

// getSampleQueriesJSON is a helper function that reads the sample queries from
// the configuration provided and marshals them as json.
func getSampleQueriesJSON(cfg *viper.Viper) (string, error) {
	var sampleQueries []map[string]string
	if err := cfg.UnmarshalKey("theme.sampleQueries", &sampleQueries); err != nil {
		return "", err
	}
	sampleQueriesJSON, err := json.Marshal(sampleQueries)
	if err != nil {
		return "", err
	}
	return string(sampleQueriesJSON), nil
}
