package server

import (
	"fmt"
	"net/http"
	"sort"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"warpbox/lib/config"
)

type adminSettingsCategoryView struct {
	Key   string
	Label string
	Icon  string
	Count int
	Rows  []adminSettingRowView
}

type adminSettingRowView struct {
	Key           string `json:"key"`
	Label         string `json:"label"`
	EnvName       string `json:"env_name"`
	Category      string `json:"category"`
	CategoryLabel string `json:"category_label"`
	Type          string `json:"type"`
	Value         string `json:"value"`
	DefaultValue  string `json:"default_value"`
	Source        string `json:"source"`
	SourceBadge   string `json:"source_badge"`
	Editable      bool   `json:"editable"`
	Locked        bool   `json:"locked"`
	HardLimit     bool   `json:"hard_limit"`
	Minimum       int64  `json:"minimum"`
	Description   string `json:"description"`
}

type adminSettingsSaveRequest struct {
	Values map[string]string `json:"values"`
}

type adminSettingsImportRequest struct {
	Settings         map[string]string `json:"settings"`
	EditableSettings map[string]string `json:"editable_settings"`
	Values           map[string]string `json:"values"`
	Changes          map[string]string `json:"changes"`
}

type adminSettingsResetRequest struct {
	Keys []string `json:"keys"`
}

type adminSettingsExportResponse struct {
	Format           string                `json:"format"`
	ExportedAt       string                `json:"exported_at"`
	Settings         map[string]string     `json:"settings"`
	EditableSettings map[string]string     `json:"editable_settings"`
	Rows             []adminSettingRowView `json:"rows"`
}

func (app *App) handleAdminSettings(ctx *gin.Context) {
	rows, categories := app.buildAdminSettingsRows()
	ctx.HTML(http.StatusOK, "admin/settings.html", gin.H{
		"AdminUsername": app.config.AdminUsername,
		"AdminEmail":    app.config.AdminEmail,
		"ActivePage":    "settings",
		"Rows":          rows,
		"Categories":    categories,
		"RowsJSON":      rows,
	})
}

func (app *App) handleAdminSettingsExport(ctx *gin.Context) {
	rows, _ := app.buildAdminSettingsRows()
	ctx.JSON(http.StatusOK, app.buildSettingsExportPayload(rows))
}

func (app *App) handleAdminSettingsSave(ctx *gin.Context) {
	var request adminSettingsSaveRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid save payload"})
		return
	}
	currentOverrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load current settings overrides"})
		return
	}
	if currentOverrides == nil {
		currentOverrides = map[string]string{}
	}
	for key, value := range request.Values {
		currentOverrides[key] = value
	}

	rows, warnings, err := app.applySettingsOverrideSet(currentOverrides)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"ok":       true,
		"message":  fmt.Sprintf("Saved %d editable setting(s)", len(request.Values)),
		"warnings": warnings,
		"rows":     rows,
	})
}

func (app *App) handleAdminSettingsImport(ctx *gin.Context) {
	var request adminSettingsImportRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid import payload"})
		return
	}

	values := request.Values
	if len(values) == 0 {
		values = request.Settings
	}
	if len(values) == 0 {
		values = request.EditableSettings
	}
	if len(values) == 0 {
		values = request.Changes
	}
	if len(values) == 0 {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "No importable settings found"})
		return
	}

	editable := map[string]bool{}
	for _, def := range config.EditableDefinitions() {
		editable[def.Key] = true
	}
	filtered := make(map[string]string, len(values))
	warnings := make([]string, 0)
	for key, value := range values {
		if editable[key] {
			filtered[key] = value
			continue
		}
		if _, found := config.Definition(key); found {
			warnings = append(warnings, fmt.Sprintf("%s skipped: locked", key))
			continue
		}
		warnings = append(warnings, fmt.Sprintf("%s skipped: unknown key", key))
	}
	currentOverrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load current settings overrides"})
		return
	}
	for key, value := range currentOverrides {
		if _, exists := filtered[key]; !exists {
			filtered[key] = value
		}
	}

	rows, applyWarnings, err := app.applySettingsOverrideSet(filtered)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	warnings = append(warnings, applyWarnings...)

	ctx.JSON(http.StatusOK, gin.H{
		"ok":       true,
		"message":  fmt.Sprintf("Imported %d setting value(s)", len(values)),
		"warnings": warnings,
		"rows":     rows,
	})
}

