// Package ldapconfig — TenantStore manages per-tenant LDAP/AD configuration
// stored in the tenant_ldap table. The bind password is encrypted with
// AES-256-GCM using a SHA-256 derived key, identical to the global Store.
package ldapconfig

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// TenantLDAPConfig is the persisted LDAP/AD configuration for a single tenant.
type TenantLDAPConfig struct {
	TenantID      int64          `json:"tenant_id"`
	Enabled       bool           `json:"enabled"`
	URL           string         `json:"url"`
	BindDN        string         `json:"bind_dn"`
	BindPassword  string         `json:"bind_password"`
	BaseDN        string         `json:"base_dn"`
	UserFilter    string         `json:"user_filter"`
	TLS           bool           `json:"tls"`
	TLSSkipVerify bool           `json:"tls_skip_verify"`
	DefaultRole   string         `json:"default_role"`
	GroupMappings []GroupMapping `json:"group_mappings"`
}

// TenantLDAPSummary is the abbreviated view returned by ListAll (for superadmin).
type TenantLDAPSummary struct {
	TenantID   int64  `json:"tenant_id"`
	TenantName string `json:"tenant_name"`
	Enabled    bool   `json:"enabled"`
	URL        string `json:"url"`
}

// TenantStore manages per-tenant LDAP configuration persistence.
type TenantStore struct {
	pool   *pgxpool.Pool
	encKey [32]byte
}

// NewTenantStore connects to PostgreSQL and returns a TenantStore.
// The tenant_ldap table is created by the tenantstore package schema migration.
// keyHex is the hex-encoded AES key (same as used for the global LDAP store).
func NewTenantStore(dsn, keyHex string) (*TenantStore, error) {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		return nil, fmt.Errorf("tenant ldap store: connect: %w", err)
	}
	key := sha256.Sum256([]byte(keyHex))
	return &TenantStore{pool: pool, encKey: key}, nil
}

// Close releases the underlying connection pool.
func (s *TenantStore) Close() {
	s.pool.Close()
}

// Get returns the LDAP configuration for a tenant with the bind password masked.
// Returns nil, nil when no configuration exists.
func (s *TenantStore) Get(ctx context.Context, tenantID int64) (*TenantLDAPConfig, error) {
	cfg, err := s.query(ctx, tenantID)
	if err != nil {
		return nil, err
	}
	if cfg == nil {
		return nil, nil
	}
	if cfg.BindPassword != "" {
		cfg.BindPassword = "\u2022\u2022\u2022\u2022\u2022\u2022"
	}
	return cfg, nil
}

// GetWithPassword returns the LDAP configuration including the decrypted bind password.
// Returns nil, nil when no configuration exists.
func (s *TenantStore) GetWithPassword(ctx context.Context, tenantID int64) (*TenantLDAPConfig, error) {
	return s.query(ctx, tenantID)
}

// Save upserts the LDAP configuration for a tenant.
// When bindPassword is empty the existing stored password is preserved.
func (s *TenantStore) Save(ctx context.Context, cfg TenantLDAPConfig, updatedBy string) error {
	mappingsJSON, err := json.Marshal(cfg.GroupMappings)
	if err != nil {
		return fmt.Errorf("tenant ldap store: marshal group_mappings: %w", err)
	}

	var encryptedPw []byte
	if cfg.BindPassword != "" {
		encryptedPw, err = s.encrypt(cfg.BindPassword)
		if err != nil {
			return fmt.Errorf("tenant ldap store: encrypt password: %w", err)
		}
	} else {
		// Preserve existing password.
		existing, qErr := s.query(ctx, cfg.TenantID)
		if qErr == nil && existing != nil && existing.BindPassword != "" {
			encryptedPw, err = s.encrypt(existing.BindPassword)
			if err != nil {
				return fmt.Errorf("tenant ldap store: re-encrypt existing password: %w", err)
			}
		}
	}

	_, err = s.pool.Exec(ctx, `
		INSERT INTO tenant_ldap
			(tenant_id, enabled, url, bind_dn, bind_password, base_dn, user_filter,
			 tls, tls_skip_verify, default_role, group_mappings)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
		ON CONFLICT (tenant_id) DO UPDATE SET
			enabled         = EXCLUDED.enabled,
			url             = EXCLUDED.url,
			bind_dn         = EXCLUDED.bind_dn,
			bind_password   = CASE WHEN EXCLUDED.bind_password IS NULL THEN tenant_ldap.bind_password ELSE EXCLUDED.bind_password END,
			base_dn         = EXCLUDED.base_dn,
			user_filter     = EXCLUDED.user_filter,
			tls             = EXCLUDED.tls,
			tls_skip_verify = EXCLUDED.tls_skip_verify,
			default_role    = EXCLUDED.default_role,
			group_mappings  = EXCLUDED.group_mappings
	`,
		cfg.TenantID, cfg.Enabled, cfg.URL, cfg.BindDN, encryptedPw, cfg.BaseDN,
		cfg.UserFilter, cfg.TLS, cfg.TLSSkipVerify, cfg.DefaultRole,
		string(mappingsJSON),
	)
	if err != nil {
		return fmt.Errorf("tenant ldap store: upsert: %w", err)
	}
	return nil
}

