package server

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

	"github.com/mem9-ai/drive9/pkg/logger"
	"github.com/mem9-ai/drive9/pkg/metrics"
	"github.com/mem9-ai/drive9/pkg/tenant"
	"github.com/mem9-ai/drive9/pkg/vault"
)

// isVaultDuplicateEntryErr reports whether err is a TiDB/MySQL duplicate-key
// error (case-insensitive match on "Duplicate entry") or a SQLite unique
// violation. The previous lowercase-only check against "duplicate"/"unique"
// missed TiDB's capitalized "Duplicate entry" message and surfaced a 500
// instead of a 409.
func isVaultDuplicateEntryErr(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "duplicate entry") || strings.Contains(msg, "unique constraint failed")
}

// handleVault dispatches /v1/vault/* requests.
func (s *Server) handleVault(w http.ResponseWriter, r *http.Request) {
	rest := strings.TrimPrefix(r.URL.Path, "/v1/vault/")

	// Management API: /v1/vault/secrets, /v1/vault/tokens, /v1/vault/audit
	// Authenticated by tenant token (existing auth middleware).
	if strings.HasPrefix(rest, "secrets") {
		s.handleVaultSecrets(w, r, strings.TrimPrefix(rest, "secrets"))
		return
	}
	if strings.HasPrefix(rest, "tokens") {
		s.handleVaultTokens(w, r, strings.TrimPrefix(rest, "tokens"))
		return
	}
	if rest == "grants" || strings.HasPrefix(rest, "grants/") {
		s.handleVaultGrants(w, r, strings.TrimPrefix(rest, "grants"))
		return
	}
	if rest == "audit" || strings.HasPrefix(rest, "audit") {
		s.handleVaultAudit(w, r)
		return
	}

	// /v1/vault/read/* is routed separately via capabilityAuthMiddleware.
	errJSON(w, http.StatusNotFound, "not found")
}

var errVaultUnsupported = errors.New("vault is not supported for this provider")

func recordVaultOperation(ctx context.Context, tenantID, op string, start time.Time, result string) {
	scopeTenantID, _, _, tidbCloudOrgID := requestMetricScope(ctx)
	if scopeTenantID != tenantID {
		tidbCloudOrgID = ""
	}
	metrics.RecordTenantOperationWithOrg(tenantID, tidbCloudOrgID, "vault", op, result, time.Since(start))
}

// vaultStore returns the vault store for the current tenant.
// Vault is only supported on TiDB providers; db9 tenants get a clear error.
func (s *Server) vaultStore(r *http.Request) (*vault.Store, error) {
	scope := ScopeFromContext(r.Context())
	if scope == nil || scope.Backend == nil {
		return nil, fmt.Errorf("no tenant scope")
	}
	if scope.Provider == tenant.ProviderDB9 {
		return nil, errVaultUnsupported
	}
	if s.vaultMK == nil {
		return nil, fmt.Errorf("vault master key not configured")
	}
	return vault.NewStoreScoped(scope.Backend.Store().DB(), s.vaultMK, scope.Backend.Store().Scope()), nil
}

// ---- Management API: /v1/vault/secrets ----

func (s *Server) handleVaultSecrets(w http.ResponseWriter, r *http.Request, sub string) {
	vs, err := s.vaultStore(r)
	if err != nil {
		if errors.Is(err, errVaultUnsupported) {
			errJSON(w, http.StatusNotImplemented, err.Error())
			return
		}
		logger.Error(r.Context(), "vault_secrets_store_failed", eventFields(r.Context(), "vault_secrets_store_failed", "error", err)...)
		writeBackendError(w, r, err)
		return
	}
	scope := ScopeFromContext(r.Context())
	tenantID := scope.TenantID

	// /v1/vault/secrets (no name)
	if sub == "" || sub == "/" {
		switch r.Method {
		case http.MethodPost:
			s.handleVaultSecretCreate(w, r, vs, tenantID)
		case http.MethodGet:
			s.handleVaultSecretList(w, r, vs, tenantID)
		default:
			errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
		}
		return
	}

	// /v1/vault/secrets/{name}[/value[/{field}]]
	name := strings.TrimPrefix(sub, "/")

	// Owner-read: /v1/vault/secrets/{name}/value[/{field}]
	if i := strings.Index(name, "/value"); i >= 0 {
		secretName := name[:i]
		rest := name[i+len("/value"):]
		if r.Method != http.MethodGet {
			errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
			return
		}
		if rest == "" {
			s.handleVaultSecretReadValue(w, r, vs, tenantID, secretName)
		} else {
			fieldName := strings.TrimPrefix(rest, "/")
			if fieldName == "" {
				errJSON(w, http.StatusBadRequest, "field name required after /value/")
				return
			}
			s.handleVaultSecretReadField(w, r, vs, tenantID, secretName, fieldName)
		}
		return
	}

	switch r.Method {
	case http.MethodGet:
		s.handleVaultSecretGet(w, r, vs, tenantID, name)
	case http.MethodPut:
		s.handleVaultSecretUpdate(w, r, vs, tenantID, name)
	case http.MethodDelete:
		s.handleVaultSecretDelete(w, r, vs, tenantID, name)
	default:
		errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
	}
}

