package pushover

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

var deviceNameRegexp *regexp.Regexp

func init() {
	deviceNameRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]{1,25}$`)
}

// Message represents a pushover message.
type Message struct {
	// Required
	Message string

	// Optional
	Title       string
	Priority    int
	URL         string
	URLTitle    string
	Timestamp   int64
	Retry       time.Duration
	Expire      time.Duration
	CallbackURL string
	DeviceName  string
	Sound       string
	HTML        bool
	Monospace   bool

	// attachment
	attachment io.Reader
}

// NewMessage returns a simple new message.
func NewMessage(message string) *Message {
	return &Message{Message: message}
}

// NewMessageWithTitle returns a simple new message with a title.
func NewMessageWithTitle(message, title string) *Message {
	return &Message{Message: message, Title: title}
}

// AddAttachment adds an attachment to the message it's programmer's
// responsibility to close the reader.
func (m *Message) AddAttachment(attachment io.Reader) error {
	m.attachment = attachment
	return nil
}

// Validate the message values.
func (m *Message) validate() error {
	// Message should no be empty
	if m.Message == "" {
		return ErrMessageEmpty
	}

	// Validate message length
	if utf8.RuneCountInString(m.Message) > MessageMaxLength {
		return ErrMessageTooLong
	}

	// Validate Title field length
	if utf8.RuneCountInString(m.Title) > MessageTitleMaxLength {
		return ErrMessageTitleTooLong
	}

	// Validate URL field
	if utf8.RuneCountInString(m.URL) > MessageURLMaxLength {
		return ErrMessageURLTooLong
	}

	// Validate URL title field
	if utf8.RuneCountInString(m.URLTitle) > MessageURLTitleMaxLength {
		return ErrMessageURLTitleTooLong
	}

	// URLTitle should not be set with an empty URL
	if m.URL == "" && m.URLTitle != "" {
		return ErrEmptyURL
	}

	// Validate priorities
	if m.Priority > PriorityEmergency || m.Priority < PriorityLowest {
		return ErrInvalidPriority
	}

	// Validate emergency priority
	if m.Priority == PriorityEmergency {
		if m.Retry == 0 || m.Expire == 0 {
			return ErrMissingEmergencyParameter
		}
	}

	// Test device name
	if m.DeviceName != "" {
		// Accept comma separated device names
		devices := strings.Split(m.DeviceName, ",")
		for _, d := range devices {
			if !deviceNameRegexp.MatchString(d) {
				return ErrInvalidDeviceName
			}
		}
	}

	return nil
}

// Return a map filled with the relevant data.
func (m *Message) toMap(pToken, rToken string) map[string]string {
	ret := map[string]string{
		"token":    pToken,
		"user":     rToken,
		"message":  m.Message,
		"priority": strconv.Itoa(m.Priority),
	}

	if m.Title != "" {
		ret["title"] = m.Title
	}

	if m.URL != "" {
		ret["url"] = m.URL
	}

	if m.URLTitle != "" {
		ret["url_title"] = m.URLTitle
	}

	if m.Sound != "" {
		ret["sound"] = m.Sound
	}

	if m.DeviceName != "" {
		ret["device"] = m.DeviceName
	}

	if m.Timestamp != 0 {
		ret["timestamp"] = strconv.FormatInt(m.Timestamp, 10)
	}

	if m.HTML {
		ret["html"] = "1"
	}

	if m.Monospace {
		ret["monospace"] = "1"
	}

	if m.Priority == PriorityEmergency {
		ret["retry"] = strconv.FormatFloat(m.Retry.Seconds(), 'f', -1, 64)
		ret["expire"] = strconv.FormatFloat(m.Expire.Seconds(), 'f', -1, 64)
		if m.CallbackURL != "" {
			ret["callback"] = m.CallbackURL
		}
	}

	return ret
}

// Send sends the message using the pushover and the recipient tokens.
func (m *Message) send(pToken, rToken string) (*Response, error) {
	url := fmt.Sprintf("%s/messages.json", APIEndpoint)

	var f func(string, string, string) (*http.Request, error)
	if m.attachment == nil {
		// Use a URL-encoded request if there's no need to attach files
		f = m.urlEncodedRequest
	} else {
		// Use a multipart request if a file should be sent
		f = m.multipartRequest
	}

	// Post the from and check the headers of the response
	req, err := f(pToken, rToken, url)
	if err != nil {
		return nil, err
	}

	resp := &Response{}
	if err := do(req, resp, true); err != nil {
		return nil, err
	}

	return resp, nil
}

// multipartRequest returns a new multipart POST request with a file attached.
func (m *Message) multipartRequest(pToken, rToken, url string) (*http.Request, error) {
	body := &bytes.Buffer{}

	if m.attachment == nil {
		return nil, ErrMissingAttachment
	}

	// Write the body as multipart form data
	w := multipart.NewWriter(body)

	// Write the file in the body
	fw, err := w.CreateFormFile("attachment", "attachment")
	if err != nil {
		return nil, err
	}

	written, err := io.Copy(fw, m.attachment)
	if err != nil {
		return nil, err
	}

	if written > MessageMaxAttachmentByte {
		return nil, ErrMessageAttachmentTooLarge
	}

	// Handle params
	for k, v := range m.toMap(pToken, rToken) {
		if err := w.WriteField(k, v); err != nil {
			return nil, err
		}
	}

	if err := w.Close(); err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", w.FormDataContentType())

	return req, nil
}

// urlEncodedRequest returns a new url encoded request.
func (m *Message) urlEncodedRequest(pToken, rToken, endpoint string) (*http.Request, error) {
	return newURLEncodedRequest("POST", endpoint, m.toMap(pToken, rToken))
}
