package apple

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

const (
	// ValidationURL is the endpoint for verifying tokens
	ValidationURL string = "https://appleid.apple.com/auth/token"
	// RevokeURL is the endpoint for revoking tokens
	RevokeURL string = "https://appleid.apple.com/auth/revoke"
	// ContentType is the one expected by Apple
	ContentType string = "application/x-www-form-urlencoded"
	// UserAgent is required by Apple or the request will fail
	UserAgent string = "go-signin-with-apple"
	// AcceptHeader is the content that we are willing to accept
	AcceptHeader string = "application/json"
)

// ValidationClient is an interface to call the validation API
type ValidationClient interface {
	VerifyWebToken(ctx context.Context, reqBody WebValidationTokenRequest, result interface{}) error
	VerifyAppToken(ctx context.Context, reqBody AppValidationTokenRequest, result interface{}) error
	VerifyRefreshToken(ctx context.Context, reqBody ValidationRefreshRequest, result interface{}) error
	RevokeAccessToken(ctx context.Context, reqBody RevokeAccessTokenRequest, result interface{}) error
	RevokeRefreshToken(ctx context.Context, reqBody RevokeRefreshTokenRequest, result interface{}) error
}

// Client implements ValidationClient
type Client struct {
	config        Config
	validationURL string
	revokeURL     string
	secret        string
	client        *http.Client
}

// ClientOptions is a struct to hold the options for the client
type ClientOptions struct {
	validationURL string
	revokeURL     string
	//nolint:unused
	secret string
	client *http.Client
}

// NewWithURL creates a Client object with a custom URL provided
//
// Deprecated: This function is deprecated and will be removed in a future version. Use NewWithOptions instead.
func NewWithURL(validationURL string, revokeURL string) *Client {
	return NewWithOptions(ClientOptions{
		validationURL: validationURL,
		revokeURL:     revokeURL,
	})
}

// NewWithOptions creates a Client object with custom options. It will default to the standard options if not provided
func NewWithOptions(options ClientOptions) *Client {
	if options.client == nil {
		options.client = &http.Client{
			Timeout: 5 * time.Second,
		}
	}
	if options.validationURL == "" {
		options.validationURL = ValidationURL
	}
	if options.revokeURL == "" {
		options.revokeURL = RevokeURL
	}
	client := &Client{
		validationURL: options.validationURL,
		revokeURL:     options.revokeURL,
		client:        options.client,
	}
	return client
}

// VerifyWebToken sends the WebValidationTokenRequest and gets validation result
func (c *Client) VerifyWebToken(ctx context.Context, code string) (ValidationResponse, error) {
	data := url.Values{
		"client_id":     {c.config.ClientID},
		"client_secret": {c.secret},
		"code":          {code},
		"redirect_uri":  {c.config.RedirectURI},
		"grant_type":    {"authorization_code"},
	}
	var resp ValidationResponse
	err := doRequest(ctx, c.client, &resp, c.validationURL, data)

	return resp, err
}

// VerifyAppToken sends the AppValidationTokenRequest and gets validation result
func (c *Client) VerifyAppToken(ctx context.Context, reqBody AppValidationTokenRequest, result interface{}) error {
	data := url.Values{
		"client_id":     {reqBody.ClientID},
		"client_secret": {reqBody.ClientSecret},
		"code":          {reqBody.Code},
		"grant_type":    {"authorization_code"},
	}

	return doRequest(ctx, c.client, &result, c.validationURL, data)
}

// VerifyRefreshToken sends the WebValidationTokenRequest and gets validation result
func (c *Client) VerifyRefreshToken(ctx context.Context, reqBody ValidationRefreshRequest, result interface{}) error {
	data := url.Values{
		"client_id":     {reqBody.ClientID},
		"client_secret": {reqBody.ClientSecret},
		"refresh_token": {reqBody.RefreshToken},
		"grant_type":    {"refresh_token"},
	}

	return doRequest(ctx, c.client, &result, c.validationURL, data)
}

// RevokeRefreshToken revokes the Refresh Token and gets the revoke result
func (c *Client) RevokeRefreshToken(ctx context.Context, token string, result interface{}) error {
	data := url.Values{
		"client_id":       {c.config.ClientID},
		"client_secret":   {c.secret},
		"token":           {token},
		"token_type_hint": {"refresh_token"},
	}

	return doRequest(ctx, c.client, result, c.revokeURL, data)
}

// RevokeAccessToken revokes the Access Token and gets the revoke result
func (c *Client) RevokeAccessToken(ctx context.Context, token string, result interface{}) error {
	data := url.Values{
		"client_id":       {c.config.ClientID},
		"client_secret":   {c.secret},
		"token":           {token},
		"token_type_hint": {"access_token"},
	}
	log.Printf("revoke access token: %v", data)
	return doRequest(ctx, c.client, &result, c.revokeURL, data)
}

// GetUniqueID decodes the id_token response and returns the unique subject ID to identify the user
func GetUniqueID(idToken string) (string, error) {
	token, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{})
	if err != nil {
		return "", err
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return "", fmt.Errorf("invalid token claims")
	}

	return fmt.Sprintf("%v", claims["sub"]), nil
}

// GetClaims decodes the id_token response and returns the JWT claims to identify the user
func GetClaims(idToken string) (*jwt.MapClaims, error) {
	token, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{})
	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return nil, fmt.Errorf("invalid token claims")
	}

	return &claims, nil
}

func doRequest(ctx context.Context, client *http.Client, result interface{}, url string, data url.Values) error {
	req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(data.Encode()))
	if err != nil {
		return err
	}
	req.Header.Add("content-type", ContentType)
	req.Header.Add("accept", AcceptHeader)
	req.Header.Add("user-agent", UserAgent) // apple requires a user agent

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

	if res.StatusCode != http.StatusOK {
		log.Printf("error response from apple: %s", res.Status)
	}

	return json.NewDecoder(res.Body).Decode(result)
}
