package extensionflow

import (
	"encoding/json"
	"fmt"

	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

// credentialKeyLength is the AES-256 key length required by the AES-GCM
// credential codec. The codec rejects any other length.
const credentialKeyLength = 32

// encryptCredentials AES-GCM encrypts a plaintext provider-credential object
// using the same key (AGENTTEAM_APP_CREDENTIALS_MASTER_KEY), scheme (aes-gcm-v1),
// and envelope format used by the rest of the server (internal/store/app). It
// fails closed when the credential key is absent or the wrong length: no
// plaintext credential is ever handed to the store unencrypted.
//
// The returned bytes are the opaque storage envelope
// {"_storage":"aes-gcm-v1","ciphertext":"..."}. An EMPTY credential object is
// STILL sealed under AES-GCM (the empty JSON object "{}" is encrypted, not
// stored as plaintext) so the AES-GCM-before-storage acceptance criterion holds
// for every Connect — the shared codec's empty-short-circuit is deliberately
// bypassed here because an Extension connection row with plaintext "{}" is an
// inconsistent storage posture (stage-5 round-1 hardening).
func (service *Service) encryptCredentials(credentials map[string]any) ([]byte, error) {
	if err := service.assertCredentialKey(); err != nil {
		return nil, err
	}
	if !app.HasCredentialStorage(service.credentialKey) {
		return nil, fmt.Errorf("extension credential encryption key is not configured (AGENTTEAM_APP_CREDENTIALS_MASTER_KEY must be a 32-byte key)")
	}
	if len(credentials) == 0 {
		return sealEmptyCredentialsEnvelope(service.credentialKey)
	}
	encrypted, err := app.MarshalCredentialsObject(service.credentialKey, credentials)
	if err != nil {
		return nil, fmt.Errorf("encrypt extension provider credentials: %w", err)
	}
	return encrypted, nil
}

// sealEmptyCredentialsEnvelope encrypts the empty JSON object "{}" under AES-GCM
// and wraps it in the standard credential envelope. The result decrypts back to
// an empty object via app.DecryptStoredCredentialsJSON, so it is transparent to
// the runtime while satisfying the "no plaintext credentials at rest" invariant.
func sealEmptyCredentialsEnvelope(key []byte) ([]byte, error) {
	ciphertext, err := app.EncryptCredential(key, "{}")
	if err != nil {
		return nil, fmt.Errorf("encrypt empty extension provider credentials: %w", err)
	}
	envelope := model.CredentialEnvelope{
		Storage:    model.CredentialStorageScheme,
		Ciphertext: ciphertext,
	}
	encoded, err := json.Marshal(envelope)
	if err != nil {
		return nil, fmt.Errorf("encode empty extension credential envelope: %w", err)
	}
	return encoded, nil
}

// assertCredentialKey fails closed when the configured key is not usable. It is
// the gate called before any encrypt/decrypt path so a misconfigured server can
// never silently store plaintext credentials.
func (service *Service) assertCredentialKey() error {
	if len(service.credentialKey) != credentialKeyLength {
		return fmt.Errorf("extension credential encryption key must be exactly %d bytes", credentialKeyLength)
	}
	if !app.HasCredentialStorage(service.credentialKey) {
		return fmt.Errorf("extension credential encryption key is not configured")
	}
	return nil
}

// decryptRuntimeCredentials requires the Extension-owned AES-GCM envelope.
// app.DecryptStoredCredentialsJSON intentionally accepts plaintext JSON for
// legacy App compatibility; the Extension runtime has no such compatibility
// seam because every Connect path has always sealed credentials. Accepting a
// plaintext row here would hide an at-rest policy violation.
func (service *Service) decryptRuntimeCredentials(stored []byte) ([]byte, error) {
	if err := service.assertCredentialKey(); err != nil {
		return nil, err
	}
	var envelope model.CredentialEnvelope
	if err := decodeStrictCredentialJSON(stored, &envelope); err != nil {
		return nil, fmt.Errorf("decode extension credential envelope: %w", err)
	}
	if envelope.Storage != model.CredentialStorageScheme ||
		envelope.Ciphertext == "" {
		return nil, fmt.Errorf("extension credential is not sealed with %s", model.CredentialStorageScheme)
	}
	plaintext, err := app.DecryptStoredCredentialsJSON(service.credentialKey, stored)
	if err != nil {
		return nil, fmt.Errorf("decrypt extension provider credential: %w", err)
	}
	return plaintext, nil
}
