package oidc

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

const discoveryCacheTTL = 24 * time.Hour

var discoveryCache = struct {
	sync.Mutex
	entries map[string]discoveryCacheEntry
}{
	entries: make(map[string]discoveryCacheEntry),
}

type discoveryCacheEntry struct {
	discovery *Discovery
	expiresAt time.Time
}

// Provider represents an OIDC provider configuration
type Provider struct {
	Issuer       string
	ClientID     string
	ClientSecret string
	RedirectURI  string
	Scopes       []string
	HTTPClient   *http.Client
}

type ProviderOption func(*Provider)

func WithHTTPClient(httpClient *http.Client) ProviderOption {
	return func(provider *Provider) {
		if httpClient != nil {
			provider.HTTPClient = httpClient
		}
	}
}

// UserInfo represents the user information from OIDC
type UserInfo struct {
	OpenID              string `json:"sub"`
	DisplayName         string `json:"name"`
	Email               string `json:"email"`
	EmailVerified       bool   `json:"email_verified"`
	PhoneNumber         string `json:"phone_number"`
	PhoneNumberVerified bool   `json:"phone_number_verified"`
	AvatarURL           string `json:"picture"`
}

// TokenResponse represents the token response from OIDC
type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token"`
}

// Discovery represents the OIDC discovery document
type Discovery struct {
	Issuer                 string   `json:"issuer"`
	AuthorizationEndpoint  string   `json:"authorization_endpoint"`
	TokenEndpoint          string   `json:"token_endpoint"`
	UserInfoEndpoint       string   `json:"userinfo_endpoint"`
	JWKSURI                string   `json:"jwks_uri"`
	ScopesSupported        []string `json:"scopes_supported"`
	ResponseTypesSupported []string `json:"response_types_supported"`
}

// NewProvider creates a new OIDC provider
func NewProvider(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...ProviderOption) *Provider {
	provider := &Provider{
		Issuer:       strings.TrimSuffix(issuer, "/"),
		ClientID:     clientID,
		ClientSecret: clientSecret,
		RedirectURI:  redirectURI,
		Scopes:       scopes,
		HTTPClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
	for _, option := range options {
		if option != nil {
			option(provider)
		}
	}
	return provider
}

// Discover fetches the OIDC discovery document
func (p *Provider) Discover(ctx context.Context) (*Discovery, error) {
	now := time.Now()
	discoveryCache.Lock()
	if entry, ok := discoveryCache.entries[p.Issuer]; ok && now.Before(entry.expiresAt) {
		discovery := cloneDiscovery(entry.discovery)
		discoveryCache.Unlock()
		return discovery, nil
	}
	discoveryCache.Unlock()

	discoveryURL := fmt.Sprintf("%s/.well-known/openid-configuration", p.Issuer)

	req, err := http.NewRequestWithContext(ctx, "GET", discoveryURL, nil)
	if err != nil {
		return nil, fmt.Errorf("create discovery request: %w", err)
	}

	resp, err := p.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetch discovery document: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("discovery endpoint returned status %d", resp.StatusCode)
	}

	var discovery Discovery
	if err := json.NewDecoder(resp.Body).Decode(&discovery); err != nil {
		return nil, fmt.Errorf("decode discovery document: %w", err)
	}

	discoveryCache.Lock()
	discoveryCache.entries[p.Issuer] = discoveryCacheEntry{
		discovery: cloneDiscovery(&discovery),
		expiresAt: now.Add(discoveryCacheTTL),
	}
	discoveryCache.Unlock()

	return cloneDiscovery(&discovery), nil
}

// BuildAuthorizationURL builds the authorization URL for the OAuth flow
func (p *Provider) BuildAuthorizationURL(state, nonce, codeChallenge string) (string, error) {
	return p.BuildAuthorizationURLContext(context.Background(), state, nonce, codeChallenge)
}

// BuildAuthorizationURLContext builds the authorization URL for the OAuth flow.
func (p *Provider) BuildAuthorizationURLContext(ctx context.Context, state, nonce, codeChallenge string) (string, error) {
	discovery, err := p.Discover(ctx)
	if err != nil {
		return "", fmt.Errorf("discover endpoints: %w", err)
	}

	authURL, err := url.Parse(discovery.AuthorizationEndpoint)
	if err != nil {
		return "", fmt.Errorf("parse authorization endpoint: %w", err)
	}

	params := url.Values{}
	params.Set("client_id", p.ClientID)
	params.Set("redirect_uri", p.RedirectURI)
	params.Set("response_type", "code")
	params.Set("scope", strings.Join(p.Scopes, " "))
	params.Set("state", state)
	params.Set("nonce", nonce)

	if codeChallenge != "" {
		params.Set("code_challenge", codeChallenge)
		params.Set("code_challenge_method", "S256")
	}

	authURL.RawQuery = params.Encode()
	return authURL.String(), nil
}

func cloneDiscovery(discovery *Discovery) *Discovery {
	if discovery == nil {
		return nil
	}
	cloned := *discovery
	cloned.ScopesSupported = append([]string(nil), discovery.ScopesSupported...)
	cloned.ResponseTypesSupported = append([]string(nil), discovery.ResponseTypesSupported...)
	return &cloned
}

// ExchangeCodeForToken exchanges an authorization code for tokens
func (p *Provider) ExchangeCodeForToken(ctx context.Context, code, codeVerifier string) (*TokenResponse, error) {
	discovery, err := p.Discover(ctx)
	if err != nil {
		return nil, fmt.Errorf("discover endpoints: %w", err)
	}

	data := url.Values{}
	data.Set("grant_type", "authorization_code")
	data.Set("code", code)
	data.Set("redirect_uri", p.RedirectURI)
	data.Set("client_id", p.ClientID)

	if p.ClientSecret != "" {
		data.Set("client_secret", p.ClientSecret)
	}

	if codeVerifier != "" {
		data.Set("code_verifier", codeVerifier)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", discovery.TokenEndpoint, strings.NewReader(data.Encode()))
	if err != nil {
		return nil, fmt.Errorf("create token request: %w", err)
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := p.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("exchange code for token: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token endpoint returned status %d", resp.StatusCode)
	}

	var tokenResp TokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return nil, fmt.Errorf("decode token response: %w", err)
	}

	return &tokenResp, nil
}

// GetUserInfo fetches user info using the access token
func (p *Provider) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) {
	discovery, err := p.Discover(ctx)
	if err != nil {
		return nil, fmt.Errorf("discover endpoints: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "GET", discovery.UserInfoEndpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("create userinfo request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := p.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetch userinfo: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("userinfo endpoint returned status %d", resp.StatusCode)
	}

	var userInfo UserInfo
	if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
		return nil, fmt.Errorf("decode userinfo: %w", err)
	}

	return &userInfo, nil
}

// GenerateCodeVerifier generates a PKCE code verifier
func GenerateCodeVerifier() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("generate random bytes: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

// GenerateCodeChallenge creates a PKCE code challenge from verifier
func GenerateCodeChallenge(verifier string) string {
	h := sha256.New()
	h.Write([]byte(verifier))
	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

// GenerateRandomState generates a random state parameter
func GenerateRandomState() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("generate random bytes: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

// GenerateRandomNonce generates a random nonce parameter
func GenerateRandomNonce() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("generate random bytes: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}
