package auth

import (
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/subtle"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/go-jose/go-jose/v4"
	"github.com/go-jose/go-jose/v4/jwt"
	"golang.org/x/oauth2"
)

const (
	OAuthCookieState = "state"
	OAuthCookieNonce = "nonce"
)

const oauthCookieTTL = 5 * time.Minute

type GoogleProviderConfig struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string
	AuthURL      string
	TokenURL     string
	Issuer       string
}

type AppleProviderConfig struct {
	ClientID    string
	TeamID      string
	KeyID       string
	PrivateKey  string
	RedirectURL string
	AuthURL     string
	TokenURL    string
	Issuer      string
}

func (c AppleProviderConfig) Configured() bool {
	return c.ClientID != "" && c.TeamID != "" && c.KeyID != "" && c.PrivateKey != "" && c.RedirectURL != ""
}

func (c AppleProviderConfig) withDefaults() AppleProviderConfig {
	if c.AuthURL == "" {
		c.AuthURL = "https://appleid.apple.com/auth/authorize"
	}
	if c.TokenURL == "" {
		c.TokenURL = "https://appleid.apple.com/auth/token"
	}
	if c.Issuer == "" {
		c.Issuer = "https://appleid.apple.com"
	}
	return c
}

func (c AppleProviderConfig) OAuth2Config(clientSecret string) oauth2.Config {
	c = c.withDefaults()
	return oauth2.Config{
		ClientID:     c.ClientID,
		ClientSecret: clientSecret,
		RedirectURL:  c.RedirectURL,
		Scopes:       []string{"name", "email"},
		Endpoint: oauth2.Endpoint{
			AuthURL:  c.AuthURL,
			TokenURL: c.TokenURL,
		},
	}
}

func (c AppleProviderConfig) ClientSecret(now time.Time) (string, error) {
	privateKey, err := parseApplePrivateKey(c.PrivateKey)
	if err != nil {
		return "", err
	}
	opts := (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", c.KeyID)
	signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: privateKey}, opts)
	if err != nil {
		return "", fmt.Errorf("auth: create apple client secret signer: %w", err)
	}
	claims := jwt.Claims{
		Issuer:   c.TeamID,
		Subject:  c.ClientID,
		Audience: jwt.Audience{"https://appleid.apple.com"},
		IssuedAt: jwt.NewNumericDate(now),
		Expiry:   jwt.NewNumericDate(now.Add(6 * time.Hour)),
	}
	secret, err := jwt.Signed(signer).Claims(claims).Serialize()
	if err != nil {
		return "", fmt.Errorf("auth: sign apple client secret: %w", err)
	}
	return secret, nil
}

func parseApplePrivateKey(raw string) (*ecdsa.PrivateKey, error) {
	normalized := strings.ReplaceAll(raw, `\n`, "\n")
	block, _ := pem.Decode([]byte(normalized))
	if block == nil {
		return nil, errors.New("auth: apple private key PEM block missing")
	}
	if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
		return key, nil
	}
	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("auth: parse apple private key: %w", err)
	}
	key, ok := parsed.(*ecdsa.PrivateKey)
	if !ok {
		return nil, errors.New("auth: apple private key is not ECDSA")
	}
	return key, nil
}

func (c GoogleProviderConfig) Configured() bool {
	return c.ClientID != "" && c.ClientSecret != "" && c.RedirectURL != ""
}

func (c GoogleProviderConfig) withDefaults() GoogleProviderConfig {
	if c.AuthURL == "" {
		c.AuthURL = "https://accounts.google.com/o/oauth2/v2/auth"
	}
	if c.TokenURL == "" {
		c.TokenURL = "https://oauth2.googleapis.com/token"
	}
	if c.Issuer == "" {
		c.Issuer = "https://accounts.google.com"
	}
	return c
}

func (c GoogleProviderConfig) OAuth2Config() oauth2.Config {
	c = c.withDefaults()
	return oauth2.Config{
		ClientID:     c.ClientID,
		ClientSecret: c.ClientSecret,
		RedirectURL:  c.RedirectURL,
		Scopes:       []string{oidc.ScopeOpenID, "email", "profile"},
		Endpoint: oauth2.Endpoint{
			AuthURL:  c.AuthURL,
			TokenURL: c.TokenURL,
		},
	}
}

