package api

import (
	"errors"
	"log/slog"
	"net/http"
	"strings"

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

	"github.com/alexei/tinyforge/internal/crypto"
	"github.com/alexei/tinyforge/internal/store"
)

// sharedSecretRow is the JSON shape returned to clients. The secret value is
// NEVER returned — once stored it is write-only (mirroring workload_env). The
// has_value flag lets the UI show whether a value is set without exposing it;
// to rotate, the operator submits a new value.
type sharedSecretRow struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	HasValue    bool   `json:"has_value"`
	Encrypted   bool   `json:"encrypted"`
	Scope       string `json:"scope"`
	AppID       string `json:"app_id"`
	Description string `json:"description"`
	Enabled     bool   `json:"enabled"`
	CreatedAt   string `json:"created_at"`
	UpdatedAt   string `json:"updated_at"`
}

func toSharedSecretRow(sec store.SharedSecret) sharedSecretRow {
	return sharedSecretRow{
		ID:          sec.ID,
		Name:        sec.Name,
		HasValue:    sec.Value != "",
		Encrypted:   sec.Encrypted,
		Scope:       sec.Scope,
		AppID:       sec.AppID,
		Description: sec.Description,
		Enabled:     sec.Enabled,
		CreatedAt:   sec.CreatedAt,
		UpdatedAt:   sec.UpdatedAt,
	}
}

// listSharedSecrets handles GET /api/shared-secrets. Values are redacted.
func (s *Server) listSharedSecrets(w http.ResponseWriter, r *http.Request) {
	rows, err := s.store.ListSharedSecrets()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "list shared secrets")
		return
	}
	out := make([]sharedSecretRow, 0, len(rows))
	for _, sec := range rows {
		out = append(out, toSharedSecretRow(sec))
	}
	respondJSON(w, http.StatusOK, out)
}

// getSharedSecret handles GET /api/shared-secrets/{id}. Value is redacted.
func (s *Server) getSharedSecret(w http.ResponseWriter, r *http.Request) {
	sec, err := s.store.GetSharedSecret(chi.URLParam(r, "id"))
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "shared secret")
			return
		}
		respondError(w, http.StatusInternalServerError, "get shared secret")
		return
	}
	respondJSON(w, http.StatusOK, toSharedSecretRow(sec))
}

// createSharedSecretRequest is the POST body. Encrypted=true (the default for
// a non-empty value) causes the value to be encrypted at rest with the global
// key before it ever reaches the store.
type createSharedSecretRequest struct {
	Name        string `json:"name"`
	Value       string `json:"value"`
	Encrypted   *bool  `json:"encrypted"` // defaults true
	Scope       string `json:"scope"`     // global | app
	AppID       string `json:"app_id"`    // required when scope == app
	Description string `json:"description"`
	Enabled     *bool  `json:"enabled"` // defaults true
}

func (s *Server) createSharedSecret(w http.ResponseWriter, r *http.Request) {
	var req createSharedSecretRequest
	if !decodeJSONStrict(w, r, &req) {
		return
	}
	req.Name = strings.TrimSpace(req.Name)
	if !validEnvKey(req.Name) {
		respondError(w, http.StatusBadRequest, "name must be a valid env key [A-Za-z_][A-Za-z0-9_]*")
		return
	}
	if msg := validateSharedSecretScope(req.Scope, req.AppID); msg != "" {
		respondError(w, http.StatusBadRequest, msg)
		return
	}

	encrypted := true
	if req.Encrypted != nil {
		encrypted = *req.Encrypted
	}
	enabled := true
	if req.Enabled != nil {
		enabled = *req.Enabled
	}

	value, err := s.encryptSecretValue(req.Value, encrypted)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "encrypt value")
		return
	}

	sec, err := s.store.CreateSharedSecret(store.SharedSecret{
		Name:        req.Name,
		Value:       value,
		Encrypted:   encrypted,
		Scope:       req.Scope,
		AppID:       strings.TrimSpace(req.AppID),
		Description: req.Description,
		Enabled:     enabled,
	})
	if err != nil {
		if errors.Is(err, store.ErrUnique) {
			respondError(w, http.StatusConflict, "a shared secret with this scope and name already exists")
			return
		}
		respondError(w, http.StatusInternalServerError, "create shared secret")
		return
	}
	respondJSON(w, http.StatusCreated, toSharedSecretRow(sec))
}

