package apprise

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sync"

	"github.com/safing/portmaster/base/utils"
)

// Notifier sends messsages to an Apprise API.
type Notifier struct {
	// URL defines the Apprise API endpoint.
	URL string

	// DefaultType defines the default message type.
	DefaultType MsgType

	// DefaultTag defines the default message tag.
	DefaultTag string

	// DefaultFormat defines the default message format.
	DefaultFormat MsgFormat

	// AllowUntagged defines if untagged messages are allowed,
	// which are sent to all configured apprise endpoints.
	AllowUntagged bool

	client     *http.Client
	clientLock sync.Mutex
}

// Message represents the message to be sent to the Apprise API.
type Message struct {
	// Title is an optional title to go along with the body.
	Title string `json:"title,omitempty"`

	// Body is the main message content. This is the only required field.
	Body string `json:"body"`

	// Type defines the message type you want to send as.
	// The valid options are info, success, warning, and failure.
	// If no type is specified then info is the default value used.
	Type MsgType `json:"type,omitempty"`

	// Tag is used to notify only those tagged accordingly.
	// Use a comma (,) to OR your tags and a space ( ) to AND them.
	Tag string `json:"tag,omitempty"`

	// Format optionally identifies the text format of the data you're feeding Apprise.
	// The valid options are text, markdown, html.
	// The default value if nothing is specified is text.
	Format MsgFormat `json:"format,omitempty"`
}

// MsgType defines the message type.
type MsgType string

// Message Types.
const (
	TypeInfo    MsgType = "info"
	TypeSuccess MsgType = "success"
	TypeWarning MsgType = "warning"
	TypeFailure MsgType = "failure"
)

// MsgFormat defines the message format.
type MsgFormat string

// Message Formats.
const (
	FormatText     MsgFormat = "text"
	FormatMarkdown MsgFormat = "markdown"
	FormatHTML     MsgFormat = "html"
)

type errorResponse struct {
	Error string `json:"error"`
}

// Send sends a message to the Apprise API.
func (n *Notifier) Send(ctx context.Context, m *Message) error {
	// Check if the message has a body.
	if m.Body == "" {
		return errors.New("the message must have a body")
	}

	// Apply notifier defaults.
	n.applyDefaults(m)

	// Check if the message is tagged.
	if m.Tag == "" && !n.AllowUntagged {
		return errors.New("the message must have a tag")
	}

	// Marshal the message to JSON.
	payload, err := json.Marshal(m)
	if err != nil {
		return fmt.Errorf("failed to marshal message: %w", err)
	}

	// Create request.
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, n.URL, bytes.NewReader(payload))
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}
	request.Header.Set("Content-Type", "application/json")

	// Send message to API.
	resp, err := n.getClient().Do(request)
	if err != nil {
		return fmt.Errorf("failed to send message: %w", err)
	}
	defer resp.Body.Close() //nolint:errcheck,gosec
	switch resp.StatusCode {
	case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted:
		return nil
	default:
		// Try to tease body contents.
		if body, err := io.ReadAll(resp.Body); err == nil && len(body) > 0 {
			// Try to parse json response.
			errorResponse := &errorResponse{}
			if err := json.Unmarshal(body, errorResponse); err == nil && errorResponse.Error != "" {
				return fmt.Errorf("failed to send message: apprise returned %q with an error message: %s", resp.Status, errorResponse.Error)
			}
			return fmt.Errorf("failed to send message: %s (body teaser: %s)", resp.Status, utils.SafeFirst16Bytes(body))
		}
		return fmt.Errorf("failed to send message: %s", resp.Status)
	}
}

func (n *Notifier) applyDefaults(m *Message) {
	if m.Type == "" {
		m.Type = n.DefaultType
	}
	if m.Tag == "" {
		m.Tag = n.DefaultTag
	}
	if m.Format == "" {
		m.Format = n.DefaultFormat
	}
}

// SetClient sets a custom http client for accessing the Apprise API.
func (n *Notifier) SetClient(client *http.Client) {
	n.clientLock.Lock()
	defer n.clientLock.Unlock()

	n.client = client
}

func (n *Notifier) getClient() *http.Client {
	n.clientLock.Lock()
	defer n.clientLock.Unlock()

	// Create client if needed.
	if n.client == nil {
		n.client = &http.Client{}
	}

	return n.client
}