func (app *App) handleAdminSettingsReset(ctx *gin.Context) {
	var request adminSettingsResetRequest
	_ = ctx.ShouldBindJSON(&request)

	overrideSet, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not load settings overrides"})
		return
	}
	if overrideSet == nil {
		overrideSet = map[string]string{}
	}
	targetKeys := map[string]bool{}
	for _, key := range request.Keys {
		targetKeys[config.NormalizeLegacySettingKey(key)] = true
	}

	if len(targetKeys) == 0 {
		overrideSet = map[string]string{}
	} else {
		for key := range targetKeys {
			delete(overrideSet, key)
		}
	}

	rows, warnings, err := app.applySettingsOverrideSet(overrideSet)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"ok":       true,
		"message":  "Selected overrides cleared; environment and defaults now apply",
		"warnings": warnings,
		"rows":     rows,
	})
}

func (app *App) applySettingsOverrideSet(values map[string]string) ([]adminSettingRowView, []string, error) {
	if !app.config.AllowAdminSettingsOverride {
		return nil, nil, fmt.Errorf("runtime admin setting overrides are disabled by environment")
	}
	if values == nil {
		values = map[string]string{}
	}

	overrideSet := make(map[string]string, len(values))
	warnings := make([]string, 0)
	editable := map[string]config.SettingDefinition{}
	for _, def := range config.EditableDefinitions() {
		editable[def.Key] = def
	}

	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	for _, key := range keys {
		normalizedKey, normalizedValue, err := config.NormalizeOverrideInput(key, strings.TrimSpace(values[key]))
		if err != nil {
			return nil, nil, fmt.Errorf("%s: %w", key, err)
		}
		key = normalizedKey
		value := normalizedValue
		def, ok := editable[key]
		if !ok {
			if _, found := config.Definition(key); found {
				return nil, nil, fmt.Errorf("setting %q is locked and cannot be changed", key)
			}
			warnings = append(warnings, fmt.Sprintf("%s skipped: unknown key", key))
			continue
		}
		if value == "" && def.Type != config.SettingTypeText {
			return nil, nil, fmt.Errorf("setting %q cannot be blank", key)
		}
		overrideSet[key] = value
	}

	nextCfg, err := config.Load()
	if err != nil {
		return nil, nil, err
	}
	if err := nextCfg.ApplyOverrides(overrideSet); err != nil {
		return nil, nil, err
	}
	if err := config.WriteAdminSettingsOverrides(app.settingsOverridesPath, overrideSet); err != nil {
		return nil, nil, err
	}

	app.config = nextCfg
	applyBoxstoreRuntimeConfig(app.config)
	app.reloadSecurityConfig()
	rows, _ := app.buildAdminSettingsRows()
	return rows, warnings, nil
}

func (app *App) buildSettingsExportPayload(rows []adminSettingRowView) adminSettingsExportResponse {
	settings := make(map[string]string, len(rows))
	editable := make(map[string]string)
	for _, row := range rows {
		settings[row.Key] = row.Value
		if row.Editable && !row.Locked {
			editable[row.Key] = row.Value
		}
	}
	return adminSettingsExportResponse{
		Format:           "warpbox.settings.export.v1",
		ExportedAt:       time.Now().UTC().Format(time.RFC3339),
		Settings:         settings,
		EditableSettings: editable,
		Rows:             rows,
	}
}

func (app *App) buildAdminSettingsRows() ([]adminSettingRowView, []adminSettingsCategoryView) {
	cfgRows := app.config.SettingRows()
	rows := make([]adminSettingRowView, 0, len(cfgRows)+5)
	for _, row := range cfgRows {
		rows = append(rows, app.makeDefinitionSettingRow(row))
	}
	rows = append(rows,
		app.makeLockedSettingRow("admin_username", "Admin username", "WARPBOX_ADMIN_USERNAME", "accounts", "admin", app.config.AdminUsername, "Environment-controlled admin login name."),
		app.makeLockedSettingRow("admin_email", "Admin email", "WARPBOX_ADMIN_EMAIL", "accounts", "admin", app.config.AdminEmail, "Administrative contact address used for future account and alert workflows."),
		app.makeLockedSettingRow("admin_enabled", "Admin enabled mode", "WARPBOX_ADMIN_ENABLED", "accounts", "admin", string(app.config.AdminEnabled), "Controls whether administrative login is disabled, forced on, or auto-detected."),
		app.makeLockedSettingRow("admin_cookie_secure", "Admin cookie secure", "WARPBOX_ADMIN_COOKIE_SECURE", "accounts", "bool", boolString(app.config.AdminCookieSecure), "Secure admin cookie flag. Locking this avoids accidental auth regressions."),
		app.makeLockedSettingRow("allow_admin_settings_override", "Admin settings override allowed", "WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE", "accounts", "bool", boolString(app.config.AllowAdminSettingsOverride), "Master switch for runtime admin setting overrides."),
	)

	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Category == rows[j].Category {
			return rows[i].Label < rows[j].Label
		}
		return settingsCategoryRank(rows[i].Category) < settingsCategoryRank(rows[j].Category)
	})

	categoryMeta := settingsCategoryMeta()
	categories := make([]adminSettingsCategoryView, 0, len(categoryMeta)+1)
	allCategory := adminSettingsCategoryView{Key: "all", Label: "All settings", Icon: "▤", Count: len(rows)}
	categories = append(categories, allCategory)

	grouped := map[string][]adminSettingRowView{}
	for _, row := range rows {
		grouped[row.Category] = append(grouped[row.Category], row)
	}

	for _, meta := range categoryMeta {
		categories = append(categories, adminSettingsCategoryView{
			Key:   meta.Key,
			Label: meta.Label,
			Icon:  meta.Icon,
			Count: len(grouped[meta.Key]),
			Rows:  grouped[meta.Key],
		})
	}

	return rows, categories
}

