package auth

import (
	"context"
	"crypto/rsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	jwt "github.com/golang-jwt/jwt/v5"
	"hyapp/pkg/appcode"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

const (
	// defaultFirebaseCertsURL 是 Firebase Secure Token 服务公开 x509 证书端点。
	defaultFirebaseCertsURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
	// defaultFirebaseCertTTL 是证书响应缺少 max-age 时的保守缓存时间。
	defaultFirebaseCertTTL = time.Hour
	// firebaseClockLeeway 是 Firebase token 时间校验允许的轻微客户端/服务端时钟偏差。
	firebaseClockLeeway = 30 * time.Second
	// maxFirebaseSubjectLength 跟 Firebase uid 约束保持一致，避免异常 sub 进入身份绑定表。
	maxFirebaseSubjectLength = 128
)

// FirebaseVerifierConfig 描述 Firebase ID token verifier 的最小运行配置。
type FirebaseVerifierConfig struct {
	// ProjectID 必须匹配 Firebase ID token 的 aud 和 issuer 尾部项目 ID。
	ProjectID string
	// AllowedSignInProviders 限制 Firebase token 内 firebase.sign_in_provider，首版只允许 google.com。
	AllowedSignInProviders []string
	// CertsURL 允许测试替换 Firebase 公钥端点；生产为空时使用官方默认端点。
	CertsURL string
	// HTTPClient 允许测试注入短超时 client；生产为空时使用默认超时 client。
	HTTPClient *http.Client
	// Now 允许测试固定 verifier 时钟。
	Now func() time.Time
}

// AppFirebaseVerifierConfig 保存多 App Firebase 项目的路由配置。
type AppFirebaseVerifierConfig struct {
	// Default 是旧版单 project_id 配置的兼容入口；未配置 app 专属项目时才使用。
	Default FirebaseVerifierConfig
	// Projects 按 app_code 持有 Firebase verifier 配置，避免 Huwaa token 被 Lalu 项目规则误拒。
	Projects map[string]FirebaseVerifierConfig
}

// FirebaseThirdPartyVerifier 校验 Firebase Auth ID token 并抽取稳定 Firebase uid。
type FirebaseThirdPartyVerifier struct {
	// projectID 是 Firebase project id，参与 aud/iss 双重校验。
	projectID string
	// issuer 是 Firebase Secure Token 固定 issuer 前缀加 project id。
	issuer string
	// allowedSignInProviders 是允许进入当前三方登录链路的 Firebase 登录方式。
	allowedSignInProviders map[string]bool
	// certsURL 是 Firebase Secure Token x509 证书端点。
	certsURL string
	// httpClient 拉取 Firebase 证书。
	httpClient *http.Client
	// now 提供可测试时钟。
	now func() time.Time
	// mu 保护证书缓存。
	mu sync.Mutex
	// cachedKeys 保存 kid 到 RSA 公钥的缓存。
	cachedKeys map[string]*rsa.PublicKey
	// cacheExpiresAt 是证书缓存失效时间。
	cacheExpiresAt time.Time
}

// AppFirebaseThirdPartyVerifier 按当前请求的 app_code 选择 Firebase 项目。
type AppFirebaseThirdPartyVerifier struct {
	// defaultVerifier 兼容历史单项目部署；有明确 app 项目配置时不会走到这里。
	defaultVerifier *FirebaseThirdPartyVerifier
	// verifiers 用归一化 app_code 路由到各自 Firebase verifier。
	verifiers map[string]*FirebaseThirdPartyVerifier
}

// firebaseIDTokenClaims 是 Firebase ID token 中本服务关心的字段集合。
type firebaseIDTokenClaims struct {
	jwt.RegisteredClaims
	// AuthTime 是 Firebase 用户最近完成认证的时间，必须存在。
	AuthTime *jwt.NumericDate `json:"auth_time,omitempty"`
	// Firebase 包含 Firebase 专有登录来源信息。
	Firebase struct {
		SignInProvider string `json:"sign_in_provider"`
	} `json:"firebase"`
}

// NewAppFirebaseThirdPartyVerifier 创建按 App 隔离的 Firebase verifier。
func NewAppFirebaseThirdPartyVerifier(config AppFirebaseVerifierConfig) *AppFirebaseThirdPartyVerifier {
	var defaultVerifier *FirebaseThirdPartyVerifier
	if strings.TrimSpace(config.Default.ProjectID) != "" {
		// 旧配置只有一个 project_id 时仍可启动；新 App 应优先写入 Projects 显式隔离。
		defaultVerifier = NewFirebaseThirdPartyVerifier(config.Default)
	}

	verifiers := make(map[string]*FirebaseThirdPartyVerifier, len(config.Projects))
	for rawAppCode, projectConfig := range config.Projects {
		app := appcode.Normalize(rawAppCode)
		projectConfig = inheritFirebaseVerifierConfig(config.Default, projectConfig)
		if strings.TrimSpace(projectConfig.ProjectID) == "" {
			// 空 project_id 不能生成有效 aud/iss 校验器；保留 fail-closed 行为。
			continue
		}
		verifiers[app] = NewFirebaseThirdPartyVerifier(projectConfig)
	}

	return &AppFirebaseThirdPartyVerifier{
		defaultVerifier: defaultVerifier,
		verifiers:       verifiers,
	}
}

// Verify 根据 context 中的 app_code 校验对应 Firebase ID token。
func (v *AppFirebaseThirdPartyVerifier) Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error) {
	if v == nil {
		return authdomain.ThirdPartyProfile{}, authFailed()
	}
	if verifier := v.verifiers[appcode.FromContext(ctx)]; verifier != nil {
		// app 专属配置是强隔离边界；Huwaa token 只能命中 Huwaa project_id。
		return verifier.Verify(ctx, provider, credential)
	}
	if v.defaultVerifier != nil {
		// 兼容旧部署的单 project_id 配置，避免只配置 Lalu 的环境立即无法登录。
		return v.defaultVerifier.Verify(ctx, provider, credential)
	}

	return authdomain.ThirdPartyProfile{}, authFailed()
}

