package webui

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "modernc.org/sqlite"
)

var timezoneNames = []string{
	"UTC", "Europe/London", "Europe/Berlin", "Europe/Paris", "Europe/Madrid", "Europe/Rome",
	"America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles",
	"Asia/Tokyo", "Asia/Shanghai", "Asia/Kolkata", "Asia/Dubai", "Australia/Sydney",
}

// settingsPage mirrors settings.py's settings().
func (a *App) settingsPage(w http.ResponseWriter, r *http.Request) {
	sections := M{}
	for _, name := range a.Cfg.SectionStrings() {
		sec := a.Cfg.Section(name)
		kv := M{}
		for _, k := range sec.Keys() {
			kv[strings.ToLower(k.Name())] = k.Value()
		}
		sections[name] = kv
	}
	a.render(w, r, "settings.html", M{"active": "settings", "settings": sections, "timezones": timezoneNames})
}

// settingsUpdate mirrors settings.py's settings_update(): iterate every existing
// Section.key, update from the matching form field if present and different, write
// settings.ini back out. Preserves the '""'-means-empty convention for server_banner.
func (a *App) settingsUpdate(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		setFlash(w, "error", "Invalid form data")
		http.Redirect(w, r, Prefix+"/settings", http.StatusFound)
		return
	}
	// Turning on enforce_admin_mfa immediately blocks every admin route except
	// /account for any admin without MFA configured — including /settings itself.
	// Without this precondition, an admin who enables enforcement before setting up
	// their own MFA would lock themselves out with no way back in (short of editing
	// the database directly), since requireAuth's gate applies to this very handler.
	if r.FormValue("Auth.enforce_admin_mfa") == "true" && a.Cfg.Section("Auth").Key("enforce_admin_mfa").Value() != "true" {
		user := userFromContext(r)
		hasMFA := user.TOTPEnabled
		if !hasMFA {
			if n, _ := a.DB.CountWebAuthnCredentials(user.ID); n > 0 {
				hasMFA = true
			}
		}
		if !hasMFA {
			setFlash(w, "error", "Set up your own two-factor authentication (see Account) before enforcing it for all admins — otherwise you'd lock yourself out.")
			http.Redirect(w, r, Prefix+"/settings", http.StatusFound)
			return
		}
	}
	changed := false
	for _, name := range a.Cfg.SectionStrings() {
		sec := a.Cfg.Section(name)
		for _, k := range sec.Keys() {
			// Form field names are always lowercase (matches settingsPage's
			// strings.ToLower(k.Name()) template keys), but ini key names keep
			// whatever case the defaults table used (e.g. "SMTP_PORT") — comparing
			// k.Name() directly here meant every uppercase-defined key silently
			// never saved.
			field := name + "." + strings.ToLower(k.Name())
			if !r.Form.Has(field) {
				continue
			}
			val := r.FormValue(field)
			if name == "Server" && strings.EqualFold(k.Name(), "server_banner") && strings.TrimSpace(val) == "" {
				val = `""`
			}
			if val != k.Value() {
				k.SetValue(val)
				changed = true
			}
		}
	}
	if !changed {
		setFlash(w, "info", "No changes were made")
		http.Redirect(w, r, Prefix+"/settings", http.StatusFound)
		return
	}
	if err := a.Cfg.SaveTo(a.ConfigPath); err != nil {
		setFlash(w, "error", "Error saving settings: "+err.Error())
		http.Redirect(w, r, Prefix+"/settings", http.StatusFound)
		return
	}
	setFlash(w, "success", "Settings saved. Restart the server for changes to take effect.")
	http.Redirect(w, r, Prefix+"/settings", http.StatusFound)
}

