package admin

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"sort"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/txn2/mcp-data-platform/pkg/configstore"
	"github.com/txn2/mcp-data-platform/pkg/platform"
)

// defaultChangelogLimit is the maximum number of changelog entries to return.
const defaultChangelogLimit = 50

// Config key aliases for whitelisted, hot-reloadable platform settings.
// Source of truth lives in pkg/platform — these are local references so
// the admin package depends on the same canonical wire-protocol keys
// the platform reads (a rename on one side without the other would
// silently misroute admin writes from platform reads, dropping live
// overrides).
const (
	cfgKeyServerDescription       = platform.ConfigKeyServerDescription
	cfgKeyServerAgentInstructions = platform.ConfigKeyServerAgentInstructions
)

// Config store source modes for the merged "effective" view.
const (
	configSourceDatabase = "database"
	configSourceFile     = "file"
)

// configEntryWhitelist defines the static keys that can be set via the
// admin API. Dynamic per-tool keys (tool.<name>.description) are accepted
// in addition to these via isConfigKeyAllowed when the named tool exists
// in the toolkit registry. The "tools.deny" key carries a JSON-encoded
// []string and is normally written through the per-tool visibility
// endpoint, but can also be edited directly via /config/entries.
var configEntryWhitelist = map[string]bool{
	cfgKeyServerDescription:       true,
	cfgKeyServerAgentInstructions: true,
	toolsDenyConfigKey:            true,
}

// toolDescriptionPrefix and toolDescriptionSuffix bracket the dynamic
// per-tool description override key. A key like "tool.trino_query.description"
// is editable iff the substring between them matches a registered tool name.
const (
	toolDescriptionPrefix = "tool."
	toolDescriptionSuffix = ".description"
)

// extractToolNameFromDescriptionKey returns the tool name embedded in a
// "tool.<name>.description" key, or ("", false) if the key does not match
// the pattern.
func extractToolNameFromDescriptionKey(key string) (string, bool) {
	if len(key) <= len(toolDescriptionPrefix)+len(toolDescriptionSuffix) {
		return "", false
	}
	if !strings.HasPrefix(key, toolDescriptionPrefix) || !strings.HasSuffix(key, toolDescriptionSuffix) {
		return "", false
	}
	return key[len(toolDescriptionPrefix) : len(key)-len(toolDescriptionSuffix)], true
}

// isConfigKeyAllowed returns true if the key is editable via PUT/DELETE
// /api/v1/admin/config/entries/{key}. Static whitelist always wins;
// otherwise tool.<name>.description is allowed when <name> matches a
// tool currently registered in the toolkit registry OR a platform-level
// tool registered directly on the MCP server (platform_info,
// list_connections, manage_prompt). A typo for an unregistered name is
// rejected so config_entries doesn't accumulate orphan rows.
func (h *Handler) isConfigKeyAllowed(key string) bool {
	if configEntryWhitelist[key] {
		return true
	}
	name, ok := extractToolNameFromDescriptionKey(key)
	if !ok {
		return false
	}
	if h.deps.ToolkitRegistry != nil &&
		slices.Contains(h.deps.ToolkitRegistry.AllTools(), name) {
		return true
	}
	for _, pt := range h.deps.PlatformTools {
		if pt.Name == name {
			return true
		}
	}
	return false
}

// Sensitive config-key name constants. These match exact keys (lower-cased)
// inside YAML/JSON config maps that should be redacted when serialized for
// admin clients. Some are reused by connection_handler.go's redaction list.
//
// The full set must match (or be a superset of) the at-rest encryption
// list in pkg/platform/fieldcrypt.go::sensitiveConfigKeys — any key
// the encryption layer treats as a secret must also be redacted by the
// admin endpoints, or a misconfigured deployment (e.g. ENCRYPTION_KEY
// unset, plaintext at rest) leaks through /api/admin/config.
// Names declared in connection_handler.go (sensKeySecretKey,
// sensKeyAccessToken, sensKeyRefreshToken, sensKeyOAuthClientSecret)
// are package-scoped and re-used by sensitiveKeys below — admin
// API redaction must cover the same fields the at-rest encryption
// layer protects.
const (
	sensKeyKey             = "key"
	sensKeySecret          = "secret"
	sensKeyPassword        = "password"
	sensKeyToken           = "token"
	sensKeySigningKey      = "signing_key"
	sensKeyDSN             = "dsn"
	sensKeySecretAccessKey = "secret_access_key"
	sensKeyClientSecret    = "client_secret"
	sensKeyAPIKey          = "api_key"
	sensKeyCredential      = "credential"
)