func (s *Server) handleVaultSecretCreate(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_create", start, result) }()

	var req struct {
		Name       string            `json:"name"`
		SecretType string            `json:"secret_type"`
		Fields     map[string]string `json:"fields"`
		CreatedBy  string            `json:"created_by"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	if req.Name == "" {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "name is required")
		return
	}
	if len(req.Fields) == 0 {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "fields are required")
		return
	}
	if req.CreatedBy == "" {
		req.CreatedBy = "api"
	}
	secretType := vault.SecretType(req.SecretType)
	if secretType == "" {
		secretType = vault.SecretTypeGeneric
	}

	fields := make(map[string][]byte, len(req.Fields))
	for k, v := range req.Fields {
		fields[k] = []byte(v)
	}

	sec, err := vs.CreateSecret(r.Context(), tenantID, req.Name, req.CreatedBy, secretType, fields)
	if err != nil {
		if isVaultDuplicateEntryErr(err) {
			result = "already_exists"
			errJSON(w, http.StatusConflict, "secret already exists")
			return
		}
		result = "error"
		logger.Error(r.Context(), "vault_secret_create_failed",
			eventFields(r.Context(), "vault_secret_create_failed",
				"tenant_id", tenantID,
				"secret_name", req.Name,
				"error", err.Error())...)
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to create secret")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   tenantID,
		EventType:  "secret.created",
		SecretName: req.Name,
	})

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(sec)
}

func (s *Server) handleVaultSecretList(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_list", start, result) }()

	secrets, err := vs.ListSecrets(r.Context(), tenantID)
	if err != nil {
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to list secrets")
		return
	}
	if secrets == nil {
		secrets = []*vault.Secret{}
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"secrets": secrets})
}

func (s *Server) handleVaultSecretGet(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, name string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_get", start, result) }()

	sec, err := vs.GetSecret(r.Context(), tenantID, name)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "secret not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to get secret")
		return
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(sec)
}

// handleVaultSecretReadValue returns all decrypted fields for a secret (owner-read path).
// Response shape matches handleVaultReadSecret (Invariant #9: response schema parity).
func (s *Server) handleVaultSecretReadValue(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, secretName string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_read_value", start, result) }()

	fields, err := vs.ReadSecretFields(r.Context(), tenantID, secretName)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to read secret")
		return
	}

	scope := ScopeFromContext(r.Context())
	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   tenantID,
		EventType:  "secret.read",
		TokenID:    "owner:" + scope.APIKeyID,
		SecretName: secretName,
		Adapter:    "management-api",
	})

	format := r.URL.Query().Get("format")
	switch format {
	case "env":
		w.Header().Set("Content-Type", "text/plain")
		for k, v := range fields {
			_, _ = fmt.Fprintf(w, "%s=%s\n", strings.ToUpper(k), string(v))
		}
	case "json", "":
		payload := make(map[string]string, len(fields))
		for k, v := range fields {
			payload[k] = string(v)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(payload)
	default:
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "unsupported format")
	}
}

// handleVaultSecretReadField returns a single decrypted field (owner-read path).
// Response shape matches handleVaultReadField (Invariant #9: response schema parity).
func (s *Server) handleVaultSecretReadField(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, secretName, fieldName string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_read_field", start, result) }()

	plaintext, err := vs.ReadSecretField(r.Context(), tenantID, secretName, fieldName)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) || errors.Is(err, vault.ErrFieldNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to read field")
		return
	}

	scope := ScopeFromContext(r.Context())
	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   tenantID,
		EventType:  "secret.read",
		TokenID:    "owner:" + scope.APIKeyID,
		SecretName: secretName,
		FieldName:  fieldName,
		Adapter:    "management-api",
	})

	w.Header().Set("Content-Type", "text/plain")
	_, _ = w.Write(plaintext)
}

func (s *Server) handleVaultSecretUpdate(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, name string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_update", start, result) }()

	var req struct {
		Fields    map[string]string `json:"fields"`
		UpdatedBy string            `json:"updated_by"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	if len(req.Fields) == 0 {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "fields are required")
		return
	}
	if req.UpdatedBy == "" {
		req.UpdatedBy = "api"
	}

	fields := make(map[string][]byte, len(req.Fields))
	for k, v := range req.Fields {
		fields[k] = []byte(v)
	}

	sec, err := vs.UpdateSecret(r.Context(), tenantID, name, req.UpdatedBy, fields)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "secret not found")
			return
		}
		result = "error"
		logger.Error(r.Context(), "vault_secret_update_failed",
			eventFields(r.Context(), "vault_secret_update_failed",
				"tenant_id", tenantID,
				"secret_name", name,
				"error", err.Error())...)
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to update secret")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   tenantID,
		EventType:  "secret.rotated",
		SecretName: name,
	})

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(sec)
}

