package admin

import (
	"context"
	"fmt"
	"net"
	"strconv"
	"strings"

	"github.com/owncord/server/config"
	"github.com/owncord/server/db"
)

// ─── SetupOptions ────────────────────────────────────────────────────────────

// SetupOptions wires the first-run setup wizard to the running server. The
// zero value disables everything beyond legacy owner-account creation, which
// keeps every existing NewAdminAPI/NewHandler call site behaving as before.
type SetupOptions struct {
	// ConfigPath is the config.yaml path the wizard patches. Empty disables
	// config writing entirely.
	ConfigPath string
	// RunningCfg is the configuration the server booted with. It provides the
	// wizard's prefill defaults, the values compared against to decide whether
	// a restart is needed, and the generated LiveKit credentials to persist.
	// Nil disables prefill and restarting.
	RunningCfg *config.Config
	// Restart replaces the process-restart hook (tests). Nil = requestRestart.
	Restart func(reason string)
}

// ─── Wizard payload ──────────────────────────────────────────────────────────

// setupWizardRequest is the optional "wizard" object on POST /api/setup.
// Every field is a pointer: absent means "keep the current/default value".
type setupWizardRequest struct {
	// Stored in the settings table (read live, no restart needed).
	ServerName       *string `json:"server_name"`
	Motd             *string `json:"motd"`
	RegistrationOpen *bool   `json:"registration_open"`

	// Stored in config.yaml (consumed at startup — changes need a restart).
	Port            *int    `json:"port"`
	TLSMode         *string `json:"tls_mode"`
	TLSDomain       *string `json:"tls_domain"`
	UploadMaxSizeMB *int    `json:"upload_max_size_mb"`
	VoiceQuality    *string `json:"voice_quality"`
	// VoiceAutoDownload toggles voice.auto_download_livekit — download and
	// run livekit-server automatically so voice works with zero setup.
	VoiceAutoDownload *bool `json:"voice_auto_download"`
}

// setupDefaults is the prefill data the wizard shows. Exposed only while
// needs_setup is true, and deliberately free of secrets, filesystem paths and
// network ACLs.
type setupDefaults struct {
	ServerName        string `json:"server_name"`
	Motd              string `json:"motd"`
	RegistrationOpen  bool   `json:"registration_open"`
	Port              int    `json:"port"`
	TLSMode           string `json:"tls_mode"`
	TLSDomain         string `json:"tls_domain"`
	UploadMaxSizeMB   int    `json:"upload_max_size_mb"`
	VoiceQuality      string `json:"voice_quality"`
	VoiceAutoDownload bool   `json:"voice_auto_download"`
}

// ─── Validation ──────────────────────────────────────────────────────────────

const (
	maxServerNameLen = 100
	maxMotdLen       = 500
	maxUploadSizeMB  = 10240 // 10 GiB
)

var validTLSModes = map[string]struct{}{
	"self_signed": {}, "acme": {}, "manual": {}, "off": {},
}

var validVoiceQualities = map[string]struct{}{
	"low": {}, "medium": {}, "high": {},
}