func boolString(value bool) string {
	if value {
		return "true"
	}
	return "false"
}

func (app *App) makeDefinitionSettingRow(row config.SettingRow) adminSettingRowView {
	def := row.Definition
	locked := !def.Editable || def.HardLimit
	source := string(row.Source)
	sourceBadge := source
	if locked {
		sourceBadge = "hard env"
	}
	return adminSettingRowView{
		Key:           def.Key,
		Label:         def.Label,
		EnvName:       def.EnvName,
		Category:      settingsCategoryForKey(def.Key),
		CategoryLabel: settingsCategoryLabel(settingsCategoryForKey(def.Key)),
		Type:          string(def.Type),
		Value:         row.Value,
		DefaultValue:  app.config.DefaultValue(def.Key),
		Source:        source,
		SourceBadge:   sourceBadge,
		Editable:      def.Editable && !def.HardLimit,
		Locked:        locked,
		HardLimit:     def.HardLimit,
		Minimum:       def.Minimum,
		Description:   settingsDescription(def.Key),
	}
}

func (app *App) makeLockedSettingRow(key string, label string, envName string, category string, rowType string, value string, description string) adminSettingRowView {
	return adminSettingRowView{
		Key:           key,
		Label:         label,
		EnvName:       envName,
		Category:      category,
		CategoryLabel: settingsCategoryLabel(category),
		Type:          rowType,
		Value:         value,
		DefaultValue:  "",
		Source:        "environment",
		SourceBadge:   "hard env",
		Editable:      false,
		Locked:        true,
		HardLimit:     true,
		Description:   description,
	}
}

type settingsCategoryInfo struct {
	Key   string
	Label string
	Icon  string
}

func settingsCategoryMeta() []settingsCategoryInfo {
	return []settingsCategoryInfo{
		{Key: "uploads", Label: "Uploads", Icon: "↥"},
		{Key: "downloads", Label: "Downloads", Icon: "↧"},
		{Key: "retention", Label: "Retention", Icon: "⌛"},
		{Key: "security", Label: "Security", Icon: "🔒"},
		{Key: "activity", Label: "Activity", Icon: "☰"},
		{Key: "accounts", Label: "Accounts", Icon: "☺"},
		{Key: "api", Label: "API", Icon: "{ }"},
		{Key: "storage", Label: "Storage", Icon: "▥"},
		{Key: "workers", Label: "Workers", Icon: "⚙"},
	}
}

func settingsCategoryLabel(key string) string {
	for _, meta := range settingsCategoryMeta() {
		if meta.Key == key {
			return meta.Label
		}
	}
	return "General"
}

func settingsCategoryRank(key string) int {
	for index, meta := range settingsCategoryMeta() {
		if meta.Key == key {
			return index
		}
	}
	return len(settingsCategoryMeta()) + 1
}