func (s *Server) handleVaultSecretDelete(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, name string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "secret_delete", start, result) }()

	err := vs.DeleteSecret(r.Context(), tenantID, name)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "secret not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to delete secret")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   tenantID,
		EventType:  "secret.deleted",
		SecretName: name,
	})

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// ---- Management API: /v1/vault/tokens ----

func (s *Server) handleVaultTokens(w http.ResponseWriter, r *http.Request, sub string) {
	vs, err := s.vaultStore(r)
	if err != nil {
		if errors.Is(err, errVaultUnsupported) {
			errJSON(w, http.StatusNotImplemented, err.Error())
			return
		}
		logger.Error(r.Context(), "vault_tokens_store_failed", eventFields(r.Context(), "vault_tokens_store_failed", "error", err)...)
		writeBackendError(w, r, err)
		return
	}
	scope := ScopeFromContext(r.Context())
	tenantID := scope.TenantID

	if sub == "" || sub == "/" {
		if r.Method == http.MethodPost {
			s.handleVaultTokenIssue(w, r, vs, tenantID)
			return
		}
		errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}

	// DELETE /v1/vault/tokens/{token_id}
	tokenID := strings.TrimPrefix(sub, "/")
	if r.Method == http.MethodDelete {
		s.handleVaultTokenRevoke(w, r, vs, tenantID, tokenID)
		return
	}
	errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
}

