package discord

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

const baseURL = "https://discord.com/api/v10"

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	Scope        string `json:"scope"`
}

type User struct {
	ID            string  `json:"id"`
	Username      string  `json:"username"`
	Discriminator string  `json:"discriminator"`
	Avatar        *string `json:"avatar"`
	Email         *string `json:"email"`
	Verified      bool    `json:"verified"`
}

type ApplicationRPCResponse struct {
	ID                  string   `json:"id"`
	Name                string   `json:"name"`
	Icon                string   `json:"icon"`
	Description         string   `json:"description"`
	Summary             string   `json:"summary"`
	IsMonetized         bool     `json:"is_monetized"`
	IsVerified          bool     `json:"is_verified"`
	IsDiscoverable      bool     `json:"is_discoverable"`
	Hook                bool     `json:"hook"`
	GuildID             string   `json:"guild_id"`
	StorefrontAvailable bool     `json:"storefront_available"`
	BotPublic           bool     `json:"bot_public"`
	BotRequireCodeGrant bool     `json:"bot_require_code_grant"`
	TermsOfServiceURL   string   `json:"terms_of_service_url"`
	PrivacyPolicyURL    string   `json:"privacy_policy_url"`
	CustomInstallURL    string   `json:"custom_install_url"`
	VerifyKey           string   `json:"verify_key"`
	Flags               int      `json:"flags"`
	Tags                []string `json:"tags"`
}

type Client struct {
	clientID     string
	clientSecret string
	redirectURI  string
	httpClient   *http.Client
}

func NewClient(clientID, clientSecret, redirectURI string) *Client {
	return &Client{
		clientID:     clientID,
		clientSecret: clientSecret,
		redirectURI:  redirectURI,
		httpClient:   &http.Client{Timeout: 10 * time.Second},
	}
}

// AuthURL returns the Discord OAuth2 authorization URL.
// state should be a random, unguessable string stored in the session.
func (c *Client) AuthURL(state string) string {
	v := url.Values{}
	v.Set("client_id", c.clientID)
	v.Set("redirect_uri", c.redirectURI)
	v.Set("response_type", "code")
	v.Set("scope", "identify email")
	v.Set("state", state)
	return baseURL + "/oauth2/authorize?" + v.Encode()
}

// ExchangeCode exchanges an authorization code for an access token.
func (c *Client) ExchangeCode(ctx context.Context, code string) (*TokenResponse, error) {
	body := url.Values{}
	body.Set("grant_type", "authorization_code")
	body.Set("code", code)
	body.Set("redirect_uri", c.redirectURI)

	return c.tokenRequest(ctx, body)
}

// RefreshToken exchanges a refresh token for a new access token.
func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
	body := url.Values{}
	body.Set("grant_type", "refresh_token")
	body.Set("refresh_token", refreshToken)

	return c.tokenRequest(ctx, body)
}

// GetCurrentUser fetches the authenticated user's profile.
func (c *Client) GetCurrentUser(ctx context.Context, accessToken string) (*User, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/@me", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	var user User
	if err := c.do(req, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

// GetApplication fetches public metadata for a bot application.
func (c *Client) GetApplication(ctx context.Context, id string) (*ApplicationRPCResponse, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet,
		fmt.Sprintf("%s/applications/%s/rpc", baseURL, id), nil)
	if err != nil {
		return nil, err
	}

	var app ApplicationRPCResponse
	if err := c.do(req, &app); err != nil {
		return nil, err
	}
	return &app, nil
}

func (c *Client) tokenRequest(ctx context.Context, body url.Values) (*TokenResponse, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		baseURL+"/oauth2/token",
		strings.NewReader(body.Encode()),
	)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(c.clientID, c.clientSecret)

	var t TokenResponse
	if err := c.do(req, &t); err != nil {
		return nil, err
	}
	return &t, nil
}

func (c *Client) do(req *http.Request, out any) error {
	res, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		body, _ := io.ReadAll(res.Body)
		return fmt.Errorf("discord API error %d: %s", res.StatusCode, body)
	}

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