// updateSharedSecretRequest is the PATCH body. Every field is optional; nil
// means "leave unchanged". A nil Value preserves the stored ciphertext (so a
// metadata-only edit can't accidentally blank a secret); a non-nil Value
// rotates it (re-encrypted under the effective Encrypted flag).
type updateSharedSecretRequest struct {
	Name        *string `json:"name"`
	Value       *string `json:"value"`
	Encrypted   *bool   `json:"encrypted"`
	Scope       *string `json:"scope"`
	AppID       *string `json:"app_id"`
	Description *string `json:"description"`
	Enabled     *bool   `json:"enabled"`
}

func (s *Server) updateSharedSecret(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	existing, err := s.store.GetSharedSecret(id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "shared secret")
			return
		}
		respondError(w, http.StatusInternalServerError, "get shared secret")
		return
	}

	var req updateSharedSecretRequest
	if !decodeJSONStrict(w, r, &req) {
		return
	}

	merged := existing
	if req.Name != nil {
		merged.Name = strings.TrimSpace(*req.Name)
		if !validEnvKey(merged.Name) {
			respondError(w, http.StatusBadRequest, "name must be a valid env key [A-Za-z_][A-Za-z0-9_]*")
			return
		}
	}
	if req.Encrypted != nil {
		merged.Encrypted = *req.Encrypted
	}
	if req.Scope != nil {
		merged.Scope = *req.Scope
	}
	if req.AppID != nil {
		merged.AppID = strings.TrimSpace(*req.AppID)
	}
	if req.Description != nil {
		merged.Description = *req.Description
	}
	if req.Enabled != nil {
		merged.Enabled = *req.Enabled
	}
	if msg := validateSharedSecretScope(merged.Scope, merged.AppID); msg != "" {
		respondError(w, http.StatusBadRequest, msg)
		return
	}

	// Value handling: only (re)encrypt when the caller supplied a new value.
	// Otherwise keep the stored ciphertext untouched — but if the Encrypted
	// flag flipped without a new value we cannot transcode the opaque stored
	// bytes, so reject that ambiguous request rather than corrupting the row.
	if req.Value != nil {
		v, encErr := s.encryptSecretValue(*req.Value, merged.Encrypted)
		if encErr != nil {
			respondError(w, http.StatusInternalServerError, "encrypt value")
			return
		}
		merged.Value = v
	} else if req.Encrypted != nil && *req.Encrypted != existing.Encrypted {
		respondError(w, http.StatusBadRequest, "changing 'encrypted' requires resubmitting 'value'")
		return
	}

	sec, err := s.store.UpdateSharedSecret(merged)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "shared secret")
			return
		}
		if errors.Is(err, store.ErrUnique) {
			respondError(w, http.StatusConflict, "a shared secret with this scope and name already exists")
			return
		}
		respondError(w, http.StatusInternalServerError, "update shared secret")
		return
	}
	respondJSON(w, http.StatusOK, toSharedSecretRow(sec))
}

func (s *Server) deleteSharedSecret(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if err := s.store.DeleteSharedSecret(id); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "shared secret")
			return
		}
		respondError(w, http.StatusInternalServerError, "delete shared secret")
		return
	}
	respondJSON(w, http.StatusOK, map[string]string{"deleted": id})
}

// encryptSecretValue encrypts value with the global key when encrypted is set
// and the value is non-empty; otherwise it returns the value unchanged. An
// empty value stays empty (no value set) regardless of the flag.
func (s *Server) encryptSecretValue(value string, encrypted bool) (string, error) {
	if !encrypted || value == "" {
		return value, nil
	}
	enc, err := crypto.Encrypt(s.encKey, value)
	if err != nil {
		slog.Error("encrypt shared secret value", "error", err)
		return "", err
	}
	return enc, nil
}

// validateSharedSecretScope returns a non-empty 400 message when the scope /
// app_id pairing is invalid; "" when valid. Mirrors the store-side invariant
// so the API rejects with a clear message before hitting the store.
func validateSharedSecretScope(scope, appID string) string {
	switch scope {
	case store.SharedSecretScopeGlobal:
		return ""
	case store.SharedSecretScopeApp:
		if strings.TrimSpace(appID) == "" {
			return "app_id is required when scope is 'app'"
		}
		return ""
	default:
		return "scope must be 'global' or 'app'"
	}
}