func (s *Server) handleVaultTokenIssue(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "token_issue", start, result) }()

	var req struct {
		AgentID string   `json:"agent_id"`
		TaskID  string   `json:"task_id"`
		Scope   []string `json:"scope"`
		TTLSecs int      `json:"ttl_seconds"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	if req.AgentID == "" {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "agent_id is required")
		return
	}
	if len(req.Scope) == 0 {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "scope is required")
		return
	}
	if err := vault.ValidateScope(req.Scope); err != nil {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, err.Error())
		return
	}
	ttl := time.Duration(req.TTLSecs) * time.Second
	if ttl <= 0 {
		ttl = time.Hour // default 1 hour
	}

	tokenStr, capToken, err := vs.IssueCapToken(r.Context(), tenantID, req.AgentID, req.TaskID, req.Scope, ttl)
	if err != nil {
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to issue token")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:  tenantID,
		EventType: "token.issued",
		TokenID:   capToken.TokenID,
		AgentID:   req.AgentID,
		TaskID:    req.TaskID,
	})

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"token":      tokenStr,
		"token_id":   capToken.TokenID,
		"expires_at": capToken.ExpiresAt,
	})
}

func (s *Server) handleVaultTokenRevoke(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, tokenID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "token_revoke", start, result) }()

	var req struct {
		RevokedBy string `json:"revoked_by"`
		Reason    string `json:"reason"`
	}
	// Body is optional for DELETE.
	_ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req)
	if req.RevokedBy == "" {
		req.RevokedBy = "api"
	}

	err := vs.RevokeCapToken(r.Context(), tenantID, tokenID, req.RevokedBy, req.Reason)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "token not found or already revoked")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to revoke token")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:  tenantID,
		EventType: "token.revoked",
		TokenID:   tokenID,
	})

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// ---- Management API: /v1/vault/grants ----
//
// Grants are the delegated grant token shape. They coexist with the legacy
// /v1/vault/tokens endpoint (additive); the legacy endpoint has a separate
// removal path.
//
// Owner-only enforcement is structural: this route is wired through
// tenantAuthMiddleware, which requires a tenant API key. Grant (vt_) JWTs do
// not satisfy tenantAuthMiddleware, so a delegated caller cannot reach this
// handler to mint further grants — re-delegation is blocked at the router
// layer, not here.

func (s *Server) handleVaultGrants(w http.ResponseWriter, r *http.Request, sub string) {
	vs, err := s.vaultStore(r)
	if err != nil {
		if errors.Is(err, errVaultUnsupported) {
			errJSON(w, http.StatusNotImplemented, err.Error())
			return
		}
		logger.Error(r.Context(), "vault_grants_store_failed", eventFields(r.Context(), "vault_grants_store_failed", "error", err)...)
		writeBackendError(w, r, err)
		return
	}
	scope := ScopeFromContext(r.Context())
	tenantID := scope.TenantID

	if sub == "" || sub == "/" {
		if r.Method == http.MethodPost {
			s.handleVaultGrantIssue(w, r, vs, tenantID)
			return
		}
		errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}

	grantID := strings.TrimPrefix(sub, "/")
	if r.Method == http.MethodDelete {
		s.handleVaultGrantRevoke(w, r, vs, tenantID, grantID)
		return
	}
	errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
}

func (s *Server) handleVaultGrantIssue(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "grant_issue", start, result) }()

	// Grants require a canonical server URL for the `iss` claim. If the
	// operator hasn't configured one, we cannot mint non-forgeable tokens:
	// fail closed rather than emit a grant without issuer binding.
	if s.vaultIssuerURL == "" {
		result = "unavailable"
		errJSON(w, http.StatusServiceUnavailable, "vault issuer URL not configured")
		return
	}
	var req struct {
		Agent     string   `json:"agent"`
		Scope     []string `json:"scope"`
		Perm      string   `json:"perm"`
		TTLSecs   int      `json:"ttl_seconds"`
		LabelHint string   `json:"label_hint"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	// principal_type is NOT caller-controlled. The end-state spec (§16 claim
	// table + §13.3 ctx-import contract) requires `vault grant` output to be
	// a delegated JWT; owner grants are a non-sequitur on this endpoint. By
	// fixing principal_type server-side we also get defense-in-depth against
	// a future router misconfiguration that lets a delegated caller reach
	// here (adv-2 Block B).
	principal := vault.PrincipalDelegated
	perm := vault.GrantPerm(req.Perm)
	if req.TTLSecs <= 0 {
		result = "invalid_argument"
		// Spec §6 "--ttl is required". Silent defaulting at the handler
		// would emit a grant the caller didn't ask for — audit/debug trap
		// (adv-1 Block 4, adv-2 Block B context).
		errJSON(w, http.StatusBadRequest, "ttl_seconds is required and must be > 0")
		return
	}
	ttl := time.Duration(req.TTLSecs) * time.Second

	tokenStr, grant, err := vs.IssueGrant(
		r.Context(), tenantID, s.vaultIssuerURL,
		principal, req.Agent, req.Scope, perm, ttl, req.LabelHint,
	)
	if err != nil {
		// IssueGrant returns user-visible validation errors (bad scope /
		// enum / agent / issuer); map them to 400. Internal DB errors
		// surface the same way but prefixed "insert grant:" — map to
		// backend error status.
		if strings.HasPrefix(err.Error(), "insert grant") {
			result = "error"
			errJSON(w, backendErrorStatus(r.Context(), err), "failed to issue grant")
			return
		}
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, err.Error())
		return
	}

	// Per impl spec §5: grant.issued Detail carries grant_id, agent,
	// principal_type, perm, scope so downstream audit tooling can
	// reconstruct the minted grant without re-reading the DB.
	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:  tenantID,
		EventType: "grant.issued",
		TokenID:   grant.GrantID,
		AgentID:   grant.Agent,
		Detail: map[string]any{
			"grant_id":       grant.GrantID,
			"agent":          grant.Agent,
			"principal_type": string(grant.PrincipalType),
			"perm":           string(grant.Perm),
			"scope":          grant.Scope,
		},
	})

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"token":      tokenStr,
		"grant_id":   grant.GrantID,
		"expires_at": grant.ExpiresAt,
		"scope":      grant.Scope,
		"perm":       grant.Perm,
	})
}

