package main

import (
	_ "embed"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/google/shlex"
)

const webEditorIdleTimeout = 30 * time.Minute

//go:embed templates/webeditor.html
var webEditorTemplateSource string

var webEditorTemplate = template.Must(template.New("webeditor.html").Funcs(template.FuncMap{
	"joinLines": func(vals []string) string { return strings.Join(vals, "\n") },
}).Parse(webEditorTemplateSource))

var webEditor struct {
	mu   sync.Mutex
	addr string
	idle *time.Timer
}

func ensureWebEditor() (string, error) {
	webEditor.mu.Lock()
	defer webEditor.mu.Unlock()
	if webEditor.addr != "" {
		webEditor.idle.Reset(webEditorIdleTimeout)
		return webEditor.addr, nil
	}

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return "", err
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", webEditorIndex)
	mux.HandleFunc("/save/gui", webEditorSaveGUI)
	mux.HandleFunc("/save/profile", webEditorSaveProfile)
	mux.HandleFunc("/profile/add", webEditorAddProfile)
	mux.HandleFunc("/profile/delete", webEditorDeleteProfile)
	mux.HandleFunc("/profile/env", webEditorSaveEnv)

	touch := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			webEditor.mu.Lock()
			webEditor.idle.Reset(webEditorIdleTimeout)
			webEditor.mu.Unlock()
			next.ServeHTTP(w, r)
		})
	}

	webEditor.idle = time.AfterFunc(webEditorIdleTimeout, func() {
		webEditor.mu.Lock()
		defer webEditor.mu.Unlock()
		ln.Close()
		webEditor.addr = ""
	})

	go func() {
		if err := http.Serve(ln, touch(mux)); err != nil && !errors.Is(err, net.ErrClosed) {
			log.Printf("webeditor: %v", err)
		}
	}()

	webEditor.addr = "http://" + ln.Addr().String() + "/"
	return webEditor.addr, nil
}

func splitLines(s string) []string {
	var out []string
	for _, line := range strings.Split(s, "\n") {
		line = strings.TrimSpace(line)
		if line != "" {
			out = append(out, line)
		}
	}
	return out
}

func splitArgs(s string) []string {
	var out []string
	for _, line := range splitLines(s) {
		words, err := shlex.Split(line)
		if err != nil {
			out = append(out, line)
			continue
		}
		out = append(out, words...)
	}
	return out
}

type webEditorProfileView struct {
	Profile
	DisplayName string
	EnvContents string
}