// sensitiveKeys contains key name patterns that should be redacted.
// isSensitiveKey uses exact case-insensitive matching against this list,
// so every distinct field name that may carry a secret must appear here
// in its canonical (lowercase) form.
//
// MUST be a superset of pkg/platform/fieldcrypt.go::sensitiveConfigKeys
// — any field the encryption layer treats as a secret must also be
// redacted here, or a misconfigured deployment (ENCRYPTION_KEY unset,
// plaintext at rest) leaks through /api/admin/config.
// TestSensitiveKeysCoverFieldcryptSet locks this in.
var sensitiveKeys = []string{
	sensKeyKey,
	sensKeySecret,
	sensKeyPassword,
	sensKeyToken,
	sensKeySigningKey,
	sensKeyDSN,
	sensKeySecretAccessKey,
	sensKeyClientSecret,
	sensKeyAPIKey,
	sensKeyCredential,
	// From connection_handler.go's parallel set: these also need
	// redaction in the global config endpoint, not just the
	// connection endpoint, because Config.Toolkits map serialization
	// flows through redactMap.
	sensKeySecretKey,
	sensKeyAccessToken,
	sensKeyRefreshToken,
	sensKeyOAuthClientSecret,
	sensKeyOAuth2ClientSecret,
}

// configToMap converts a config struct to map[string]any via YAML round-trip.
func configToMap(v any) (map[string]any, error) {
	yamlBytes, err := yaml.Marshal(v)
	if err != nil {
		return nil, fmt.Errorf("marshaling config: %w", err)
	}
	var m map[string]any
	if err := yaml.Unmarshal(yamlBytes, &m); err != nil {
		return nil, fmt.Errorf("unmarshaling config: %w", err)
	}
	return m, nil
}

// getConfig handles GET /api/v1/admin/config.
//
// @Summary      Get config
// @Description  Returns the current configuration as JSON with sensitive values redacted.
// @Tags         Config
// @Produce      json
// @Success      200  {object}  map[string]any
// @Failure      500  {object}  problemDetail
// @Security     ApiKeyAuth
// @Security     BearerAuth
// @Router       /admin/config [get]
func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
	configMap, err := configToMap(h.deps.Config)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to process config")
		return
	}
	redactMap(configMap)
	writeJSON(w, http.StatusOK, configMap)
}

// configModeResponse describes the current config store mode.
type configModeResponse struct {
	Mode     string `json:"mode" example:"database"`
	ReadOnly bool   `json:"read_only" example:"false"`
}

// configMode handles GET /api/v1/admin/config/mode.
//
// @Summary      Get config mode
// @Description  Returns the current config store mode (file or database).
// @Tags         Config
// @Produce      json
// @Success      200  {object}  configModeResponse
// @Security     ApiKeyAuth
// @Security     BearerAuth
// @Router       /admin/config/mode [get]
func (h *Handler) configMode(w http.ResponseWriter, _ *http.Request) {
	mode := configModeFile
	if h.deps.ConfigStore != nil {
		mode = h.deps.ConfigStore.Mode()
	}
	writeJSON(w, http.StatusOK, configModeResponse{
		Mode:     mode,
		ReadOnly: mode == configModeFile,
	})
}