func (s *Server) handleVaultGrantRevoke(w http.ResponseWriter, r *http.Request, vs *vault.Store, tenantID, grantID string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), tenantID, "grant_revoke", start, result) }()

	var req struct {
		RevokedBy string `json:"revoked_by"`
		Reason    string `json:"reason"`
	}
	_ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req)
	if req.RevokedBy == "" {
		req.RevokedBy = "api"
	}

	err := vs.RevokeGrant(r.Context(), tenantID, grantID, req.RevokedBy, req.Reason)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "grant not found or already revoked")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to revoke grant")
		return
	}

	// Per impl spec §5: grant.revoked Detail carries grant_id, revoked_by,
	// reason. AgentID mirrors revoked_by so the top-level column reflects
	// "who did this" for filter queries.
	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:  tenantID,
		EventType: "grant.revoked",
		TokenID:   grantID,
		AgentID:   req.RevokedBy,
		Detail: map[string]any{
			"grant_id":   grantID,
			"revoked_by": req.RevokedBy,
			"reason":     req.Reason,
		},
	})

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// ---- Management API: /v1/vault/audit ----

func (s *Server) handleVaultAudit(w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	result := "ok"
	scope := ScopeFromContext(r.Context())
	if scope == nil {
		result = "error"
		errJSON(w, http.StatusForbidden, "missing tenant scope")
		return
	}
	auditTenantID := scope.TenantID
	defer func() { recordVaultOperation(r.Context(), auditTenantID, "audit_query", start, result) }()

	if r.Method != http.MethodGet {
		result = "invalid_argument"
		errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	vs, err := s.vaultStore(r)
	if err != nil {
		if errors.Is(err, errVaultUnsupported) {
			result = "unsupported"
			errJSON(w, http.StatusNotImplemented, err.Error())
			return
		}
		result = "error"
		logger.Error(r.Context(), "vault_audit_store_failed", eventFields(r.Context(), "vault_audit_store_failed", "error", err)...)
		writeBackendError(w, r, err)
		return
	}

	secretName := r.URL.Query().Get("secret")
	limit := 100
	if l := r.URL.Query().Get("limit"); l != "" {
		if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 1000 {
			limit = n
		}
	}

	events, err := vs.QueryAuditLog(r.Context(), auditTenantID, secretName, limit)
	if err != nil {
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to query audit log")
		return
	}
	if events == nil {
		events = []*vault.AuditEvent{}
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"events": events})
}

// ---- Consumption API: /v1/vault/read ----
// Authenticated by capability token or grant token (NOT tenant token).

// vaultReadPrincipal is the unified read-path identity extracted from either
// a legacy cap token (vault_ prefix) or a grant token (vt_ prefix). The
// handlers below operate on this struct, not on raw token types.
type vaultReadPrincipal struct {
	TenantID string
	TokenID  string   // grant_id or cap token_id — used for audit
	AgentID  string   // agent field from token
	TaskID   string   // legacy cap tokens only; empty for grants
	Scope    []string // secret scope
}

