package service

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"

	"wucher/internal/config"
	mastersettings "wucher/internal/domain/master_settings"
)

type MasterSettingsService struct {
	repo      mastersettings.Repository
	protector SecretProtector
}

type MasterSettingsServiceDependencies struct {
	Protector SecretProtector
}

func NewMasterSettingsService(repo mastersettings.Repository, deps MasterSettingsServiceDependencies) *MasterSettingsService {
	return &MasterSettingsService{
		repo:      repo,
		protector: deps.Protector,
	}
}

func (s *MasterSettingsService) SetSecretProtector(protector SecretProtector) *MasterSettingsService {
	if s == nil {
		return nil
	}
	s.protector = protector
	return s
}

func (s *MasterSettingsService) Create(ctx context.Context, row *mastersettings.MasterSettings) error {
	return s.repo.Create(ctx, row)
}

func (s *MasterSettingsService) Update(ctx context.Context, row *mastersettings.MasterSettings) error {
	return s.repo.Update(ctx, row)
}

func (s *MasterSettingsService) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
	return s.repo.Delete(ctx, id, deletedBy)
}

func (s *MasterSettingsService) GetByID(ctx context.Context, id []byte) (*mastersettings.MasterSettings, error) {
	return s.repo.GetByID(ctx, id)
}

func (s *MasterSettingsService) List(ctx context.Context, filter, sort string, limit, offset int) ([]mastersettings.MasterSettings, int64, error) {
	return s.repo.List(ctx, filter, normalizeMasterSettingsSort(sort), limit, offset)
}

func (s *MasterSettingsService) UpsertMicrosoftEntraConfig(
	ctx context.Context,
	input mastersettings.MicrosoftEntraConfigInput,
	actor []byte,
) (*mastersettings.MicrosoftEntraConfigView, error) {
	normalized, err := normalizeMicrosoftEntraConfigInput(input)
	if err != nil {
		return nil, err
	}

	rows := []mastersettings.MasterSettingValue{
		{
			SettingKey:  mastersettings.SettingMicrosoftEntraTenantID,
			Value:       normalized.TenantID,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: false,
		},
		{
			SettingKey:  mastersettings.SettingMicrosoftEntraClientID,
			Value:       normalized.ClientID,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: false,
		},
		{
			SettingKey:  mastersettings.SettingMicrosoftEntraRedirectURL,
			Value:       normalized.RedirectURL,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: false,
		},
		{
			SettingKey:  mastersettings.SettingMicrosoftEntraAuthority,
			Value:       normalized.Authority,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: false,
		},
		{
			SettingKey:  mastersettings.SettingMicrosoftEntraScopes,
			Value:       normalized.Scopes,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: false,
		},
	}
	if normalized.ClientSecret != "" {
		if s.protector == nil {
			return nil, errors.New("secret protector not configured")
		}
		encryptedSecret, err := s.protector.Encrypt([]byte(normalized.ClientSecret))
		if err != nil {
			return nil, fmt.Errorf("encrypt MS_ENTRA_CLIENT_SECRET: %w", err)
		}
		encodedSecret := base64.StdEncoding.EncodeToString(encryptedSecret)
		rows = append(rows, mastersettings.MasterSettingValue{
			SettingKey:  mastersettings.SettingMicrosoftEntraClientSecret,
			Value:       encodedSecret,
			CreatedBy:   actor,
			UpdatedBy:   actor,
			IsEncrypted: true,
		})
	} else {
		existingSecretRows, err := s.repo.GetSettingValuesByKeys(ctx, []string{mastersettings.SettingMicrosoftEntraClientSecret})
		if err != nil {
			return nil, err
		}
		_, _, existingSecret, _, _, _, _ := extractMicrosoftEntraValues(existingSecretRows)
		if strings.TrimSpace(existingSecret) == "" {
			return nil, errors.New("MS_ENTRA_CLIENT_SECRET is required for initial setup")
		}
	}
	if err := s.repo.UpsertSettingValues(ctx, rows); err != nil {
		return nil, err
	}
	return s.GetMicrosoftEntraConfig(ctx)
}

func (s *MasterSettingsService) GetMicrosoftEntraConfig(ctx context.Context) (*mastersettings.MicrosoftEntraConfigView, error) {
	rows, err := s.repo.GetSettingValuesByKeys(ctx, mastersettings.MicrosoftEntraSettingKeys)
	if err != nil {
		return nil, err
	}
	tenant, clientID, secret, redirectURL, authority, scopes, encrypted := extractMicrosoftEntraValues(rows)
	// UI view supports partial config so non-sensitive bootstrapped keys can be rendered.
	// Runtime login flow still uses LoadMicrosoftEntraRuntimeConfig, which remains strict.
	if redirectURL == "" || authority == "" || scopes == "" {
		return nil, nil
	}

	secretMasked := ""
	if secret != "" && encrypted {
		secretMasked = "********"
	} else if secret != "" {
		secretMasked = maskMicrosoftSecret(secret)
	}

	return &mastersettings.MicrosoftEntraConfigView{
		TenantID:           tenant,
		ClientID:           clientID,
		ClientSecretMasked: secretMasked,
		RedirectURL:        redirectURL,
		Authority:          authority,
		Scopes:             scopes,
	}, nil
}