func inheritFirebaseVerifierConfig(defaultConfig FirebaseVerifierConfig, projectConfig FirebaseVerifierConfig) FirebaseVerifierConfig {
	if len(projectConfig.AllowedSignInProviders) == 0 {
		// 登录方式默认继承全局 allowlist，减少每个 App 重复写 google.com 的配置噪音。
		projectConfig.AllowedSignInProviders = defaultConfig.AllowedSignInProviders
	}
	if strings.TrimSpace(projectConfig.CertsURL) == "" {
		projectConfig.CertsURL = defaultConfig.CertsURL
	}
	if projectConfig.HTTPClient == nil {
		projectConfig.HTTPClient = defaultConfig.HTTPClient
	}
	if projectConfig.Now == nil {
		projectConfig.Now = defaultConfig.Now
	}
	return projectConfig
}

// NewFirebaseThirdPartyVerifier 创建 Firebase ID token verifier。
func NewFirebaseThirdPartyVerifier(config FirebaseVerifierConfig) *FirebaseThirdPartyVerifier {
	projectID := strings.TrimSpace(config.ProjectID)
	certsURL := strings.TrimSpace(config.CertsURL)
	if certsURL == "" {
		certsURL = defaultFirebaseCertsURL
	}
	httpClient := config.HTTPClient
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 5 * time.Second}
	}
	now := config.Now
	if now == nil {
		now = time.Now
	}
	allowedSignInProviders := normalizeAllowedSignInProviders(config.AllowedSignInProviders)

	return &FirebaseThirdPartyVerifier{
		projectID:              projectID,
		issuer:                 "https://securetoken.google.com/" + projectID,
		allowedSignInProviders: allowedSignInProviders,
		certsURL:               certsURL,
		httpClient:             httpClient,
		now:                    now,
		cachedKeys:             map[string]*rsa.PublicKey{},
	}
}

// Verify 校验 Firebase ID token，并把 sub/uid 作为 provider_subject 返回。
func (v *FirebaseThirdPartyVerifier) Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error) {
	provider = strings.ToLower(strings.TrimSpace(provider))
	credential = strings.TrimSpace(credential)
	if provider != "firebase" || credential == "" || v == nil || v.projectID == "" {
		// provider 不匹配、token 为空或 Firebase 项目未配置都统一认证失败，避免泄漏配置状态。
		return authdomain.ThirdPartyProfile{}, authFailed()
	}

	claims := &firebaseIDTokenClaims{}
	parser := jwt.NewParser(
		jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
		jwt.WithIssuer(v.issuer),
		jwt.WithAudience(v.projectID),
		jwt.WithExpirationRequired(),
		jwt.WithIssuedAt(),
		jwt.WithLeeway(firebaseClockLeeway),
		jwt.WithTimeFunc(v.now),
	)
	token, err := parser.ParseWithClaims(credential, claims, func(token *jwt.Token) (any, error) {
		kid, _ := token.Header["kid"].(string)
		return v.publicKey(ctx, kid)
	})
	if err != nil || token == nil || !token.Valid {
		return authdomain.ThirdPartyProfile{}, authFailed()
	}

	subject := strings.TrimSpace(claims.Subject)
	if subject == "" || len(subject) > maxFirebaseSubjectLength {
		// Firebase uid/sub 是唯一登录绑定主键，不能为空或异常超长。
		return authdomain.ThirdPartyProfile{}, authFailed()
	}
	if claims.AuthTime == nil || claims.AuthTime.Time.After(v.now().Add(firebaseClockLeeway)) {
		// auth_time 缺失或明显来自未来时拒绝，避免接受非 Firebase Auth 形态的 JWT。
		return authdomain.ThirdPartyProfile{}, authFailed()
	}
	signInProvider := strings.TrimSpace(claims.Firebase.SignInProvider)
	if !v.allowedSignInProviders[signInProvider] {
		// 首版只允许 Firebase Google 登录，其他 sign_in_provider 后续按产品确认再开放。
		return authdomain.ThirdPartyProfile{}, authFailed()
	}

	return authdomain.ThirdPartyProfile{ProviderSubject: subject}, nil
}