func (s *Server) handleVaultRead(w http.ResponseWriter, r *http.Request, sub string) {
	if r.Method != http.MethodGet {
		errJSON(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}

	// Extract capability token from Authorization header.
	raw := bearerToken(r)
	if raw == "" {
		errJSON(w, http.StatusUnauthorized, "missing capability token")
		return
	}

	// We need a tenant scope to get the DB. For the consumption API,
	// we extract tenant_id from the token itself, but we need the tenant's
	// backend to access the vault DB. Check if tenant scope is already set
	// (management auth middleware), otherwise we need the cap token's tenant_id.
	scope := ScopeFromContext(r.Context())
	if scope == nil || scope.Backend == nil {
		errJSON(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	tenantID := scope.TenantID

	if scope.Provider == tenant.ProviderDB9 {
		errJSON(w, http.StatusNotImplemented, "vault is not supported for this provider")
		return
	}
	if s.vaultMK == nil {
		errJSON(w, http.StatusInternalServerError, "vault master key not configured")
		return
	}

	vs := vault.NewStoreScoped(scope.Backend.Store().DB(), s.vaultMK, scope.Backend.Store().Scope())

	// Full verification: HMAC signature → TTL → DB revocation → claims.
	// IMPORTANT: Do NOT write audit events before verification succeeds.
	// The tenant_id comes from an unverified peek and could be forged;
	// writing to tenant audit before proving token authenticity would let
	// an attacker inject events into any tenant's audit log.
	principal, err := s.verifyVaultReadToken(r, vs, tenantID, raw)
	if err != nil {
		// Log to server-level observability only — not tenant audit.
		if strings.Contains(err.Error(), "expired") {
			errJSON(w, http.StatusUnauthorized, "token expired")
		} else if strings.Contains(err.Error(), "revoked") {
			errJSON(w, http.StatusUnauthorized, "token revoked")
		} else {
			errJSON(w, http.StatusUnauthorized, "invalid capability token")
		}
		return
	}

	// Parse path: /v1/vault/read, /v1/vault/read/{name}, /v1/vault/read/{name}/{field}
	sub = strings.TrimPrefix(sub, "/")
	if sub == "" {
		// Enumerate: list secrets in scope.
		s.handleVaultReadEnumerate(w, r, vs, principal)
		return
	}

	parts := strings.SplitN(sub, "/", 2)
	secretName := parts[0]
	fieldName := ""
	if len(parts) > 1 {
		fieldName = parts[1]
	}

	if fieldName != "" {
		s.handleVaultReadField(w, r, vs, principal, secretName, fieldName)
	} else {
		s.handleVaultReadSecret(w, r, vs, principal, secretName)
	}
}

// verifyVaultReadToken verifies a capability or grant token and returns a
// unified vaultReadPrincipal. Supports both legacy cap tokens (vault_ prefix)
// and grant tokens (vt_ prefix).
func (s *Server) verifyVaultReadToken(r *http.Request, vs *vault.Store, tenantID, raw string) (*vaultReadPrincipal, error) {
	if strings.HasPrefix(raw, "vt_") {
		return s.verifyGrantReadToken(r, vs, tenantID, raw)
	}
	return s.verifyCapReadToken(r, vs, tenantID, raw)
}

// verifyCapReadToken verifies a legacy cap token and maps to vaultReadPrincipal.
func (s *Server) verifyCapReadToken(r *http.Request, vs *vault.Store, tenantID, raw string) (*vaultReadPrincipal, error) {
	claims, err := vs.VerifyAndResolveCapToken(r.Context(), tenantID, raw)
	if err != nil {
		return nil, err
	}
	return &vaultReadPrincipal{
		TenantID: claims.TenantID,
		TokenID:  claims.TokenID,
		AgentID:  claims.AgentID,
		TaskID:   claims.TaskID,
		Scope:    claims.Scope,
	}, nil
}

// verifyGrantReadToken verifies a grant token and maps to vaultReadPrincipal.
// Grant tokens must have perm == "read" for the read path.
func (s *Server) verifyGrantReadToken(r *http.Request, vs *vault.Store, tenantID, raw string) (*vaultReadPrincipal, error) {
	claims, err := vs.VerifyAndResolveGrant(r.Context(), tenantID, s.vaultIssuerURL, raw)
	if err != nil {
		return nil, err
	}
	if claims.Perm != vault.GrantPermRead {
		return nil, fmt.Errorf("grant perm must be read")
	}
	return &vaultReadPrincipal{
		TenantID: claims.TenantID,
		TokenID:  claims.GrantID,
		AgentID:  claims.Agent,
		Scope:    claims.Scope,
	}, nil
}

func (s *Server) handleVaultReadEnumerate(w http.ResponseWriter, r *http.Request, vs *vault.Store, p *vaultReadPrincipal) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), p.TenantID, "read_enumerate", start, result) }()

	scopedNames := vault.ScopedSecretNames(p.Scope)
	// Filter to secrets that actually exist.
	var available []string
	for _, name := range scopedNames {
		_, err := vs.GetSecret(r.Context(), p.TenantID, name)
		if err == nil {
			available = append(available, name)
		}
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:  p.TenantID,
		EventType: "secret.list",
		TokenID:   p.TokenID,
		AgentID:   p.AgentID,
		TaskID:    p.TaskID,
		Adapter:   "api",
	})

	w.Header().Set("Content-Type", "application/json")
	if available == nil {
		available = []string{}
	}
	_ = json.NewEncoder(w).Encode(map[string]any{"secrets": available})
}