func webEditorIndex(w http.ResponseWriter, r *http.Request) {
	cfg := loadConfig()
	views := make([]webEditorProfileView, len(cfg.Profiles))
	for i, p := range cfg.Profiles {
		contents, err := os.ReadFile(p.EnvFile)
		if err != nil {
			contents = []byte(defaultEnvTemplate)
		}
		views[i] = webEditorProfileView{Profile: p, DisplayName: p.displayName(), EnvContents: string(contents)}
	}
	activeTab := r.URL.Query().Get("tab")
	if activeTab == "" {
		activeTab = "gui"
	}
	data := struct {
		GUI                    GUI
		Profiles               []webEditorProfileView
		Writable               bool
		ActiveTab              string
		NotificationOptions    []string
		IconOptions            []string
		ScheduleDisplayOptions []string
	}{
		GUI:                    cfg.GUI,
		Profiles:               views,
		Writable:               configWritable(),
		ActiveTab:              activeTab,
		NotificationOptions:    []string{"none", "errors", "all"},
		IconOptions:            []string{"", "color", "mono", "white", "black"},
		ScheduleDisplayOptions: []string{"description", "cron", "last", "none"},
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := webEditorTemplate.Execute(w, data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func webEditorRequireWritable(w http.ResponseWriter, r *http.Request) bool {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return false
	}
	if !configWritable() {
		http.Error(w, "config file is read-only", http.StatusForbidden)
		return false
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return false
	}
	return true
}

func webEditorParseIndex(r *http.Request, cfg Config) (int, bool) {
	idx, err := strconv.Atoi(r.FormValue("index"))
	if err != nil || idx < 0 || idx >= len(cfg.Profiles) {
		return 0, false
	}
	return idx, true
}

func webEditorSaveAndRedirect(w http.ResponseWriter, r *http.Request, cfg Config, tab string) {
	if err := saveConfig(cfg); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	dest := "/"
	if tab != "" {
		dest = "/?tab=" + tab
	}
	http.Redirect(w, r, dest, http.StatusSeeOther)
}

func webEditorSaveGUI(w http.ResponseWriter, r *http.Request) {
	if !webEditorRequireWritable(w, r) {
		return
	}
	cfg := loadConfig()
	checkUpdates := r.FormValue("check_updates") == "on"
	cfg.GUI.CheckUpdates = &checkUpdates
	manageBackend := r.FormValue("manage_backend") == "on"
	cfg.GUI.ManageBackend = &manageBackend
	cfg.GUI.Notifications = r.FormValue("notifications")
	cfg.GUI.Icon = r.FormValue("icon")
	cfg.GUI.ScheduleDisplay = r.FormValue("schedule_display")
	cfg.GUI.Terminal = r.FormValue("terminal")
	webEditorSaveAndRedirect(w, r, cfg, "gui")
}

func webEditorSaveProfile(w http.ResponseWriter, r *http.Request) {
	if !webEditorRequireWritable(w, r) {
		return
	}
	cfg := loadConfig()
	idx, ok := webEditorParseIndex(r, cfg)
	if !ok {
		http.Error(w, "profile not found", http.StatusNotFound)
		return
	}
	p := &cfg.Profiles[idx]
	p.Name = r.FormValue("name")
	p.Backend = r.FormValue("backend")
	p.EnvFile = r.FormValue("env_file")
	p.RcloneConfigFile = r.FormValue("rclone_config_file")
	p.RetryLock = r.FormValue("retry_lock")
	p.PreHook = r.FormValue("pre_hook")
	p.PostHook = r.FormValue("post_hook")
	p.Schedule.Cron = r.FormValue("cron")
	p.Schedule.OnBattery = r.FormValue("on_battery") == "on"
	backupEnabled := r.FormValue("sched_backup") == "on"
	p.Schedule.Backup = &backupEnabled
	p.Schedule.Prune = r.FormValue("sched_prune") == "on"
	p.Schedule.Check = r.FormValue("sched_check") == "on"
	p.Backup.Paths = splitLines(r.FormValue("backup_paths"))
	p.Backup.Args = splitArgs(r.FormValue("backup_args"))
	p.Backup.ArgsScheduled = splitArgs(r.FormValue("backup_args_scheduled"))
	p.Prune.Args = splitArgs(r.FormValue("prune_args"))
	p.Check.Args = splitArgs(r.FormValue("check_args"))
	p.Mount.Args = splitArgs(r.FormValue("mount_args"))
	webEditorSaveAndRedirect(w, r, cfg, fmt.Sprintf("profile-%d", idx))
}

func webEditorAddProfile(w http.ResponseWriter, r *http.Request) {
	if !webEditorRequireWritable(w, r) {
		return
	}
	cfg := loadConfig()
	newIdx := len(cfg.Profiles)
	name := fmt.Sprintf("profile-%d", newIdx+1)
	cfg.Profiles = append(cfg.Profiles, Profile{Name: name})
	webEditorSaveAndRedirect(w, r, cfg, fmt.Sprintf("profile-%d", newIdx))
}

func webEditorDeleteProfile(w http.ResponseWriter, r *http.Request) {
	if !webEditorRequireWritable(w, r) {
		return
	}
	cfg := loadConfig()
	idx, ok := webEditorParseIndex(r, cfg)
	if !ok {
		http.Error(w, "profile not found", http.StatusNotFound)
		return
	}
	cfg.Profiles = append(cfg.Profiles[:idx], cfg.Profiles[idx+1:]...)
	webEditorSaveAndRedirect(w, r, cfg, "")
}

func webEditorSaveEnv(w http.ResponseWriter, r *http.Request) {
	if !webEditorRequireWritable(w, r) {
		return
	}
	cfg := loadConfig()
	idx, ok := webEditorParseIndex(r, cfg)
	if !ok {
		http.Error(w, "profile not found", http.StatusNotFound)
		return
	}
	envFile := cfg.Profiles[idx].EnvFile
	if err := os.MkdirAll(filepath.Dir(envFile), 0700); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := os.WriteFile(envFile, []byte(r.FormValue("env_contents")), 0600); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, fmt.Sprintf("/?tab=profile-%d", idx), http.StatusSeeOther)
}
