// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package ntfy // import "miniflux.app/v2/internal/integration/ntfy"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"time"

	"miniflux.app/v2/internal/config"
	"miniflux.app/v2/internal/model"
	"miniflux.app/v2/internal/version"
)

const (
	defaultClientTimeout = 10 * time.Second
	defaultNtfyURL       = "https://ntfy.sh"
)

type Client struct {
	ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
	ntfyInternalLinks                                                         bool
	ntfyPriority                                                              int
}

func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyInternalLinks bool, ntfyPriority int) *Client {
	if ntfyURL == "" {
		ntfyURL = defaultNtfyURL
	}
	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyInternalLinks, ntfyPriority}
}

func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
	for _, entry := range entries {
		ntfyMessage := &ntfyMessage{
			Topic:    c.ntfyTopic,
			Message:  entry.Title,
			Title:    feed.Title,
			Priority: c.ntfyPriority,
			Click:    entry.URL,
		}

		if c.ntfyIconURL != "" {
			ntfyMessage.Icon = c.ntfyIconURL
		}

		if c.ntfyInternalLinks {
			url, err := url.Parse(config.Opts.BaseURL())
			if err != nil {
				slog.Error("Unable to parse base URL", slog.Any("error", err))
			} else {
				ntfyMessage.Click = fmt.Sprintf("%s%s%d", url, "/unread/entry/", entry.ID)
			}
		}

		slog.Debug("Sending Ntfy message",
			slog.String("url", c.ntfyURL),
			slog.String("topic", c.ntfyTopic),
			slog.Int("priority", ntfyMessage.Priority),
			slog.String("message", ntfyMessage.Message),
			slog.String("entry_url", ntfyMessage.Click),
		)

		if err := c.makeRequest(ntfyMessage); err != nil {
			return err
		}
	}

	return nil
}

func (c *Client) makeRequest(payload any) error {
	requestBody, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("ntfy: unable to encode request body: %v", err)
	}

	request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
	if err != nil {
		return fmt.Errorf("ntfy: unable to create request: %v", err)
	}

	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("User-Agent", "Miniflux/"+version.Version)

	// See https://docs.ntfy.sh/publish/#access-tokens
	if c.ntfyApiToken != "" {
		request.Header.Set("Authorization", "Bearer "+c.ntfyApiToken)
	}

	// See https://docs.ntfy.sh/publish/#username-password
	if c.ntfyUsername != "" && c.ntfyPassword != "" {
		request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
	}

	httpClient := &http.Client{Timeout: defaultClientTimeout}
	response, err := httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("ntfy: unable to send request: %v", err)
	}
	defer response.Body.Close()

	if response.StatusCode >= 400 {
		return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
	}

	return nil
}

// See https://docs.ntfy.sh/publish/#publish-as-json
type ntfyMessage struct {
	Topic    string       `json:"topic"`
	Message  string       `json:"message"`
	Title    string       `json:"title"`
	Tags     []string     `json:"tags,omitempty"`
	Priority int          `json:"priority,omitempty"`
	Icon     string       `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
	Click    string       `json:"click,omitempty"`
	Actions  []ntfyAction `json:"actions,omitempty"`
}

// See https://docs.ntfy.sh/publish/#action-buttons
type ntfyAction struct {
	Action string `json:"action"`
	Label  string `json:"label"`
	URL    string `json:"url"`
}