type OAuthConfig struct {
	Google GoogleProviderConfig
	Apple  AppleProviderConfig
}

type ProviderClaims struct {
	Provider      string
	Subject       string
	Email         string
	EmailVerified bool
	DisplayName   string
	AvatarURL     string
	Nonce         string
}

type CodeExchanger interface {
	Exchange(ctx context.Context, code string) (*oauth2.Token, error)
}

type OAuth2CodeExchanger struct {
	Config oauth2.Config
}

func (e OAuth2CodeExchanger) Exchange(ctx context.Context, code string) (*oauth2.Token, error) {
	return e.Config.Exchange(ctx, code)
}

type IDTokenVerifier interface {
	Verify(ctx context.Context, rawIDToken string) (ProviderClaims, error)
}

type OIDCVerifier struct {
	Provider string
	Issuer   string
	ClientID string
}

func (v OIDCVerifier) Verify(ctx context.Context, rawIDToken string) (ProviderClaims, error) {
	provider, err := oidc.NewProvider(ctx, v.Issuer)
	if err != nil {
		return ProviderClaims{}, fmt.Errorf("auth: oidc provider discovery: %w", err)
	}
	verified, err := provider.Verifier(&oidc.Config{ClientID: v.ClientID}).Verify(ctx, rawIDToken)
	if err != nil {
		return ProviderClaims{}, fmt.Errorf("auth: verify id token: %w", err)
	}
	var claims struct {
		Subject       string       `json:"sub"`
		Email         string       `json:"email"`
		EmailVerified verifiedBool `json:"email_verified"`
		Name          string       `json:"name"`
		Picture       string       `json:"picture"`
		Nonce         string       `json:"nonce"`
	}
	if err := verified.Claims(&claims); err != nil {
		return ProviderClaims{}, fmt.Errorf("auth: decode id token claims: %w", err)
	}
	if claims.Subject == "" {
		return ProviderClaims{}, errors.New("auth: id token missing subject")
	}
	return ProviderClaims{
		Provider:      v.Provider,
		Subject:       claims.Subject,
		Email:         claims.Email,
		EmailVerified: bool(claims.EmailVerified),
		DisplayName:   claims.Name,
		AvatarURL:     claims.Picture,
		Nonce:         claims.Nonce,
	}, nil
}

type verifiedBool bool

func (v *verifiedBool) UnmarshalJSON(data []byte) error {
	var b bool
	if err := json.Unmarshal(data, &b); err == nil {
		*v = verifiedBool(b)
		return nil
	}
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return err
	}
	*v = verifiedBool(s == "true")
	return nil
}

func GenerateOAuthValue() (string, error) {
	raw := make([]byte, 32)
	if _, err := rand.Read(raw); err != nil {
		return "", fmt.Errorf("auth: generate oauth value: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

func OAuthCookieName(provider, kind string) string {
	return "xtablo_oauth_" + provider + "_" + kind
}

func SetOAuthCookie(w http.ResponseWriter, provider, kind, value string, secure bool) {
	http.SetCookie(w, &http.Cookie{
		Name:     OAuthCookieName(provider, kind),
		Value:    value,
		Path:     "/",
		MaxAge:   int(oauthCookieTTL.Seconds()),
		Expires:  time.Now().Add(oauthCookieTTL),
		HttpOnly: true,
		Secure:   secure,
		SameSite: http.SameSiteLaxMode,
	})
}

func ClearOAuthCookie(w http.ResponseWriter, provider, kind string, secure bool) {
	http.SetCookie(w, &http.Cookie{
		Name:     OAuthCookieName(provider, kind),
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		Expires:  time.Unix(0, 0),
		HttpOnly: true,
		Secure:   secure,
		SameSite: http.SameSiteLaxMode,
	})
}

func ValidateOAuthCookie(r *http.Request, provider, kind, value string) bool {
	c, err := r.Cookie(OAuthCookieName(provider, kind))
	if err != nil || c.Value == "" || value == "" {
		return false
	}
	if len(c.Value) != len(value) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(c.Value), []byte(value)) == 1
}
