package main

import (
	"context"
	"flag"
	"fmt"
	"io/fs"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"time"

	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/api"
	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
	"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
	"github.com/joho/godotenv"
	"golang.org/x/crypto/bcrypt"

	_ "modernc.org/sqlite"
)

func init() {
	_, jsonLogger := os.LookupEnv("JSON_LOGGER")
	_, debug := os.LookupEnv("DEBUG")

	var programLevel slog.Level
	if debug {
		programLevel = slog.LevelDebug
	}
	programLevel = slog.LevelDebug

	if jsonLogger {
		jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
			Level: programLevel,
		})
		slog.SetDefault(slog.New(jsonHandler))
	} else {
		textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
			Level: programLevel,
		})
		slog.SetDefault(slog.New(textHandler))
	}

	slog.Info("Logger initialized", slog.Bool("debug", debug))
}

func main() {
	var flagMigrateDB, flagRevertDB, flagForceDB bool
	var flagNewUser, flagDelUser string

	flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit")
	flag.BoolVar(&flagRevertDB, "revert-db", false, "Migrate database to previous version and exit")
	flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit")
	flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: <username>:<password>")
	flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: <username>")
	flag.Parse()

	err := godotenv.Load()
	if err != nil {
		slog.Error("Could not parse existing .env file at location './.env'. Application startup failed, exited.\nError: %s\n", "Error", err.Error())
	}

	dbURL := os.Getenv("DB")
	if dbURL == "" {
		dbURL = "app.db"
	}

	if flagMigrateDB {
		err := repository.MigrateDB(dbURL)
		if err != nil {
			slog.Error("MigrateDB Failed: Could not migrate database at location.", "version", repository.Version, "error", err)
			os.Exit(1)
		}
		slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version)
		os.Exit(0)
	}

	if flagRevertDB {
		err := repository.RevertDB(dbURL)
		if err != nil {
			slog.Error("RevertDB Failed: Could not revert database at location", "version", (repository.Version - 1), "error", err)
			os.Exit(1)
		}
		slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1))
		os.Exit(0)
	}

	if flagForceDB {
		err := repository.ForceDB(dbURL)
		if err != nil {
			slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err)
			os.Exit(1)
		}
		slog.Info("ForceDB Success: Forced database version", "version", repository.Version)
		os.Exit(0)
	}

	repository.Connect(dbURL)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	addr := ":" + port

	auth.Init()

	if flagNewUser != "" {
		parts := strings.SplitN(flagNewUser, ":", 2)
		if len(parts) != 2 || len(parts[0]) == 0 {
			slog.Error("Add User: Could not parse supplied argument format: No changes.\n"+
				"Want: <username>::<password>\n", "have", flagNewUser)
		}

		ctx := context.Background()
		q := repository.GetRepository()
		hash, err := bcrypt.GenerateFromPassword([]byte(parts[1]), bcrypt.DefaultCost)
		password := string(hash)
		if err != nil {
			slog.Error("Error while encrypting new user password")
			os.Exit(1)
		}
		if err := q.CreateUser(ctx, repository.CreateUserParams{
			UserName: parts[0], UserPass: &password,
		}); err != nil {
			slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
			os.Exit(1)
		} else {
			slog.Info("add new user", "username", parts[0])
		}
		os.Exit(0)
	}

	if flagDelUser != "" {
		ctx := context.Background()
		q := repository.GetRepository()
		if err := q.DeleteUser(ctx, flagDelUser); err != nil {
			slog.Error("Delete User: Could not delete user", "username", flagDelUser, "error", err)
			os.Exit(1)
		} else {
			slog.Info("deleted user from DB", "username", flagDelUser)
		}
		os.Exit(0)
	}

	mux := http.NewServeMux()

	sfs, err := fs.Sub(web.StaticAssets, "frontend/dist")
	if err != nil {
		slog.Error("Failed to create sub filesystem", "error", err)
		return
	}
	mux.Handle("GET /static/", http.StripPrefix("/static", http.FileServer(http.FS(sfs))))

	afs, err := fs.Sub(web.StaticAssets, "frontend/dist/assets")
	if err != nil {
		slog.Error("Failed to create sub filesystem", "error", err)
		return
	}
	mux.Handle("GET /assets/", http.StripPrefix("/assets", http.FileServer(http.FS(afs))))

	mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {
		data, err := web.StaticAssets.ReadFile("frontend/dist/favicon.ico")
		if err != nil {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "text/plain")
		w.Write(data)
	})
	mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) {
		data, err := web.StaticAssets.ReadFile("frontend/dist/robots.txt")
		if err != nil {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "text/plain")
		w.Write(data)
	})

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		w.Write([]byte(`OK`))
	})

	mux.HandleFunc("GET /login", func(rw http.ResponseWriter, r *http.Request) {
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
		web.RenderTemplate(rw, "login", web.PageData{Title: "Login"})
	})
	mux.HandleFunc("GET /imprint", func(rw http.ResponseWriter, r *http.Request) {
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
		web.RenderTemplate(rw, "imprint", web.PageData{Title: "Imprint"})
	})
	mux.HandleFunc("GET /privacy", func(rw http.ResponseWriter, r *http.Request) {
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
		web.RenderTemplate(rw, "privacy", web.PageData{Title: "Privacy"})
	})

	authHandle := auth.GetAuthInstance()
	mux.Handle("POST /login", authHandle.Login(
		func(rw http.ResponseWriter, r *http.Request, err error) {
			rw.Header().Add("Content-Type", "text/html; charset=utf-8")
			rw.WriteHeader(http.StatusUnauthorized)
			web.RenderTemplate(rw, "login", web.PageData{
				Title:   "Login failed - ClusterCockpit",
				MsgType: "alert-warning",
				Message: err.Error(),
			})
		}))

	mux.Handle("POST /logout", authHandle.Logout(
		http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			rw.Header().Add("Content-Type", "text/html; charset=utf-8")
			rw.WriteHeader(http.StatusOK)
			web.RenderTemplate(rw, "login", web.PageData{
				Title:   "Bye - ClusterCockpit",
				MsgType: "alert-info",
				Message: "Logout successful",
			})
		})))

	api.MountApiEndpoints(mux)
	mux.HandleFunc("GET /", handlers.RootHandler())

	securedMux := http.NewServeMux()
	securedMux.HandleFunc("GET /", handlers.AdminHandler())

	securedChain := &middleware.Chain{}
	securedChain.Use(middleware.SecuredCheck)

	mux.Handle("GET /admin/", http.StripPrefix("/admin", securedChain.Then(securedMux)))

	chain := &middleware.Chain{}
	chain.Use(middleware.Recover)
	wrappedMux := chain.Then(mux)

	server := &http.Server{
		Addr:    fmt.Sprintf(":%s", port),
		Handler: wrappedMux,
		// Recommended timeouts from
		// https://blog.cloudflare.com/exposing-go-on-the-internet/
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}

	slog.Info("Server listening", "addr", addr)

	if err := server.ListenAndServe(); err != nil {
		slog.Error("Server failed to start", "error", err)
	}
}
