package server

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"

	"github.com/operator/command-center/internal/secrets"
)

// Operator-facing administrative endpoints — secrets inventory and
// credential refresh, currently. These complement the existing
// /api/system/backup + /api/system/audit-log routes already wired in
// routes_phase15.go; this file holds the secrets surface specifically
// so the existing Polish/Phase15 plumbing stays unchanged.

// secretEntry is the JSON shape returned by GET /api/secrets/. Values
// are NEVER included — operator-facing inventory only.
type secretEntry struct {
	Key       string `json:"key"`
	UpdatedAt int64  `json:"updated_at"`
	Size      int    `json:"size"` // size of the encrypted blob, not plaintext (informational)
}

func (s *Server) handleListSecrets(w http.ResponseWriter, r *http.Request) {
	if s.sqlite == nil {
		http.Error(w, "secrets unavailable", http.StatusServiceUnavailable)
		return
	}
	rows, err := s.sqlite.QueryContext(r.Context(),
		`SELECT key, updated_at, length(encrypted_value) FROM secrets ORDER BY key`)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()
	out := []secretEntry{}
	for rows.Next() {
		var e secretEntry
		if err := rows.Scan(&e.Key, &e.UpdatedAt, &e.Size); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		out = append(out, e)
	}
	writeJSON(w, http.StatusOK, map[string]any{"count": len(out), "secrets": out})
}

// secretSetReq is the JSON body for PUT /api/secrets/{key}.
type secretSetReq struct {
	Value string `json:"value"`
}

// handleSetSecret writes a new value under the URL-path key. The same
// path covers all the previously-SSH-only credential refreshes:
//   - tracker:<id>:cookie  (matches cc-set-tracker-cookie's secret key)
//   - torrent_client:<id>:password  (matches cc-set-torrent-password)
//   - feed:<id>:url  (matches cc-set-feed-url)
//   - webhook:<id>:token  (matches webhook registry's secret key)
// and anything else the operator wants to drop in. No allow-list — the
// key is a path the operator already has to know.
func (s *Server) handleSetSecret(w http.ResponseWriter, r *http.Request) {
	key := chi.URLParam(r, "key")
	if key == "" {
		http.Error(w, "key required", http.StatusBadRequest)
		return
	}
	var req secretSetReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 64<<10)).Decode(&req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.Value == "" {
		http.Error(w, "value required (use DELETE to clear)", http.StatusBadRequest)
		return
	}
	if s.secretsWrite == nil {
		http.Error(w, "secrets writer not available", http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	if err := s.secretsWrite.Set(ctx, key, []byte(req.Value)); err != nil {
		http.Error(w, "secrets set: "+err.Error(), http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusAccepted, map[string]any{
		"ok": true, "key": key, "size": len(req.Value),
	})
}

func (s *Server) handleDeleteSecret(w http.ResponseWriter, r *http.Request) {
	key := chi.URLParam(r, "key")
	if key == "" {
		http.Error(w, "key required", http.StatusBadRequest)
		return
	}
	if s.secretsWrite == nil {
		http.Error(w, "secrets writer not available", http.StatusServiceUnavailable)
		return
	}
	if err := s.secretsWrite.Delete(r.Context(), key); err != nil {
		if errors.Is(err, secrets.ErrNotFound) {
			http.Error(w, "key not found", http.StatusNotFound)
			return
		}
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true, "key": key})
}

// SecretsWriter is what the server needs to mutate the secrets store —
// narrowed from *secrets.Store so the server can hold an interface
// rather than the concrete type (parallel to TrackerCookieFetcher).
type SecretsWriter interface {
	Set(ctx context.Context, key string, value []byte) error
	Delete(ctx context.Context, key string) error
}

// Helper used by callers that want to surface the well-known secret-key
// prefixes in the UI without having to remember the patterns.
func KnownSecretPrefixes() []string {
	return []string{
		"tracker:", "torrent_client:", "feed:", "webhook:", "auth:", "push:vapid:",
	}
}

// Used by the UI to label a key meaningfully ("Cookie for MAM tracker"
// instead of "tracker:mam:cookie"). Pure string parse, no I/O.
func DescribeSecretKey(key string) string {
	switch {
	case strings.HasPrefix(key, "tracker:") && strings.HasSuffix(key, ":cookie"):
		return "Tracker session cookie for " + strings.TrimSuffix(strings.TrimPrefix(key, "tracker:"), ":cookie")
	case strings.HasPrefix(key, "torrent_client:") && strings.HasSuffix(key, ":password"):
		return "qBit WebUI password for " + strings.TrimSuffix(strings.TrimPrefix(key, "torrent_client:"), ":password")
	case strings.HasPrefix(key, "feed:") && strings.HasSuffix(key, ":url"):
		return "Feed URL (with passkey) for " + strings.TrimSuffix(strings.TrimPrefix(key, "feed:"), ":url")
	case strings.HasPrefix(key, "webhook:") && strings.HasSuffix(key, ":token"):
		return "Webhook auth token for " + strings.TrimSuffix(strings.TrimPrefix(key, "webhook:"), ":token")
	case strings.HasPrefix(key, "auth:"):
		return "Auth internal: " + strings.TrimPrefix(key, "auth:")
	case strings.HasPrefix(key, "push:vapid:"):
		return "VAPID " + strings.TrimPrefix(key, "push:vapid:") + " key"
	default:
		return ""
	}
}