// Delete removes the LDAP configuration for a tenant.
func (s *TenantStore) Delete(ctx context.Context, tenantID int64) error {
	_, err := s.pool.Exec(ctx, `DELETE FROM tenant_ldap WHERE tenant_id = $1`, tenantID)
	return err
}

// ListAll returns a summary of all tenant LDAP configurations (for superadmin).
func (s *TenantStore) ListAll(ctx context.Context) ([]TenantLDAPSummary, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT tl.tenant_id, t.name, tl.enabled, tl.url
		FROM tenant_ldap tl
		JOIN tenants t ON t.id = tl.tenant_id
		ORDER BY tl.tenant_id
	`)
	if err != nil {
		return nil, fmt.Errorf("tenant ldap store: list all: %w", err)
	}
	defer rows.Close()

	var summaries []TenantLDAPSummary
	for rows.Next() {
		var s TenantLDAPSummary
		if err := rows.Scan(&s.TenantID, &s.TenantName, &s.Enabled, &s.URL); err != nil {
			return nil, fmt.Errorf("tenant ldap store: scan: %w", err)
		}
		summaries = append(summaries, s)
	}
	if summaries == nil {
		summaries = []TenantLDAPSummary{}
	}
	return summaries, rows.Err()
}

// query reads the tenant_ldap row for a tenant and decrypts the bind password.
func (s *TenantStore) query(ctx context.Context, tenantID int64) (*TenantLDAPConfig, error) {
	row := s.pool.QueryRow(ctx, `
		SELECT tenant_id, enabled, url, bind_dn, bind_password, base_dn, user_filter,
		       tls, tls_skip_verify, default_role, group_mappings
		FROM tenant_ldap WHERE tenant_id = $1
	`, tenantID)

	var cfg TenantLDAPConfig
	var encPw []byte
	var mappingsRaw []byte

	err := row.Scan(
		&cfg.TenantID, &cfg.Enabled, &cfg.URL, &cfg.BindDN, &encPw,
		&cfg.BaseDN, &cfg.UserFilter, &cfg.TLS, &cfg.TLSSkipVerify,
		&cfg.DefaultRole, &mappingsRaw,
	)
	if err == pgx.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("tenant ldap store: query: %w", err)
	}

	if len(encPw) > 0 {
		plain, err := s.decrypt(encPw)
		if err != nil {
			return nil, fmt.Errorf("tenant ldap store: decrypt password: %w", err)
		}
		cfg.BindPassword = plain
	}

	if len(mappingsRaw) > 0 {
		if err := json.Unmarshal(mappingsRaw, &cfg.GroupMappings); err != nil {
			return nil, fmt.Errorf("tenant ldap store: unmarshal group_mappings: %w", err)
		}
	}
	if cfg.GroupMappings == nil {
		cfg.GroupMappings = []GroupMapping{}
	}

	return &cfg, nil
}

// encrypt encrypts plaintext with AES-256-GCM using the store's key.
func (s *TenantStore) encrypt(plaintext string) ([]byte, error) {
	block, err := aes.NewCipher(s.encKey[:])
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// decrypt decrypts ciphertext produced by encrypt.
func (s *TenantStore) decrypt(ciphertext []byte) (string, error) {
	block, err := aes.NewCipher(s.encKey[:])
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	if len(ciphertext) < gcm.NonceSize() {
		return "", fmt.Errorf("tenant ldap store: ciphertext too short")
	}
	nonce, data := ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():]
	plain, err := gcm.Open(nil, nonce, data, nil)
	if err != nil {
		return "", err
	}
	return string(plain), nil
}