// validateWizard checks and normalises the wizard payload in place. It must
// be called BEFORE the owner account is created so a bad payload rejects the
// whole request instead of leaving a half-configured server.
func validateWizard(wr *setupWizardRequest) error {
	if wr.ServerName != nil {
		name := strings.TrimSpace(setupSanitizer.Sanitize(*wr.ServerName))
		if name == "" {
			return fmt.Errorf("server_name cannot be empty")
		}
		if len(name) > maxServerNameLen {
			return fmt.Errorf("server_name must be at most %d characters", maxServerNameLen)
		}
		*wr.ServerName = name
	}
	if wr.Motd != nil {
		motd := strings.TrimSpace(setupSanitizer.Sanitize(*wr.Motd))
		if len(motd) > maxMotdLen {
			return fmt.Errorf("motd must be at most %d characters", maxMotdLen)
		}
		*wr.Motd = motd
	}
	if wr.Port != nil && (*wr.Port < 1 || *wr.Port > 65535) {
		return fmt.Errorf("port must be between 1 and 65535")
	}
	if wr.TLSMode != nil {
		mode := strings.ToLower(strings.TrimSpace(*wr.TLSMode))
		if _, ok := validTLSModes[mode]; !ok {
			return fmt.Errorf("tls_mode must be one of: self_signed, acme, manual, off")
		}
		*wr.TLSMode = mode
	}
	if wr.TLSDomain != nil {
		domain := strings.ToLower(strings.TrimSpace(*wr.TLSDomain))
		if domain != "" {
			if err := validateHostname(domain); err != nil {
				return fmt.Errorf("tls_domain: %w", err)
			}
		}
		*wr.TLSDomain = domain
	}
	if wr.TLSMode != nil && *wr.TLSMode == "acme" &&
		(wr.TLSDomain == nil || *wr.TLSDomain == "") {
		return fmt.Errorf("tls_domain is required when tls_mode is acme")
	}
	if wr.UploadMaxSizeMB != nil && (*wr.UploadMaxSizeMB < 1 || *wr.UploadMaxSizeMB > maxUploadSizeMB) {
		return fmt.Errorf("upload_max_size_mb must be between 1 and %d", maxUploadSizeMB)
	}
	if wr.VoiceQuality != nil {
		q := strings.ToLower(strings.TrimSpace(*wr.VoiceQuality))
		if _, ok := validVoiceQualities[q]; !ok {
			return fmt.Errorf("voice_quality must be one of: low, medium, high")
		}
		*wr.VoiceQuality = q
	}
	return nil
}

// validateHostname checks an LDH (letters-digits-hyphen) DNS name suitable
// for ACME issuance: dotted, each label 1-63 chars, no leading/trailing
// hyphen, 253 chars max. Input is expected lowercase.
func validateHostname(h string) error {
	if len(h) > 253 {
		return fmt.Errorf("hostname too long")
	}
	labels := strings.Split(h, ".")
	if len(labels) < 2 {
		return fmt.Errorf("must be a fully qualified domain name (e.g. chat.example.com)")
	}
	for _, label := range labels {
		if label == "" || len(label) > 63 {
			return fmt.Errorf("invalid hostname label")
		}
		if label[0] == '-' || label[len(label)-1] == '-' {
			return fmt.Errorf("hostname labels cannot start or end with a hyphen")
		}
		for _, c := range label {
			if (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' {
				return fmt.Errorf("hostname contains invalid characters")
			}
		}
	}
	return nil
}

// ─── Applying the wizard ─────────────────────────────────────────────────────