func (s *MasterSettingsService) LoadMicrosoftEntraRuntimeConfig(ctx context.Context) (config.MicrosoftSSOConfig, error) {
	rows, err := s.repo.GetSettingValuesByKeys(ctx, mastersettings.MicrosoftEntraSettingKeys)
	if err != nil {
		return config.MicrosoftSSOConfig{}, err
	}
	tenant, clientID, secret, redirectURL, authority, scopes, encrypted := extractMicrosoftEntraValues(rows)
	if tenant == "" || clientID == "" || secret == "" || redirectURL == "" || authority == "" || scopes == "" {
		return config.MicrosoftSSOConfig{}, errors.New("sso not configured")
	}

	plainSecret := secret
	if encrypted {
		if s.protector == nil {
			return config.MicrosoftSSOConfig{}, errors.New("secret protector not configured")
		}
		cipherBytes, err := base64.StdEncoding.DecodeString(secret)
		if err != nil {
			return config.MicrosoftSSOConfig{}, errors.New("invalid encrypted MS_ENTRA_CLIENT_SECRET")
		}
		plainBytes, err := s.protector.Decrypt(cipherBytes)
		if err != nil {
			return config.MicrosoftSSOConfig{}, errors.New("failed to decrypt MS_ENTRA_CLIENT_SECRET")
		}
		plainSecret = strings.TrimSpace(string(plainBytes))
	}
	if plainSecret == "" {
		return config.MicrosoftSSOConfig{}, errors.New("sso not configured")
	}

	return config.MicrosoftSSOConfig{
		TenantID:     tenant,
		ClientID:     clientID,
		ClientSecret: plainSecret,
		RedirectURL:  redirectURL,
		Authority:    authority,
		Scopes:       splitCSV(scopes),
	}, nil
}

func (s *MasterSettingsService) IsLoginEmailOTPEnabled(ctx context.Context) (bool, error) {
	if s == nil || s.repo == nil {
		return true, nil
	}
	rows, err := s.repo.GetSettingValuesByKeys(ctx, []string{mastersettings.SettingAuthLoginEmailOTPEnabled})
	if err != nil {
		return true, err
	}
	if len(rows) == 0 {
		return true, nil
	}
	raw := strings.TrimSpace(rows[0].Value)
	if raw == "" {
		return true, nil
	}
	enabled, parseErr := strconv.ParseBool(raw)
	if parseErr != nil {
		return true, nil
	}
	return enabled, nil
}

// SeedMicrosoftEntraConfigIfMissing backfills missing non-sensitive SSO setting keys from app config.
// Existing non-empty values are preserved.
func (s *MasterSettingsService) SeedMicrosoftEntraConfigIfMissing(
	ctx context.Context,
	cfg config.MicrosoftSSOConfig,
) error {
	if s == nil || s.repo == nil {
		return nil
	}

	rows, err := s.repo.GetSettingValuesByKeys(ctx, mastersettings.MicrosoftEntraSettingKeys)
	if err != nil {
		return err
	}
	existing := make(map[string]mastersettings.MasterSettingValue, len(rows))
	for i := range rows {
		key := strings.TrimSpace(rows[i].SettingKey)
		if key == "" {
			continue
		}
		existing[key] = rows[i]
	}
	isMissing := func(key string) bool {
		row, ok := existing[key]
		if !ok {
			return true
		}
		return strings.TrimSpace(row.Value) == ""
	}

	normalized, err := normalizeMicrosoftEntraConfigInput(mastersettings.MicrosoftEntraConfigInput{
		// Only validate and seed non-sensitive fields.
		// Tenant ID, Client ID, and Client Secret are intentionally excluded.
		TenantID:     "placeholder",
		ClientID:     "placeholder",
		ClientSecret: "placeholder",
		RedirectURL:  cfg.RedirectURL,
		Authority:    cfg.Authority,
		Scopes:       strings.Join(cfg.Scopes, ","),
	})
	if err != nil {
		return err
	}

	insertRows := make([]mastersettings.MasterSettingValue, 0, 3)
	if isMissing(mastersettings.SettingMicrosoftEntraRedirectURL) {
		insertRows = append(insertRows, mastersettings.MasterSettingValue{
			SettingKey:  mastersettings.SettingMicrosoftEntraRedirectURL,
			Value:       normalized.RedirectURL,
			IsEncrypted: false,
		})
	}
	if isMissing(mastersettings.SettingMicrosoftEntraAuthority) {
		insertRows = append(insertRows, mastersettings.MasterSettingValue{
			SettingKey:  mastersettings.SettingMicrosoftEntraAuthority,
			Value:       normalized.Authority,
			IsEncrypted: false,
		})
	}
	if isMissing(mastersettings.SettingMicrosoftEntraScopes) {
		insertRows = append(insertRows, mastersettings.MasterSettingValue{
			SettingKey:  mastersettings.SettingMicrosoftEntraScopes,
			Value:       normalized.Scopes,
			IsEncrypted: false,
		})
	}

	if len(insertRows) == 0 {
		return nil
	}
	return s.repo.UpsertSettingValues(ctx, insertRows)
}

