package discord

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

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

// Client wraps the minimal Discord API methods required for DM notifications.
type Client struct {
	token      string
	baseURL    string
	httpClient *http.Client
}

// NewClient constructs a Discord API client using the provided bot token.
func NewClient(token string, opts ...Option) *Client {
	client := &Client{
		token:   token,
		baseURL: defaultBaseURL,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}

	for _, opt := range opts {
		opt(client)
	}

	return client
}

// Option customises the Discord client.
type Option func(*Client)

// WithHTTPClient injects a custom http.Client.
func WithHTTPClient(httpClient *http.Client) Option {
	return func(c *Client) {
		if httpClient != nil {
			c.httpClient = httpClient
		}
	}
}

// WithBaseURL overrides the API base URL. Useful for stubs/tests.
func WithBaseURL(baseURL string) Option {
	return func(c *Client) {
		if baseURL != "" {
			c.baseURL = strings.TrimRight(baseURL, "/")
		}
	}
}

// SendDM sends a direct message to the specified Discord user.
func (c *Client) SendDM(ctx context.Context, recipientID, message string) error {
	channelID, err := c.ensureDMChannel(ctx, recipientID)
	if err != nil {
		return fmt.Errorf("create DM channel: %w", err)
	}

	if err := c.postMessage(ctx, channelID, message); err != nil {
		return fmt.Errorf("send DM: %w", err)
	}

	return nil
}

func (c *Client) ensureDMChannel(ctx context.Context, recipientID string) (string, error) {
	payload := map[string]string{
		"recipient_id": recipientID,
	}

	buf, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("marshal DM channel payload: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/users/@me/channels", c.baseURL), bytes.NewReader(buf))
	if err != nil {
		return "", fmt.Errorf("build DM channel request: %w", err)
	}

	req.Header.Set("Authorization", "Bot "+c.token)
	req.Header.Set("Content-Type", "application/json")

	res, err := c.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("execute DM channel request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(res.Body, 2048))
		return "", fmt.Errorf("DM channel request failed: status=%d body=%s", res.StatusCode, string(body))
	}

	var response struct {
		ID string `json:"id"`
	}

	if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
		return "", fmt.Errorf("decode DM channel response: %w", err)
	}

	if response.ID == "" {
		return "", fmt.Errorf("missing channel ID in response")
	}

	return response.ID, nil
}

func (c *Client) postMessage(ctx context.Context, channelID, message string) error {
	payload := map[string]string{
		"content": message,
	}

	buf, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal message payload: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/channels/%s/messages", c.baseURL, channelID), bytes.NewReader(buf))
	if err != nil {
		return fmt.Errorf("build message request: %w", err)
	}

	req.Header.Set("Authorization", "Bot "+c.token)
	req.Header.Set("Content-Type", "application/json")

	res, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("execute message request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(io.LimitReader(res.Body, 2048))
		return fmt.Errorf("message request failed: status=%d body=%s", res.StatusCode, string(body))
	}

	return nil
}
