package notification

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

// NtfyConfig is the per-channel JSON shape stored AEAD-encrypted in
// notification_channels.config. AccessToken takes precedence over
// (Username, Password) when both are set; supply one or the other for
// self-hosted ntfy that requires auth.
type NtfyConfig struct {
	ServerURL   string `json:"server_url"`
	Topic       string `json:"topic"`
	AccessToken string `json:"access_token,omitempty"`
	Username    string `json:"username,omitempty"`
	Password    string `json:"password,omitempty"`
}

// NtfyChannel delivers alerts to an ntfy server using POST with
// ntfy-specific headers (Title, Priority, Tags, Click). One instance
// per configured channel row. Reused across sends — http.Client is
// goroutine-safe.
type NtfyChannel struct {
	cfg             NtfyConfig
	defaultPriority string // "min"/"low"/"default"/"high"/"urgent" or ""
	client          *http.Client
}

// NewNtfyChannel builds an ntfy channel with a 5s http.Client timeout.
// defaultPriority is the channel-configured fallback when no
// severity-specific mapping applies; pass "" to use the built-in
// fallbacks (4 for warning, 3 for everything else).
func NewNtfyChannel(cfg NtfyConfig, defaultPriority string) *NtfyChannel {
	if cfg.ServerURL == "" {
		cfg.ServerURL = "https://ntfy.sh"
	}
	return &NtfyChannel{
		cfg:             cfg,
		defaultPriority: defaultPriority,
		client:          &http.Client{Timeout: 5 * time.Second},
	}
}

// Kind returns "ntfy" for log enrichment and dispatcher routing.
func (c *NtfyChannel) Kind() string { return "ntfy" }

// Send delivers the payload as a plain-text POST to <server>/<topic>
// with ntfy headers. Returns (statusCode, latency, err). 4xx/5xx
// responses are returned as errors with the status code set.
func (c *NtfyChannel) Send(ctx context.Context, p Payload) (int, time.Duration, error) {
	server := strings.TrimRight(c.cfg.ServerURL, "/")
	url := server + "/" + c.cfg.Topic

	// Body carries the event verb so the body alone is unambiguous when
	// it shows up on a phone lockscreen without the title.
	body := p.Message
	switch p.Event {
	case EventResolved:
		body = "Resolved · " + p.Message
	case EventAcknowledged:
		body = "Acknowledged · " + p.Message
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBufferString(body))
	if err != nil {
		return 0, 0, fmt.Errorf("ntfy: build request: %w", err)
	}

	// Title prefix tracks the event so raise vs ack vs resolve are
	// visually distinct in the ntfy notification list.
	verb := "raised"
	switch p.Event {
	case EventAcknowledged:
		verb = "ack"
	case EventResolved:
		verb = "resolved"
	case EventTest:
		verb = "test"
	}
	req.Header.Set("Content-Type", "text/plain")
	req.Header.Set("Title", fmt.Sprintf("[%s · %s] %s %s", verb, p.Severity, p.HostName, p.Kind))
	req.Header.Set("Tags", verb+","+p.Severity+","+p.Kind)
	req.Header.Set("Priority", priorityForSeverity(p.Severity, c.defaultPriority))
	if p.Link != "" {
		req.Header.Set("Click", p.Link)
	}
	switch {
	case c.cfg.AccessToken != "":
		req.Header.Set("Authorization", "Bearer "+c.cfg.AccessToken)
	case c.cfg.Username != "":
		creds := c.cfg.Username + ":" + c.cfg.Password
		req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(creds)))
	}

	t0 := time.Now()
	res, err := c.client.Do(req)
	latency := time.Since(t0)
	if err != nil {
		return 0, latency, fmt.Errorf("ntfy: do: %w", err)
	}
	defer func() { _ = res.Body.Close() }()
	// Drain body to keep the connection reusable.
	_, _ = io.Copy(io.Discard, res.Body)
	if res.StatusCode >= 400 {
		return res.StatusCode, latency, fmt.Errorf("ntfy: http %d", res.StatusCode)
	}
	return res.StatusCode, latency, nil
}

// priorityForSeverity maps a severity string to an ntfy numeric priority
// string. critical always returns "5" regardless of defaultPri. For
// other severities, defaultPri is returned when non-empty, otherwise
// "4" for warning and "3" for everything else.
func priorityForSeverity(severity, defaultPri string) string {
	switch severity {
	case "critical":
		return "5"
	case "warning":
		if defaultPri != "" {
			return defaultPri
		}
		return "4"
	default:
		if defaultPri != "" {
			return defaultPri
		}
		return "3"
	}
}