func (v *FirebaseThirdPartyVerifier) publicKey(ctx context.Context, kid string) (*rsa.PublicKey, error) {
	kid = strings.TrimSpace(kid)
	if kid == "" {
		return nil, authFailed()
	}
	if key, cacheValid := v.cachedPublicKey(kid); key != nil {
		return key, nil
	} else if cacheValid {
		// Firebase 证书缓存仍有效但 kid 不存在时直接拒绝，避免随机 kid 伪造 token 放大到外部证书端点。
		return nil, authFailed()
	}
	keys, expiresAt, err := v.fetchPublicKeys(ctx)
	if err != nil {
		return nil, err
	}

	v.mu.Lock()
	v.cachedKeys = keys
	v.cacheExpiresAt = expiresAt
	key, ok := v.cachedKeys[kid]
	v.mu.Unlock()
	if !ok {
		// 未知 kid 说明 token 不是当前 Firebase Secure Token 证书签发。
		return nil, authFailed()
	}

	return key, nil
}

func (v *FirebaseThirdPartyVerifier) cachedPublicKey(kid string) (*rsa.PublicKey, bool) {
	v.mu.Lock()
	defer v.mu.Unlock()

	if v.cacheExpiresAt.IsZero() || !v.now().Before(v.cacheExpiresAt) {
		// 缓存未初始化或已过期时允许调用方刷新；未知 kid 只有在有效缓存窗口内才被本地拒绝。
		return nil, false
	}
	key := v.cachedKeys[kid]
	return key, true
}

func (v *FirebaseThirdPartyVerifier) fetchPublicKeys(ctx context.Context) (map[string]*rsa.PublicKey, time.Time, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, v.certsURL, nil)
	if err != nil {
		return nil, time.Time{}, authFailed()
	}
	response, err := v.httpClient.Do(request)
	if err != nil {
		return nil, time.Time{}, authFailed()
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return nil, time.Time{}, authFailed()
	}

	var certs map[string]string
	if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&certs); err != nil {
		return nil, time.Time{}, authFailed()
	}
	keys := make(map[string]*rsa.PublicKey, len(certs))
	for kid, encodedCert := range certs {
		key, err := parseFirebaseCertPublicKey(encodedCert)
		if err != nil {
			return nil, time.Time{}, authFailed()
		}
		keys[strings.TrimSpace(kid)] = key
	}
	if len(keys) == 0 {
		return nil, time.Time{}, authFailed()
	}

	return keys, firebaseCacheExpiry(v.now, response.Header.Get("Cache-Control")), nil
}

func parseFirebaseCertPublicKey(encodedCert string) (*rsa.PublicKey, error) {
	block, _ := pem.Decode([]byte(encodedCert))
	if block == nil {
		return nil, fmt.Errorf("firebase cert pem decode failed")
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return nil, err
	}
	key, ok := cert.PublicKey.(*rsa.PublicKey)
	if !ok {
		return nil, fmt.Errorf("firebase cert public key is not rsa")
	}

	return key, nil
}

func firebaseCacheExpiry(now func() time.Time, cacheControl string) time.Time {
	for part := range strings.SplitSeq(cacheControl, ",") {
		part = strings.TrimSpace(part)
		if !strings.HasPrefix(strings.ToLower(part), "max-age=") {
			continue
		}
		seconds, err := strconv.ParseInt(strings.TrimPrefix(part, "max-age="), 10, 64)
		if err == nil && seconds > 0 {
			return now().Add(time.Duration(seconds) * time.Second)
		}
	}

	return now().Add(defaultFirebaseCertTTL)
}

func normalizeAllowedSignInProviders(providers []string) map[string]bool {
	if len(providers) == 0 {
		// 首版 Firebase 只开放 Google 登录。
		providers = []string{"google.com"}
	}
	allowed := make(map[string]bool, len(providers))
	for _, provider := range providers {
		provider = strings.TrimSpace(provider)
		if provider != "" {
			allowed[provider] = true
		}
	}

	return allowed
}

// RoutedThirdPartyVerifier 把 firebase provider 路由到 Firebase verifier，其余 provider 交给本地/mock verifier。
type RoutedThirdPartyVerifier struct {
	firebase ThirdPartyVerifier
	fallback ThirdPartyVerifier
}

// NewRoutedThirdPartyVerifier 创建按 provider 分发的三方 verifier。
func NewRoutedThirdPartyVerifier(firebase ThirdPartyVerifier, fallback ThirdPartyVerifier) *RoutedThirdPartyVerifier {
	return &RoutedThirdPartyVerifier{
		firebase: firebase,
		fallback: fallback,
	}
}

// Verify 保证 provider=firebase 永远不会落到静态 verifier。
func (v *RoutedThirdPartyVerifier) Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error) {
	provider = strings.ToLower(strings.TrimSpace(provider))
	if provider == "firebase" {
		if v == nil || v.firebase == nil {
			return authdomain.ThirdPartyProfile{}, authFailed()
		}
		return v.firebase.Verify(ctx, provider, credential)
	}
	if v == nil || v.fallback == nil {
		return authdomain.ThirdPartyProfile{}, authFailed()
	}

	return v.fallback.Verify(ctx, provider, credential)
}
