package service

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"wucher/internal/config"
	"wucher/internal/resilience"
)

type MicrosoftTokenSet struct {
	AccessToken           string
	RefreshToken          string
	IDToken               string
	TokenType             string
	Scope                 string
	ExpiresIn             int64
	RefreshTokenExpiresIn int64
}

type MicrosoftSSOClient struct {
	cfg            config.MicrosoftSSOConfig
	client         *http.Client
	executor       *resilience.Executor
	configResolver func(context.Context) (config.MicrosoftSSOConfig, error)
}

func NewMicrosoftSSO(cfg config.MicrosoftSSOConfig) *MicrosoftSSOClient {
	return &MicrosoftSSOClient{
		cfg: cfg,
		client: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (m *MicrosoftSSOClient) WithExecutor(executor *resilience.Executor) *MicrosoftSSOClient {
	if m == nil {
		return nil
	}
	m.executor = executor
	return m
}

func (m *MicrosoftSSOClient) WithConfigResolver(resolver func(context.Context) (config.MicrosoftSSOConfig, error)) *MicrosoftSSOClient {
	if m == nil {
		return nil
	}
	m.configResolver = resolver
	return m
}

func (m *MicrosoftSSOClient) AuthCodeURL(state string) string {
	cfg, err := m.currentConfig(context.Background())
	if err != nil || !isMicrosoftSSOAuthConfigComplete(cfg) {
		return ""
	}
	scopes := ensureMicrosoftScopes(cfg.Scopes)
	q := url.Values{}
	q.Set("client_id", cfg.ClientID)
	q.Set("response_type", "code")
	q.Set("redirect_uri", cfg.RedirectURL)
	q.Set("response_mode", "query")
	q.Set("scope", strings.Join(scopes, " "))
	q.Set("state", state)
	q.Set("prompt", "select_account")
	return fmt.Sprintf("%s%s/oauth2/v2.0/authorize?%s", cfg.Authority, cfg.TenantID, q.Encode())
}

func (m *MicrosoftSSOClient) LogoutURL(postLogoutRedirectURL string) string {
	cfg, err := m.currentConfig(context.Background())
	if err != nil || !isMicrosoftSSOAuthConfigComplete(cfg) {
		return ""
	}
	q := url.Values{}
	if redirect := strings.TrimSpace(postLogoutRedirectURL); redirect != "" {
		q.Set("post_logout_redirect_uri", redirect)
	}
	base := fmt.Sprintf("%s%s/oauth2/v2.0/logout", cfg.Authority, cfg.TenantID)
	if encoded := q.Encode(); encoded != "" {
		return base + "?" + encoded
	}
	return base
}

func (m *MicrosoftSSOClient) ExchangeCode(ctx context.Context, code string) (MicrosoftClaims, error) {
	_, claims, err := m.ExchangeCodeAndTokens(ctx, code, "")
	return claims, err
}

func (m *MicrosoftSSOClient) ExchangeCodeAndTokens(ctx context.Context, code, codeVerifier string) (MicrosoftTokenSet, MicrosoftClaims, error) {
	set, err := resilience.Do(ctx, m.executor, func(ctx context.Context) (MicrosoftTokenSet, error) {
		if code == "" {
			return MicrosoftTokenSet{}, errors.New("missing code")
		}
		cfg, err := m.currentConfig(ctx)
		if err != nil || !isMicrosoftSSOConfigComplete(cfg) {
			return MicrosoftTokenSet{}, errors.New("sso not configured")
		}

		scopes := ensureMicrosoftScopes(cfg.Scopes)
		form := url.Values{}
		form.Set("client_id", cfg.ClientID)
		form.Set("client_secret", cfg.ClientSecret)
		form.Set("grant_type", "authorization_code")
		form.Set("code", code)
		form.Set("redirect_uri", cfg.RedirectURL)
		form.Set("scope", strings.Join(scopes, " "))
		if strings.TrimSpace(codeVerifier) != "" {
			form.Set("code_verifier", strings.TrimSpace(codeVerifier))
		}

		tokenURL := fmt.Sprintf("%s%s/oauth2/v2.0/token", cfg.Authority, cfg.TenantID)
		return m.exchangeTokenForm(ctx, tokenURL, form)
	})
	if err != nil {
		return MicrosoftTokenSet{}, MicrosoftClaims{}, err
	}
	claims, err := parseMicrosoftClaimsFromIDToken(set.IDToken)
	if err != nil {
		return MicrosoftTokenSet{}, MicrosoftClaims{}, err
	}
	return set, claims, nil
}

func (m *MicrosoftSSOClient) RefreshAccessToken(ctx context.Context, refreshToken, codeVerifier string) (MicrosoftTokenSet, error) {
	return resilience.Do(ctx, m.executor, func(ctx context.Context) (MicrosoftTokenSet, error) {
		if strings.TrimSpace(refreshToken) == "" {
			return MicrosoftTokenSet{}, errors.New("missing refresh token")
		}
		cfg, err := m.currentConfig(ctx)
		if err != nil || !isMicrosoftSSOConfigComplete(cfg) {
			return MicrosoftTokenSet{}, errors.New("sso not configured")
		}

		scopes := ensureMicrosoftScopes(cfg.Scopes)
		form := url.Values{}
		form.Set("client_id", cfg.ClientID)
		form.Set("client_secret", cfg.ClientSecret)
		form.Set("grant_type", "refresh_token")
		form.Set("refresh_token", strings.TrimSpace(refreshToken))
		form.Set("redirect_uri", cfg.RedirectURL)
		form.Set("scope", strings.Join(scopes, " "))
		if strings.TrimSpace(codeVerifier) != "" {
			form.Set("code_verifier", strings.TrimSpace(codeVerifier))
		}
		tokenURL := fmt.Sprintf("%s%s/oauth2/v2.0/token", cfg.Authority, cfg.TenantID)
		return m.exchangeTokenForm(ctx, tokenURL, form)
	})
}

func (m *MicrosoftSSOClient) exchangeTokenForm(ctx context.Context, tokenURL string, form url.Values) (MicrosoftTokenSet, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return MicrosoftTokenSet{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := m.client.Do(req)
	if err != nil {
		return MicrosoftTokenSet{}, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		bodyStr := string(body)
		if resp.StatusCode == http.StatusUnauthorized && strings.Contains(bodyStr, "invalid_client") {
			return MicrosoftTokenSet{}, fmt.Errorf(
				"token exchange failed: invalid_client (check MS_ENTRA_CLIENT_SECRET uses secret VALUE, not secret ID): %s",
				bodyStr,
			)
		}
		return MicrosoftTokenSet{}, &resilience.HTTPStatusError{
			Operation:  "token exchange failed",
			StatusCode: resp.StatusCode,
			Body:       bodyStr,
		}
	}

	var token struct {
		AccessToken           string `json:"access_token"`
		RefreshToken          string `json:"refresh_token"`
		IDToken               string `json:"id_token"`
		TokenType             string `json:"token_type"`
		Scope                 string `json:"scope"`
		ExpiresIn             int64  `json:"expires_in"`
		RefreshTokenExpiresIn int64  `json:"refresh_token_expires_in"`
	}
	if err := json.Unmarshal(body, &token); err != nil {
		return MicrosoftTokenSet{}, err
	}
	if strings.TrimSpace(token.IDToken) == "" {
		return MicrosoftTokenSet{}, errors.New("missing id_token")
	}
	if strings.TrimSpace(token.AccessToken) == "" {
		return MicrosoftTokenSet{}, errors.New("missing access_token")
	}

	return MicrosoftTokenSet{
		AccessToken:           strings.TrimSpace(token.AccessToken),
		RefreshToken:          strings.TrimSpace(token.RefreshToken),
		IDToken:               strings.TrimSpace(token.IDToken),
		TokenType:             strings.TrimSpace(token.TokenType),
		Scope:                 strings.TrimSpace(token.Scope),
		ExpiresIn:             token.ExpiresIn,
		RefreshTokenExpiresIn: token.RefreshTokenExpiresIn,
	}, nil
}

func (m *MicrosoftSSOClient) currentConfig(ctx context.Context) (config.MicrosoftSSOConfig, error) {
	if m == nil {
		return config.MicrosoftSSOConfig{}, errors.New("sso client is nil")
	}
	if m.configResolver == nil {
		return m.cfg, nil
	}
	return m.configResolver(ctx)
}

func isMicrosoftSSOConfigComplete(cfg config.MicrosoftSSOConfig) bool {
	return strings.TrimSpace(cfg.TenantID) != "" &&
		strings.TrimSpace(cfg.ClientID) != "" &&
		strings.TrimSpace(cfg.ClientSecret) != "" &&
		strings.TrimSpace(cfg.RedirectURL) != "" &&
		strings.TrimSpace(cfg.Authority) != "" &&
		len(ensureMicrosoftScopes(cfg.Scopes)) > 0
}

func IsMicrosoftSSOConfigComplete(cfg config.MicrosoftSSOConfig) bool {
	return isMicrosoftSSOConfigComplete(cfg)
}

func isMicrosoftSSOAuthConfigComplete(cfg config.MicrosoftSSOConfig) bool {
	return strings.TrimSpace(cfg.TenantID) != "" &&
		strings.TrimSpace(cfg.ClientID) != "" &&
		strings.TrimSpace(cfg.RedirectURL) != "" &&
		strings.TrimSpace(cfg.Authority) != "" &&
		len(ensureMicrosoftScopes(cfg.Scopes)) > 0
}

func ensureMicrosoftScopes(scopes []string) []string {
	merged := make([]string, 0, len(scopes)+5)
	seen := map[string]struct{}{}
	appendScope := func(v string) {
		v = strings.TrimSpace(v)
		if v == "" {
			return
		}
		key := strings.ToLower(v)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		merged = append(merged, v)
	}
	for i := range scopes {
		appendScope(scopes[i])
	}
	appendScope("openid")
	appendScope("profile")
	appendScope("email")
	appendScope("offline_access")
	appendScope("User.Read")
	return merged
}

func parseMicrosoftClaimsFromIDToken(idToken string) (MicrosoftClaims, error) {
	claimsMap, err := parseJWTClaims(idToken)
	if err != nil {
		return MicrosoftClaims{}, err
	}
	sub, _ := claimsMap["sub"].(string)
	email, _ := claimsMap["email"].(string)
	name, _ := claimsMap["name"].(string)
	if email == "" {
		email, _ = claimsMap["preferred_username"].(string)
	}
	tid, _ := claimsMap["tid"].(string)
	oid, _ := claimsMap["oid"].(string)
	sid, _ := claimsMap["sid"].(string)
	return MicrosoftClaims{
		Subject:  strings.TrimSpace(sub),
		Email:    strings.TrimSpace(email),
		Name:     strings.TrimSpace(name),
		TenantID: strings.TrimSpace(tid),
		ObjectID: strings.TrimSpace(oid),
		SessionID: strings.TrimSpace(sid),
	}, nil
}

func parseJWTClaims(token string) (map[string]any, error) {
	parts := strings.Split(token, ".")
	if len(parts) < 2 {
		return nil, errors.New("invalid jwt")
	}
	payload, err := decodeSegment(parts[1])
	if err != nil {
		return nil, err
	}
	var claims map[string]any
	if err := json.Unmarshal(payload, &claims); err != nil {
		return nil, err
	}
	return claims, nil
}

func decodeSegment(seg string) ([]byte, error) {
	seg = strings.ReplaceAll(seg, "-", "+")
	seg = strings.ReplaceAll(seg, "_", "/")
	switch len(seg) % 4 {
	case 2:
		seg += "=="
	case 3:
		seg += "="
	}
	return io.ReadAll(base64.NewDecoder(base64.StdEncoding, strings.NewReader(seg)))
}

func microsoftPKCEChallengeS256(verifier string) string {
	h := sha256.Sum256([]byte(strings.TrimSpace(verifier)))
	return base64.RawURLEncoding.EncodeToString(h[:])
}