// applyWizardSettings persists the wizard's DB-backed settings atomically.
// server_name, motd and registration_open are read live by the server;
// max_upload_bytes and voice_quality are written so the Settings page shows
// values consistent with what the wizard put in config.yaml.
func applyWizardSettings(ctx context.Context, database *db.DB, wr *setupWizardRequest) error {
	updates := map[string]string{}
	if wr.ServerName != nil {
		updates["server_name"] = *wr.ServerName
	}
	if wr.Motd != nil {
		updates["motd"] = *wr.Motd
	}
	if wr.RegistrationOpen != nil {
		if *wr.RegistrationOpen {
			updates["registration_open"] = "1"
		} else {
			updates["registration_open"] = "0"
		}
	}
	if wr.UploadMaxSizeMB != nil {
		updates["max_upload_bytes"] = strconv.Itoa(*wr.UploadMaxSizeMB * 1024 * 1024)
	}
	if wr.VoiceQuality != nil {
		updates["voice_quality"] = *wr.VoiceQuality
	}
	if len(updates) == 0 {
		return nil
	}

	tx, err := database.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("starting transaction: %w", err)
	}
	for key, value := range updates {
		if _, txErr := tx.ExecContext(ctx,
			`INSERT INTO settings (key, value) VALUES (?, ?)
			 ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
			key, value,
		); txErr != nil {
			_ = tx.Rollback()
			return fmt.Errorf("writing setting %s: %w", key, txErr)
		}
	}
	return tx.Commit()
}

// buildConfigPatch maps the wizard payload onto config.yaml keys. When the
// running config is known, the LiveKit credentials it booted with are included
// so config.Save can persist them if the file has none — stabilising voice
// tokens across restarts (they are otherwise regenerated randomly each boot).
func buildConfigPatch(wr *setupWizardRequest, running *config.Config) config.Patch {
	p := config.Patch{
		ServerPort:        wr.Port,
		ServerName:        wr.ServerName,
		TLSMode:           wr.TLSMode,
		TLSDomain:         wr.TLSDomain,
		UploadMaxSizeMB:   wr.UploadMaxSizeMB,
		VoiceQuality:      wr.VoiceQuality,
		VoiceAutoDownload: wr.VoiceAutoDownload,
	}
	if running != nil {
		if key := running.Voice.LiveKitAPIKey; key != "" {
			p.VoiceAPIKey = &key
		}
		if secret := running.Voice.LiveKitAPISecret; secret != "" {
			p.VoiceAPISecret = &secret
		}
	}
	return p
}

// patchedConfigKeys summarises which config.yaml keys a patch touches, for
// the audit log. Secrets are named, never valued.
func patchedConfigKeys(wr *setupWizardRequest) string {
	var keys []string
	if wr.Port != nil {
		keys = append(keys, "server.port")
	}
	if wr.ServerName != nil {
		keys = append(keys, "server.name")
	}
	if wr.TLSMode != nil {
		keys = append(keys, "tls.mode")
	}
	if wr.TLSDomain != nil {
		keys = append(keys, "tls.domain")
	}
	if wr.UploadMaxSizeMB != nil {
		keys = append(keys, "upload.max_size_mb")
	}
	if wr.VoiceQuality != nil {
		keys = append(keys, "voice.quality")
	}
	if wr.VoiceAutoDownload != nil {
		keys = append(keys, "voice.auto_download_livekit")
	}
	keys = append(keys, "voice credentials (persisted if unset)")
	return strings.Join(keys, ", ")
}

// ─── Restart decision ────────────────────────────────────────────────────────

// wizardChangesRunningConfig reports whether the wizard set any startup-only
// value to something different from what this process booted with. Only those
// changes justify a restart; server.name and the persisted voice credentials
// match the running state by construction.
func wizardChangesRunningConfig(wr *setupWizardRequest, running *config.Config) bool {
	if wr.Port != nil && *wr.Port != running.Server.Port {
		return true
	}
	if wr.TLSMode != nil && *wr.TLSMode != running.TLS.Mode {
		return true
	}
	if wr.UploadMaxSizeMB != nil && *wr.UploadMaxSizeMB != running.Upload.MaxSizeMB {
		return true
	}
	if wr.VoiceQuality != nil && *wr.VoiceQuality != running.Voice.Quality {
		return true
	}
	if wr.VoiceAutoDownload != nil && *wr.VoiceAutoDownload != running.Voice.AutoDownloadLiveKit {
		return true
	}
	// A domain change only matters when certificates come from ACME.
	effMode := running.TLS.Mode
	if wr.TLSMode != nil {
		effMode = *wr.TLSMode
	}
	if effMode == "acme" && wr.TLSDomain != nil && *wr.TLSDomain != running.TLS.Domain {
		return true
	}
	return false
}

// computeRestartURL builds the admin-panel URL the server will be reachable
// at after restarting with the wizard's values. host is the request's Host
// header (the address the user's browser is already using).
func computeRestartURL(host string, wr *setupWizardRequest, running *config.Config) string {
	h := host
	if hh, _, err := net.SplitHostPort(host); err == nil {
		h = hh
	}
	effPort := running.Server.Port
	if wr.Port != nil {
		effPort = *wr.Port
	}
	effMode := running.TLS.Mode
	if wr.TLSMode != nil {
		effMode = *wr.TLSMode
	}
	scheme := "https"
	if effMode == "off" {
		scheme = "http"
	}
	return scheme + "://" + net.JoinHostPort(h, strconv.Itoa(effPort)) + "/admin"
}
