package secrets

import (
	"context"
	"fmt"
	"log/slog"
	"path"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
	vault "github.com/openbao/openbao/api/v2"
)

type OpenBaoManager struct {
	client            *vault.Client
	mountPath         string
	logger            *slog.Logger
	connectionTimeout time.Duration
}

type OpenBaoManagerOpt func(*OpenBaoManager)

func WithMountPath(mountPath string) OpenBaoManagerOpt {
	return func(v *OpenBaoManager) {
		v.mountPath = mountPath
	}
}

func WithConnectionTimeout(timeout time.Duration) OpenBaoManagerOpt {
	return func(v *OpenBaoManager) {
		v.connectionTimeout = timeout
	}
}

// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy
// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200")
// The proxy handles all authentication automatically via Auto-Auth
func NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
	if proxyAddress == "" {
		return nil, fmt.Errorf("proxy address cannot be empty")
	}

	config := vault.DefaultConfig()
	config.Address = proxyAddress

	client, err := vault.NewClient(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create openbao client: %w", err)
	}

	manager := &OpenBaoManager{
		client:            client,
		mountPath:         "spindle", // default KV v2 mount path
		logger:            logger,
		connectionTimeout: 10 * time.Second, // default connection timeout
	}

	for _, opt := range opts {
		opt(manager)
	}

	if err := manager.testConnection(); err != nil {
		return nil, fmt.Errorf("failed to connect to bao proxy: %w", err)
	}

	logger.Info("successfully connected to bao proxy", "address", proxyAddress)
	return manager, nil
}

// testConnection verifies that we can connect to the proxy
func (v *OpenBaoManager) testConnection() error {
	ctx, cancel := context.WithTimeout(context.Background(), v.connectionTimeout)
	defer cancel()

	// try token self-lookup as a quick way to verify proxy works
	// and is authenticated
	_, err := v.client.Auth().Token().LookupSelfWithContext(ctx)
	if err != nil {
		return fmt.Errorf("proxy connection test failed: %w", err)
	}

	return nil
}

func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
	if err := ValidateKey(secret.Key); err != nil {
		return err
	}

	secretPath := v.buildSecretPath(secret.Repo, secret.Key)
	v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath)

	// Check if secret already exists
	existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
	if err == nil && existing != nil {
		v.logger.Debug("secret already exists", "path", secretPath)
		return ErrKeyAlreadyPresent
	}

	createdAt := secret.CreatedAt
	if createdAt.IsZero() {
		createdAt = time.Now()
	}
	secretData := map[string]interface{}{
		"value":      secret.Value,
		"repo":       string(secret.Repo),
		"key":        secret.Key,
		"created_at": createdAt.UTC().Format(time.RFC3339),
		"created_by": secret.CreatedBy.String(),
	}

	v.logger.Debug("writing secret to openbao", "path", secretPath, "mount", v.mountPath)
	resp, err := v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData)
	if err != nil {
		v.logger.Error("failed to write secret", "path", secretPath, "error", err)
		return fmt.Errorf("failed to store secret in openbao: %w", err)
	}

	v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime)

	v.logger.Debug("verifying secret was written", "path", secretPath)
	readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
	if err != nil {
		v.logger.Error("failed to verify secret after write", "path", secretPath, "error", err)
		return fmt.Errorf("secret not found after writing to %s/%s: %w", v.mountPath, secretPath, err)
	}

	if readBack == nil || readBack.Data == nil {
		v.logger.Error("secret verification returned empty data", "path", secretPath)
		return fmt.Errorf("secret verification failed: empty data returned for %s/%s", v.mountPath, secretPath)
	}

	v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version)
	return nil
}

func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
	secretPath := v.buildSecretPath(secret.Repo, secret.Key)

	// check if secret exists
	existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
	if err != nil || existing == nil {
		return ErrKeyNotFound
	}

	err = v.client.KVv2(v.mountPath).DeleteMetadata(ctx, secretPath)
	if err != nil {
		return fmt.Errorf("failed to delete secret from openbao: %w", err)
	}

	v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key)
	return nil
}