func settingsCategoryForKey(key string) string {
	switch key {
	case config.SettingGuestUploadsEnabled, config.SettingDefaultUserMaxFileBytes, config.SettingDefaultUserMaxBoxBytes, config.SettingGlobalMaxFileSizeBytes, config.SettingGlobalMaxBoxSizeBytes:
		return "uploads"
	case config.SettingSecurityUploadWindowSecs, config.SettingSecurityUploadMaxRequests, config.SettingSecurityUploadMaxGB:
		return "uploads"
	case config.SettingZipDownloadsEnabled, config.SettingOneTimeDownloadsEnabled, config.SettingOneTimeDownloadExpirySecs, config.SettingRenewOnDownloadEnabled:
		return "downloads"
	case config.SettingRenewOnAccessEnabled, config.SettingDefaultGuestExpirySecs, config.SettingMaxGuestExpirySecs, config.SettingOneTimeDownloadRetryFail:
		return "retention"
	case config.SettingSecurityIPWhitelist, config.SettingSecurityAdminIPWhitelist, config.SettingSecurityLoginWindowSecs, config.SettingSecurityLoginMaxAttempts, config.SettingSecurityBanSeconds, config.SettingSecurityScanWindowSecs, config.SettingSecurityScanMaxAttempts:
		return "security"
	case config.SettingActivityRetentionSeconds:
		return "activity"
	case config.SettingSessionTTLSeconds:
		return "accounts"
	case config.SettingAPIEnabled:
		return "api"
	case config.SettingDataDir:
		return "storage"
	case config.SettingBoxPollIntervalMS, config.SettingThumbnailBatchSize, config.SettingThumbnailIntervalSeconds:
		return "workers"
	default:
		return "accounts"
	}
}

func settingsDescription(key string) string {
	descriptions := map[string]string{
		config.SettingGuestUploadsEnabled:       "Allow unauthenticated guests to create boxes through the public upload flow.",
		config.SettingAPIEnabled:                "Enable API endpoints used by the browser upload and status workflows.",
		config.SettingZipDownloadsEnabled:       "Allow archive downloads for full boxes when ZIP is supported.",
		config.SettingOneTimeDownloadsEnabled:   "Enable one-time download retention mode for boxes.",
		config.SettingOneTimeDownloadExpirySecs: "Expiry window, in seconds, for one-time download boxes after upload completion.",
		config.SettingOneTimeDownloadRetryFail:  "When enabled by environment, failed one-time ZIP writes leave the box retryable.",
		config.SettingRenewOnAccessEnabled:      "Extend retention when a box page is viewed.",
		config.SettingRenewOnDownloadEnabled:    "Extend retention when file or ZIP downloads happen.",
		config.SettingDefaultGuestExpirySecs:    "Default retention presented to guest uploads.",
		config.SettingMaxGuestExpirySecs:        "Maximum retention guests may request.",
		config.SettingGlobalMaxFileSizeBytes:    "Global single-file upload ceiling in GB applied to future requests across the whole app. Decimal values allowed.",
		config.SettingGlobalMaxBoxSizeBytes:     "Global total box size ceiling in GB applied to future requests across the whole app. Decimal values allowed.",
		config.SettingDefaultUserMaxFileBytes:   "Default per-user file size ceiling in GB used by future account-aware flows. Decimal values allowed.",
		config.SettingDefaultUserMaxBoxBytes:    "Default per-user box size ceiling in GB used by future account-aware flows. Decimal values allowed.",
		config.SettingSessionTTLSeconds:         "Lifetime for authenticated browser sessions, including admin session cookies.",
		config.SettingBoxPollIntervalMS:         "Browser polling cadence for box status refreshes.",
		config.SettingThumbnailBatchSize:        "How many thumbnail jobs the worker handles per batch.",
		config.SettingThumbnailIntervalSeconds:  "Delay between thumbnail worker passes.",
		config.SettingDataDir:                   "Root data path. Locked because moving storage roots live is risky.",
		config.SettingActivityRetentionSeconds:  "How long activity events stay stored before automatic prune.",
		config.SettingSecurityIPWhitelist:       "Comma-separated IPs that bypass generic security bans and rate-limits.",
		config.SettingSecurityAdminIPWhitelist:  "Comma-separated IPs allowed to bypass admin login brute-force controls.",
		config.SettingSecurityLoginWindowSecs:   "Window used for failed admin login counting.",
		config.SettingSecurityLoginMaxAttempts:  "Max failed admin logins per window before temporary ban.",
		config.SettingSecurityBanSeconds:        "Duration for automatic temporary IP bans.",
		config.SettingSecurityScanWindowSecs:    "Window used for malicious path scan detection.",
		config.SettingSecurityScanMaxAttempts:   "Max suspicious path probes per window before temporary ban.",
		config.SettingSecurityUploadWindowSecs:  "Window used for per-IP upload throttling.",
		config.SettingSecurityUploadMaxRequests: "Max upload requests per IP per upload window.",
		config.SettingSecurityUploadMaxGB:       "Max upload volume in GB per IP per upload window.",
	}
	return descriptions[key]
}
