// Package notify delivers sentry alert messages to downstream channels.
// Currently only Lark custom-robot webhooks are supported.
package notify

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"
)

// LarkClient posts text messages to a Lark custom-robot webhook with
// HMAC-signed timestamps per the Lark reference implementation.
type LarkClient struct {
	url           string
	signingSecret string
	http          *http.Client
	now           func() time.Time // overridable for tests
}

// NewLarkClient constructs a client. signingSecret may be empty — in that
// case the client posts without the sign fields, which Lark webhooks allow
// if the webhook was configured without signing. In production the secret
// is loaded from Secrets Manager.
func NewLarkClient(url, signingSecret string, timeout time.Duration) *LarkClient {
	return &LarkClient{
		url:           url,
		signingSecret: signingSecret,
		http:          &http.Client{Timeout: timeout},
		now:           time.Now,
	}
}

type larkTextMsg struct {
	Timestamp string  `json:"timestamp,omitempty"`
	Sign      string  `json:"sign,omitempty"`
	MsgType   string  `json:"msg_type"`
	Content   larkTxt `json:"content"`
}

type larkTxt struct {
	Text string `json:"text"`
}

// SendText posts a plain-text message to the configured webhook.
// Returns an error on final failure; the caller is responsible for archiving
// the alert payload to S3 regardless of delivery outcome.
func (c *LarkClient) SendText(ctx context.Context, text string) error {
	backoffs := []time.Duration{0, 1 * time.Second, 3 * time.Second}
	var lastErr error
	for attempt, wait := range backoffs {
		if wait > 0 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(wait):
			}
		}

		body, err := c.buildBody(text)
		if err != nil {
			return err
		}

		req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
		if err != nil {
			return err
		}
		req.Header.Set("Content-Type", "application/json")

		resp, err := c.http.Do(req)
		if err != nil {
			lastErr = err
			continue
		}
		respBody, _ := io.ReadAll(resp.Body)
		_ = resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return nil
		}
		lastErr = fmt.Errorf("lark: attempt %d status %d: %s", attempt+1, resp.StatusCode, truncate(string(respBody), 200))
	}
	if lastErr == nil {
		lastErr = errors.New("lark: exhausted retries")
	}
	return lastErr
}

func (c *LarkClient) buildBody(text string) ([]byte, error) {
	msg := larkTextMsg{
		MsgType: "text",
		Content: larkTxt{Text: text},
	}
	if c.signingSecret != "" {
		ts := c.now().Unix()
		sign, err := genSign(c.signingSecret, ts)
		if err != nil {
			return nil, fmt.Errorf("lark: sign: %w", err)
		}
		msg.Timestamp = fmt.Sprintf("%d", ts)
		msg.Sign = sign
	}
	return json.Marshal(msg)
}

// genSign computes a Lark custom-robot signature per the reference implementation:
//
//	stringToSign = fmt.Sprintf("%d\n%s", timestamp, secret)
//	hmac = HMAC-SHA256(key=stringToSign, data=[])
//	sign = base64(hmac)
func genSign(secret string, timestamp int64) (string, error) {
	stringToSign := fmt.Sprintf("%v\n%v", timestamp, secret)
	h := hmac.New(sha256.New, []byte(stringToSign))
	if _, err := h.Write(nil); err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}