// testDatabaseConnection mirrors settings.py's test_database_connection_endpoint,
// backed by a Go equivalent of database.py's test_database_connection. Unlike the
// Python version (which pip-installs a driver at request time for mysql/postgresql/
// mssql), this build only compiles in the sqlite driver — other schemes report clearly
// as unsupported rather than attempting a live package install from a web request.
func (a *App) testDatabaseConnection(w http.ResponseWriter, r *http.Request) {
	var body struct {
		URL string `json:"url"`
	}
	if err := decodeJSONBody(r, &body); err != nil || body.URL == "" {
		writeJSON(w, http.StatusOK, M{"status": "error", "message": "No database URL provided"})
		return
	}
	switch {
	case strings.HasPrefix(body.URL, "sqlite:///"):
		path := strings.TrimPrefix(body.URL, "sqlite:///")
		db, err := sql.Open("sqlite", path)
		if err != nil {
			writeJSON(w, http.StatusOK, M{"status": "error", "message": err.Error()})
			return
		}
		defer db.Close()
		if err := db.Ping(); err != nil {
			writeJSON(w, http.StatusOK, M{"status": "error", "message": err.Error()})
			return
		}
		writeJSON(w, http.StatusOK, M{"status": "success", "message": "SQLite connection successful"})
	case strings.HasPrefix(body.URL, "mysql://"), strings.HasPrefix(body.URL, "postgresql://"), strings.HasPrefix(body.URL, "mssql"):
		writeJSON(w, http.StatusOK, M{"status": "error", "message": "This database type isn't compiled into this build"})
	default:
		writeJSON(w, http.StatusOK, M{"status": "error", "message": "Unrecognized database URL scheme"})
	}
}

func decodeJSONBody(r *http.Request, v any) error {
	defer r.Body.Close()
	return json.NewDecoder(r.Body).Decode(v)
}

// uploadCert mirrors settings.py's upload_cert().
func (a *App) uploadCert(w http.ResponseWriter, r *http.Request) {
	a.uploadTLSFile(w, r, "cert_file", "crt")
}

// uploadKey mirrors settings.py's upload_key().
func (a *App) uploadKey(w http.ResponseWriter, r *http.Request) {
	a.uploadTLSFile(w, r, "key_file", "key")
}

func (a *App) uploadTLSFile(w http.ResponseWriter, r *http.Request, field, forcedExt string) {
	if err := r.ParseMultipartForm(10 << 20); err != nil {
		writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "Invalid upload"})
		return
	}
	file, header, err := r.FormFile(field)
	if err != nil {
		writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "No file provided"})
		return
	}
	defer file.Close()
	ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(header.Filename), "."))
	if ext != "crt" && ext != "key" && ext != "pem" {
		writeJSON(w, http.StatusBadRequest, M{"status": "error", "message": "Invalid file extension"})
		return
	}
	sslDir := filepath.Join(filepath.Dir(a.ConfigPath), "ssl_certs")
	os.MkdirAll(sslDir, 0o755)
	filePath := filepath.Join(sslDir, fmt.Sprintf("server%d.%s", time.Now().Unix(), forcedExt))
	out, err := os.Create(filePath)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, M{"status": "error", "message": "Could not save file"})
		return
	}
	defer out.Close()
	if _, err := out.ReadFrom(file); err != nil {
		writeJSON(w, http.StatusInternalServerError, M{"status": "error", "message": "Could not save file"})
		return
	}
	writeJSON(w, http.StatusOK, M{"status": "success", "filepath": filePath})
}

// getServerIP mirrors settings.py's get_server_ip().
func (a *App) getServerIP(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, M{"status": "success", "ip": getPublicIP(a.Cfg)})
}

// testAttachmentsPath mirrors settings.py's test_attachments_path().
func (a *App) testAttachmentsPath(w http.ResponseWriter, r *http.Request) {
	path := r.FormValue("path")
	if path == "" {
		writeJSON(w, http.StatusOK, M{"success": false, "message": "No path provided"})
		return
	}
	if !filepath.IsAbs(path) {
		path = filepath.Join(filepath.Dir(a.ConfigPath), path)
	}
	if err := os.MkdirAll(path, 0o755); err != nil {
		writeJSON(w, http.StatusOK, M{"success": false, "message": err.Error()})
		return
	}
	testFile := filepath.Join(path, ".write_test")
	if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil {
		writeJSON(w, http.StatusOK, M{"success": false, "message": "Path is not writable: " + err.Error()})
		return
	}
	os.Remove(testFile)
	writeJSON(w, http.StatusOK, M{"success": true, "message": "Path is valid and writable", "absolute_path": path})
}