func (s *Server) handleVaultReadSecret(w http.ResponseWriter, r *http.Request, vs *vault.Store, p *vaultReadPrincipal, secretName string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), p.TenantID, "read_secret", start, result) }()

	// Scope check.
	allFields, allowedFields := vault.AllowedFields(p.Scope, secretName)
	if !allFields && len(allowedFields) == 0 {
		result = "out_of_scope"
		_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
			TenantID:   p.TenantID,
			EventType:  "secret.denied",
			TokenID:    p.TokenID,
			AgentID:    p.AgentID,
			SecretName: secretName,
			Detail:     map[string]string{"reason": "out_of_scope"},
		})
		// Return 404 to avoid leaking secret existence.
		errJSON(w, http.StatusNotFound, "not found")
		return
	}

	fields, err := vs.ReadSecretFields(r.Context(), p.TenantID, secretName)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to read secret")
		return
	}

	// Filter fields if scope is field-level.
	if !allFields {
		allowed := make(map[string]bool)
		for _, f := range allowedFields {
			allowed[f] = true
		}
		for k := range fields {
			if !allowed[k] {
				delete(fields, k)
			}
		}
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   p.TenantID,
		EventType:  "secret.read",
		TokenID:    p.TokenID,
		AgentID:    p.AgentID,
		TaskID:     p.TaskID,
		SecretName: secretName,
		Adapter:    "api",
	})

	format := r.URL.Query().Get("format")
	switch format {
	case "env":
		w.Header().Set("Content-Type", "text/plain")
		for k, v := range fields {
			_, _ = fmt.Fprintf(w, "%s=%s\n", strings.ToUpper(k), string(v))
		}
	case "json", "":
		payload := make(map[string]string, len(fields))
		for k, v := range fields {
			payload[k] = string(v)
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(payload)
	default:
		result = "invalid_argument"
		errJSON(w, http.StatusBadRequest, "unsupported format")
	}
}

func (s *Server) handleVaultReadField(w http.ResponseWriter, r *http.Request, vs *vault.Store, p *vaultReadPrincipal, secretName, fieldName string) {
	start := time.Now()
	result := "ok"
	defer func() { recordVaultOperation(r.Context(), p.TenantID, "read_field", start, result) }()

	// Scope check.
	if !vault.CheckScope(p.Scope, secretName, fieldName) {
		result = "out_of_scope"
		_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
			TenantID:   p.TenantID,
			EventType:  "secret.denied",
			TokenID:    p.TokenID,
			AgentID:    p.AgentID,
			SecretName: secretName,
			FieldName:  fieldName,
			Detail:     map[string]string{"reason": "out_of_scope"},
		})
		errJSON(w, http.StatusNotFound, "not found")
		return
	}

	plaintext, err := vs.ReadSecretField(r.Context(), p.TenantID, secretName, fieldName)
	if err != nil {
		if errors.Is(err, vault.ErrNotFound) || errors.Is(err, vault.ErrFieldNotFound) {
			result = "not_found"
			errJSON(w, http.StatusNotFound, "not found")
			return
		}
		result = "error"
		errJSON(w, backendErrorStatus(r.Context(), err), "failed to read field")
		return
	}

	_ = vs.WriteAuditEvent(r.Context(), &vault.AuditEvent{
		TenantID:   p.TenantID,
		EventType:  "secret.read",
		TokenID:    p.TokenID,
		AgentID:    p.AgentID,
		TaskID:     p.TaskID,
		SecretName: secretName,
		FieldName:  fieldName,
		Adapter:    "api",
	})

	w.Header().Set("Content-Type", "text/plain")
	_, _ = w.Write(plaintext)
}