// exportConfig handles GET /api/v1/admin/config/export.
//
// @Summary      Export config
// @Description  Returns the current configuration as downloadable YAML. Sensitive values are redacted by default.
// @Tags         Config
// @Produce      application/x-yaml
// @Param        secrets  query  string  false  "Set to 'true' to include sensitive values"
// @Success      200      {string}  string
// @Failure      500      {object}  problemDetail
// @Security     ApiKeyAuth
// @Security     BearerAuth
// @Router       /admin/config/export [get]
func (h *Handler) exportConfig(w http.ResponseWriter, r *http.Request) {
	if h.deps.Config == nil {
		writeError(w, http.StatusInternalServerError, "no config available")
		return
	}

	yamlBytes, err := yaml.Marshal(h.deps.Config)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to marshal config")
		return
	}

	// Redact by default unless ?secrets=true
	if r.URL.Query().Get("secrets") != "true" {
		yamlBytes, err = redactYAML(yamlBytes)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "failed to redact config")
			return
		}
	}

	w.Header().Set("Content-Type", "application/x-yaml")
	w.Header().Set("Content-Disposition", `attachment; filename="platform-config.yaml"`)
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(yamlBytes)
}

// --- Effective config (merged file defaults + DB overrides) ---

// effectiveConfigEntry is an entry with its source indicated.
type effectiveConfigEntry struct {
	Key       string  `json:"key" example:"server.description"`
	Value     string  `json:"value" example:"ACME Corp analytics platform"`
	Source    string  `json:"source" example:"database"` // "file" or "database"
	UpdatedBy *string `json:"updated_by,omitempty" example:"admin@example.com"`
	UpdatedAt *string `json:"updated_at,omitempty" example:"2026-01-15T14:30:00Z"`
}

// listEffectiveConfig handles GET /api/v1/admin/config/effective.
// Returns the merged view: for each whitelisted key, the DB override if present, otherwise the file default.
func (h *Handler) listEffectiveConfig(w http.ResponseWriter, r *http.Request) {
	// Get DB overrides.
	dbEntries, err := h.deps.ConfigStore.List(r.Context())
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to load config entries")
		return
	}
	dbMap := make(map[string]configstore.Entry)
	for _, e := range dbEntries {
		dbMap[e.Key] = e
	}

	// Build sorted list of whitelisted keys for deterministic order.
	keys := make([]string, 0, len(configEntryWhitelist))
	for k := range configEntryWhitelist {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	result := make([]effectiveConfigEntry, 0, len(keys))
	for _, key := range keys {
		if dbEntry, ok := dbMap[key]; ok {
			updatedAt := dbEntry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")
			result = append(result, effectiveConfigEntry{
				Key:       key,
				Value:     dbEntry.Value,
				Source:    configSourceDatabase,
				UpdatedBy: &dbEntry.UpdatedBy,
				UpdatedAt: &updatedAt,
			})
		} else if fileVal, ok := h.deps.FileDefaults[key]; ok {
			result = append(result, effectiveConfigEntry{
				Key:    key,
				Value:  fileVal,
				Source: configSourceFile,
			})
		}
	}

	writeJSON(w, http.StatusOK, result)
}

// --- Config Entry CRUD ---

// listConfigEntries handles GET /api/v1/admin/config/entries.
func (h *Handler) listConfigEntries(w http.ResponseWriter, r *http.Request) {
	entries, err := h.deps.ConfigStore.List(r.Context())
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to list config entries")
		return
	}
	if entries == nil {
		entries = []configstore.Entry{}
	}
	writeJSON(w, http.StatusOK, entries)
}

// getConfigEntry handles GET /api/v1/admin/config/entries/{key}.
func (h *Handler) getConfigEntry(w http.ResponseWriter, r *http.Request) {
	key := r.PathValue("key")
	entry, err := h.deps.ConfigStore.Get(r.Context(), key)
	if err != nil {
		if errors.Is(err, configstore.ErrNotFound) {
			writeError(w, http.StatusNotFound, "config entry not found")
			return
		}
		writeError(w, http.StatusInternalServerError, "failed to get config entry")
		return
	}
	writeJSON(w, http.StatusOK, entry)
}

// setConfigEntryRequest is the body for PUT /api/v1/admin/config/entries/{key}.
type setConfigEntryRequest struct {
	Value string `json:"value" example:"ACME Corp analytics platform"`
}

