package message_pusher

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"
	"wdd.io/agent-go/logger"
)

var (
	topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
	log        = logger.Log
)

const (
	maxResponseBytes = 4096
)

type Client struct {
	config *Config
}

// Message is a struct that represents a ntfy message
type Message struct { // TODO combine with server.message
	ID         string
	Event      string
	Time       int64
	Topic      string
	Message    string
	Title      string
	Priority   int
	Tags       []string
	Click      string
	Icon       string
	Attachment *Attachment

	// Additional fields
	TopicURL       string
	SubscriptionID string
	Raw            string
}

// Attachment represents a message attachment
type Attachment struct {
	Name    string `json:"name"`
	Type    string `json:"type,omitempty"`
	Size    int64  `json:"size,omitempty"`
	Expires int64  `json:"expires,omitempty"`
	URL     string `json:"url"`
	Owner   string `json:"-"` // IP address of uploader, used for rate limiting
}

// New creates a new Client using a given Config
func New(config *Config) *Client {
	return &Client{
		config: config,
	}
}

func NewDefaultClient() *Client {
	defaultConfig := NewDefaultConfig()
	return New(defaultConfig)
}

func (c *Client) PublishDefault(message bytes.Buffer, options []PublishOption) (*Message, error) {
	if c.config.DefaultTopic == "" {
		return nil, errors.New("[PublishDefault] - topic empty")
	}
	// parse default
	options = c.parseConfigToOption(options)

	return c.PublishReader(c.config.DefaultTopic, bytes.NewReader(message.Bytes()), options)
}

// Publish sends a message to a specific topic, optionally using options.
// See PublishReader for details.
func (c *Client) Publish(topic, message string, options []PublishOption) (*Message, error) {
	return c.PublishReader(topic, strings.NewReader(message), options)
}

// PublishReader sends a message to a specific topic, optionally using options.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
// WithNoFirebase, and the generic WithHeader.
func (c *Client) PublishReader(topic string, body io.Reader, options []PublishOption) (*Message, error) {
	topicURL, err := c.expandTopicURL(topic)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", topicURL, body)
	if err != nil {
		return nil, err
	}
	for _, option := range options {
		if err := option(req); err != nil {
			return nil, err
		}
	}
	log.DebugF("%s Publishing message with headers %s", topicURL, req.Header)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(strings.TrimSpace(string(b)))
	}
	m, err := toMessage(string(b), topicURL, "")
	if err != nil {
		return nil, err
	}
	return m, nil
}

func (c *Client) expandTopicURL(topic string) (string, error) {
	if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
		return topic, nil
	} else if strings.Contains(topic, "/") {
		return fmt.Sprintf("https://%s", topic), nil
	}
	if !topicRegex.MatchString(topic) {
		return "", fmt.Errorf("invalid topic name: %s", topic)
	}
	return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
}

func (c *Client) parseConfigToOption(options []PublishOption) []PublishOption {
	config := c.config

	if config.DefaultToken != "" {
		options = append(options, WithBearerAuth(config.DefaultToken))
	} else if config.DefaultUser != "" {
		if *config.DefaultPassword != "" {
			options = append(options, WithBasicAuth(config.DefaultUser, *config.DefaultPassword))
		} else {
			log.ErrorF("[parseConfigToOption] - default password is empty!")
		}
	}

	return options
}

func toMessage(s, topicURL, subscriptionID string) (*Message, error) {
	var m *Message
	if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
		return nil, err
	}
	m.TopicURL = topicURL
	m.SubscriptionID = subscriptionID
	m.Raw = s
	return m, nil
}