func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) {
	repoPath := v.buildRepoPath(repo)

	secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
	if err != nil {
		if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
			return []LockedSecret{}, nil
		}
		return nil, fmt.Errorf("failed to list secrets: %w", err)
	}

	if secretsList == nil || secretsList.Data == nil {
		return []LockedSecret{}, nil
	}

	keys, ok := secretsList.Data["keys"].([]interface{})
	if !ok {
		return []LockedSecret{}, nil
	}

	var secrets []LockedSecret

	for _, keyInterface := range keys {
		key, ok := keyInterface.(string)
		if !ok {
			continue
		}

		secretPath := fmt.Sprintf("%s/%s", repoPath, key)
		secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
		if err != nil {
			v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err)
			continue
		}

		if secretData == nil || secretData.Data == nil {
			continue
		}

		data := secretData.Data

		createdAtStr, ok := data["created_at"].(string)
		if !ok {
			createdAtStr = time.Now().Format(time.RFC3339)
		}

		createdAt, err := time.Parse(time.RFC3339, createdAtStr)
		if err != nil {
			createdAt = time.Now()
		}

		createdByStr, ok := data["created_by"].(string)
		if !ok {
			createdByStr = ""
		}

		keyStr, ok := data["key"].(string)
		if !ok {
			keyStr = key
		}

		secret := LockedSecret{
			Key:       keyStr,
			Repo:      repo,
			CreatedAt: createdAt,
			CreatedBy: syntax.DID(createdByStr),
		}

		secrets = append(secrets, secret)
	}

	v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets))
	return secrets, nil
}

func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) {
	repoPath := v.buildRepoPath(repo)

	secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
	if err != nil {
		if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
			return []UnlockedSecret{}, nil
		}
		return nil, fmt.Errorf("failed to list secrets: %w", err)
	}

	if secretsList == nil || secretsList.Data == nil {
		return []UnlockedSecret{}, nil
	}

	keys, ok := secretsList.Data["keys"].([]interface{})
	if !ok {
		return []UnlockedSecret{}, nil
	}

	var secrets []UnlockedSecret

	for _, keyInterface := range keys {
		key, ok := keyInterface.(string)
		if !ok {
			continue
		}

		secretPath := fmt.Sprintf("%s/%s", repoPath, key)
		secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
		if err != nil {
			v.logger.Warn("failed to read secret", "path", secretPath, "error", err)
			continue
		}

		if secretData == nil || secretData.Data == nil {
			continue
		}

		data := secretData.Data

		valueStr, ok := data["value"].(string)
		if !ok {
			v.logger.Warn("secret missing value", "path", secretPath)
			continue
		}

		createdAtStr, ok := data["created_at"].(string)
		if !ok {
			createdAtStr = time.Now().Format(time.RFC3339)
		}

		createdAt, err := time.Parse(time.RFC3339, createdAtStr)
		if err != nil {
			createdAt = time.Now()
		}

		createdByStr, ok := data["created_by"].(string)
		if !ok {
			createdByStr = ""
		}

		keyStr, ok := data["key"].(string)
		if !ok {
			keyStr = key
		}

		secret := UnlockedSecret{
			Key:       keyStr,
			Value:     valueStr,
			Repo:      repo,
			CreatedAt: createdAt,
			CreatedBy: syntax.DID(createdByStr),
		}

		secrets = append(secrets, secret)
	}

	v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets))
	return secrets, nil
}

// buildRepoPath creates a safe path for a repository
func (v *OpenBaoManager) buildRepoPath(repo RepoIdentifier) string {
	// convert RepoIdentifier to a safe path by replacing special characters
	repoPath := strings.ReplaceAll(string(repo), "/", "_")
	repoPath = strings.ReplaceAll(repoPath, ":", "_")
	repoPath = strings.ReplaceAll(repoPath, ".", "_")
	return fmt.Sprintf("repos/%s", repoPath)
}

// buildSecretPath creates a path for a specific secret
func (v *OpenBaoManager) buildSecretPath(repo RepoIdentifier, key string) string {
	return path.Join(v.buildRepoPath(repo), key)
}