// setConfigEntry handles PUT /api/v1/admin/config/entries/{key}.
func (h *Handler) setConfigEntry(w http.ResponseWriter, r *http.Request) {
	key := r.PathValue("key")
	if !h.isConfigKeyAllowed(key) {
		writeError(w, http.StatusBadRequest, fmt.Sprintf("key %q is not editable", key))
		return
	}

	var req setConfigEntryRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	author := ""
	if user := GetUser(r.Context()); user != nil {
		author = user.Email
		if author == "" {
			author = user.UserID
		}
	}

	if err := h.deps.ConfigStore.Set(r.Context(), key, req.Value, author); err != nil {
		writeError(w, http.StatusInternalServerError, "failed to save config entry")
		return
	}

	// Hot-reload: apply to live config.
	h.deps.Config.ApplyConfigEntry(key, req.Value)

	// Return the stored entry.
	entry, err := h.deps.ConfigStore.Get(r.Context(), key)
	if err != nil {
		writeJSON(w, http.StatusOK, configstore.Entry{Key: key, Value: req.Value})
		return
	}
	writeJSON(w, http.StatusOK, entry)
}

// deleteConfigEntry handles DELETE /api/v1/admin/config/entries/{key}.
func (h *Handler) deleteConfigEntry(w http.ResponseWriter, r *http.Request) {
	key := r.PathValue("key")
	if !h.isConfigKeyAllowed(key) {
		writeError(w, http.StatusBadRequest, fmt.Sprintf("key %q is not editable", key))
		return
	}

	author := ""
	if user := GetUser(r.Context()); user != nil {
		author = user.Email
		if author == "" {
			author = user.UserID
		}
	}

	if err := h.deps.ConfigStore.Delete(r.Context(), key, author); err != nil {
		if errors.Is(err, configstore.ErrNotFound) {
			writeError(w, http.StatusNotFound, "config entry not found")
			return
		}
		writeError(w, http.StatusInternalServerError, "failed to delete config entry")
		return
	}

	// Revert to file default if one exists; otherwise clear the live
	// override (an empty value tells ApplyConfigEntry to remove it).
	if fileVal, ok := h.deps.FileDefaults[key]; ok {
		h.deps.Config.ApplyConfigEntry(key, fileVal)
	} else {
		h.deps.Config.ApplyConfigEntry(key, "")
	}

	w.WriteHeader(http.StatusNoContent)
}

// getConfigChangelog handles GET /api/v1/admin/config/changelog.
func (h *Handler) getConfigChangelog(w http.ResponseWriter, r *http.Request) {
	entries, err := h.deps.ConfigStore.Changelog(r.Context(), defaultChangelogLimit)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to load config changelog")
		return
	}
	if entries == nil {
		entries = []configstore.ChangelogEntry{}
	}
	writeJSON(w, http.StatusOK, entries)
}

// --- Redaction helpers ---

// redactYAML marshals YAML, redacts sensitive values, then re-marshals.
func redactYAML(data []byte) ([]byte, error) {
	var m map[string]any
	if err := yaml.Unmarshal(data, &m); err != nil {
		return nil, fmt.Errorf("unmarshaling for redaction: %w", err)
	}
	redactMap(m)
	out, err := yaml.Marshal(m)
	if err != nil {
		return nil, fmt.Errorf("marshaling redacted config: %w", err)
	}
	return out, nil
}

// redactMap recursively walks the map and replaces sensitive values with "[REDACTED]".
func redactMap(m map[string]any) {
	for k, v := range m {
		if isSensitiveKey(k) {
			if s, ok := v.(string); ok && s != "" {
				m[k] = redactedValue
			}
			continue
		}
		switch val := v.(type) {
		case map[string]any:
			redactMap(val)
		case []any:
			redactSlice(val)
		}
	}
}

// redactSlice walks a slice and redacts any maps found within.
func redactSlice(s []any) {
	for _, item := range s {
		if m, ok := item.(map[string]any); ok {
			redactMap(m)
		}
	}
}

// isSensitiveKey checks if a key matches any sensitive pattern.
func isSensitiveKey(key string) bool {
	return slices.Contains(sensitiveKeys, strings.ToLower(key))
}