func normalizeMasterSettingsSort(sort string) string {
	return normalizeSortByRules(sort,
		sortField("company_name"),
		sortField("title"),
		sortField("subtitle"),
		sortField("logo_url"),
		sortField("logo_file_id", "logo_file_uuid"),
		sortField("created_at"),
		sortField("updated_at"),
	)
}

func normalizeMicrosoftEntraConfigInput(input mastersettings.MicrosoftEntraConfigInput) (mastersettings.MicrosoftEntraConfigInput, error) {
	normalized := mastersettings.MicrosoftEntraConfigInput{
		TenantID:     strings.TrimSpace(input.TenantID),
		ClientID:     strings.TrimSpace(input.ClientID),
		ClientSecret: strings.TrimSpace(input.ClientSecret),
		RedirectURL:  strings.TrimSpace(input.RedirectURL),
		Authority:    strings.TrimSpace(input.Authority),
		Scopes:       normalizeCSV(input.Scopes),
	}

	if normalized.TenantID == "" {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_TENANT_ID is required")
	}
	if normalized.ClientID == "" {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_CLIENT_ID is required")
	}
	if normalized.RedirectURL == "" {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_REDIRECT_URL is required")
	}
	if normalized.Authority == "" {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_AUTHORITY is required")
	}
	if normalized.Scopes == "" {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_SCOPES is required")
	}
	if !isValidHTTPURL(normalized.RedirectURL) {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_REDIRECT_URL must be a valid absolute URL")
	}
	if !isValidAuthorityURL(normalized.Authority) {
		return mastersettings.MicrosoftEntraConfigInput{}, errors.New("MS_ENTRA_AUTHORITY must be a valid absolute URL with scheme")
	}
	normalized.Authority = ensureTrailingSlash(normalized.Authority)

	return normalized, nil
}

func normalizeCSV(raw string) string {
	parts := strings.Split(raw, ",")
	items := make([]string, 0, len(parts))
	seen := make(map[string]struct{}, len(parts))
	for _, part := range parts {
		item := strings.TrimSpace(part)
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		items = append(items, item)
	}
	return strings.Join(items, ",")
}

func isValidHTTPURL(raw string) bool {
	u, err := url.Parse(raw)
	if err != nil {
		return false
	}
	if u == nil || !u.IsAbs() || strings.TrimSpace(u.Host) == "" {
		return false
	}
	switch strings.ToLower(strings.TrimSpace(u.Scheme)) {
	case "http", "https":
		return true
	default:
		return false
	}
}

func isValidAuthorityURL(raw string) bool {
	u, err := url.Parse(raw)
	if err != nil {
		return false
	}
	return u != nil && u.IsAbs() && strings.TrimSpace(u.Host) != "" && strings.TrimSpace(u.Scheme) != ""
}

func ensureTrailingSlash(raw string) string {
	if strings.HasSuffix(raw, "/") {
		return raw
	}
	return raw + "/"
}

func maskMicrosoftSecret(secret string) string {
	secret = strings.TrimSpace(secret)
	if secret == "" {
		return ""
	}
	if len(secret) <= 4 {
		return "****"
	}
	return strings.Repeat("*", len(secret)-4) + secret[len(secret)-4:]
}

func extractMicrosoftEntraValues(rows []mastersettings.MasterSettingValue) (tenant, clientID, secret, redirectURL, authority, scopes string, encrypted bool) {
	if len(rows) == 0 {
		return "", "", "", "", "", "", false
	}
	values := make(map[string]mastersettings.MasterSettingValue, len(rows))
	for i := range rows {
		values[rows[i].SettingKey] = rows[i]
	}
	tenant = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraTenantID].Value)
	clientID = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraClientID].Value)
	secret = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraClientSecret].Value)
	redirectURL = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraRedirectURL].Value)
	authority = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraAuthority].Value)
	scopes = strings.TrimSpace(values[mastersettings.SettingMicrosoftEntraScopes].Value)
	encrypted = values[mastersettings.SettingMicrosoftEntraClientSecret].IsEncrypted
	return tenant, clientID, secret, redirectURL, authority, scopes, encrypted
}

func splitCSV(raw string) []string {
	parts := strings.Split(raw, ",")
	items := make([]string, 0, len(parts))
	for _, part := range parts {
		item := strings.TrimSpace(part)
		if item == "" {
			continue
		}
		items = append(items, item)
	}
	return items
}
