package main

import (
	"bytes"
	"database/sql"
	"embed"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"
)

//go:embed web
var webFiles embed.FS

type application struct {
	store    *playgroundStore
	packages map[string]memoryAsset
	monaco   http.Handler
	assets   http.Handler
	pages    map[string][]byte
}

func newApplication(store *playgroundStore, packages map[string]memoryAsset, monacoDirectory string) (*application, error) {
	if info, err := os.Stat(monacoDirectory); err != nil || !info.IsDir() {
		return nil, fmt.Errorf("Monaco assets not found at %s; run npm install", monacoDirectory)
	}
	assets, err := fs.Sub(webFiles, "web/assets")
	if err != nil {
		return nil, err
	}
	pages := make(map[string][]byte)
	for _, name := range []string{"home.html", "docs.html", "playground.html"} {
		content, err := webFiles.ReadFile("web/" + name)
		if err != nil {
			return nil, err
		}
		pages[name] = content
	}
	return &application{
		store:    store,
		packages: packages,
		monaco:   http.StripPrefix("/assets/monaco/", http.FileServer(http.Dir(monacoDirectory))),
		assets:   http.StripPrefix("/assets/", http.FileServer(http.FS(assets))),
		pages:    pages,
	}, nil
}

func (app *application) routes() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/pkg/", app.servePackage)
	mux.HandleFunc("/api/playgrounds", app.savePlayground)
	mux.HandleFunc("/api/playgrounds/", app.getPlayground)
	mux.Handle("/assets/monaco/", app.monaco)
	mux.Handle("/assets/", app.assets)
	mux.HandleFunc("/docs", app.servePage("docs.html"))
	mux.HandleFunc("/docs/", app.servePage("docs.html"))
	mux.HandleFunc("/playground", app.servePlayground)
	mux.HandleFunc("/playground/", app.servePlayground)
	mux.HandleFunc("/", app.serveHome)
	return app.headers(mux)
}

func (app *application) headers(next http.Handler) http.Handler {
	return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		response.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
		response.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
		response.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
		response.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
		response.Header().Set("X-Content-Type-Options", "nosniff")
		response.Header().Set("X-Frame-Options", "SAMEORIGIN")
		next.ServeHTTP(response, request)
	})
}

func (app *application) serveHome(response http.ResponseWriter, request *http.Request) {
	if request.URL.Path != "/" {
		http.NotFound(response, request)
		return
	}
	app.writePage(response, request, "home.html")
}

func (app *application) servePage(name string) http.HandlerFunc {
	return func(response http.ResponseWriter, request *http.Request) {
		app.writePage(response, request, name)
	}
}

func (app *application) servePlayground(response http.ResponseWriter, request *http.Request) {
	if request.URL.Path != "/playground" && request.URL.Path != "/playground/" {
		parts := strings.Split(strings.Trim(request.URL.Path, "/"), "/")
		if len(parts) != 3 || parts[0] != "playground" || !playgroundIDPattern.MatchString(parts[1]) {
			http.NotFound(response, request)
			return
		}
		if revision, err := strconv.Atoi(parts[2]); err != nil || revision < 1 {
			http.NotFound(response, request)
			return
		}
	}
	app.writePage(response, request, "playground.html")
}

func (app *application) writePage(response http.ResponseWriter, request *http.Request, name string) {
	response.Header().Set("Content-Type", "text/html; charset=utf-8")
	response.Header().Set("Cache-Control", "no-cache")
	if request.Method == http.MethodHead {
		return
	}
	if request.Method != http.MethodGet {
		response.Header().Set("Allow", "GET, HEAD")
		http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	_, _ = response.Write(app.pages[name])
}

func (app *application) servePackage(response http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodGet && request.Method != http.MethodHead {
		response.Header().Set("Allow", "GET, HEAD")
		http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	name := strings.TrimPrefix(path.Clean(request.URL.Path), "/pkg/")
	asset, exists := app.packages[name]
	if !exists || name == "." {
		http.NotFound(response, request)
		return
	}
	response.Header().Set("Access-Control-Allow-Origin", "*")
	response.Header().Set("Cache-Control", "no-cache")
	response.Header().Set("Content-Type", asset.contentType)
	response.Header().Set("Cross-Origin-Resource-Policy", "cross-origin")
	response.Header().Set("ETag", asset.etag)
	if request.Header.Get("If-None-Match") == asset.etag {
		response.WriteHeader(http.StatusNotModified)
		return
	}
	http.ServeContent(response, request, name, asset.modified, bytes.NewReader(asset.content))
}

func (app *application) savePlayground(response http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodPost {
		response.Header().Set("Allow", "POST")
		http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	request.Body = http.MaxBytesReader(response, request.Body, maximumCodeBytes+4096)
	var input struct {
		ID    string `json:"id"`
		Title string `json:"title"`
		Code  string `json:"code"`
	}
	decoder := json.NewDecoder(request.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&input); err != nil {
		writeJSONError(response, "The save request is not valid.", http.StatusBadRequest)
		return
	}
	result, err := app.store.save(request.Context(), input.ID, strings.TrimSpace(input.Title), input.Code)
	if err != nil {
		writeJSONError(response, err.Error(), http.StatusBadRequest)
		return
	}
	writeJSON(response, result, http.StatusCreated)
}

func (app *application) getPlayground(response http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodGet {
		response.Header().Set("Allow", "GET")
		http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	parts := strings.Split(strings.TrimPrefix(request.URL.Path, "/api/playgrounds/"), "/")
	if len(parts) != 2 {
		http.NotFound(response, request)
		return
	}
	revision, err := strconv.Atoi(parts[1])
	if err != nil {
		http.NotFound(response, request)
		return
	}
	result, err := app.store.get(request.Context(), parts[0], revision)
	if errors.Is(err, sql.ErrNoRows) {
		writeJSONError(response, "Playground not found.", http.StatusNotFound)
		return
	}
	if err != nil {
		writeJSONError(response, "Could not load the playground.", http.StatusInternalServerError)
		return
	}
	response.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	writeJSON(response, result, http.StatusOK)
}

func writeJSON(response http.ResponseWriter, value any, status int) {
	response.Header().Set("Content-Type", "application/json; charset=utf-8")
	response.WriteHeader(status)
	_ = json.NewEncoder(response).Encode(value)
}

func writeJSONError(response http.ResponseWriter, message string, status int) {
	writeJSON(response, map[string]string{"error": message}, status)
}
