package pushnotify

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

const (
	expoPushAPIURL = "https://exp.host/--/api/v2/push/send"
	// expoMaxBatchSize is the documented per-request cap on push messages.
	expoMaxBatchSize = 100

	// Ticket error codes returned by Expo Push API. The only one we act on is
	// DeviceNotRegistered — others are logged but not retried (per product call).
	ExpoErrorDeviceNotRegistered = "DeviceNotRegistered"
)

// Sound defaults to "default" when empty (set in Send).
type Message struct {
	To    string         `json:"to"`
	Title string         `json:"title,omitempty"`
	Body  string         `json:"body,omitempty"`
	Data  map[string]any `json:"data,omitempty"`
	Sound string         `json:"sound,omitempty"`
}

// Status is "ok" or "error". On error, Details["error"] carries the code
// (e.g. "DeviceNotRegistered", "MessageTooBig", "InvalidCredentials").
type Ticket struct {
	Status  string         `json:"status"`
	ID      string         `json:"id,omitempty"`
	Message string         `json:"message,omitempty"`
	Details map[string]any `json:"details,omitempty"`
}

// ExpoClient does NOT poll receipts and does NOT retry — fire-and-forget,
// with DeviceNotRegistered handled out-of-band by the notifier.
type ExpoClient struct {
	http        *http.Client
	accessToken string
}

func NewExpoClient(accessToken string) *ExpoClient {
	return &ExpoClient{
		http:        &http.Client{Timeout: 15 * time.Second},
		accessToken: accessToken,
	}
}

type expoSendResponse struct {
	Data   []Ticket         `json:"data"`
	Errors []map[string]any `json:"errors,omitempty"`
}

// Send batches msgs (cap expoMaxBatchSize) and preserves input order:
// tickets[i] corresponds to msgs[i]. A request-level failure aborts the
// remaining batches; tickets already collected are returned with the error.
func (c *ExpoClient) Send(ctx context.Context, msgs []Message) ([]Ticket, error) {
	if len(msgs) == 0 {
		return nil, nil
	}

	for i := range msgs {
		if msgs[i].Sound == "" {
			msgs[i].Sound = "default"
		}
	}

	tickets := make([]Ticket, 0, len(msgs))
	for start := 0; start < len(msgs); start += expoMaxBatchSize {
		end := start + expoMaxBatchSize
		if end > len(msgs) {
			end = len(msgs)
		}

		batch := msgs[start:end]
		batchTickets, err := c.sendBatch(ctx, batch)
		tickets = append(tickets, batchTickets...)
		if err != nil {
			return tickets, fmt.Errorf("expo push batch [%d:%d]: %w", start, end, err)
		}
	}
	return tickets, nil
}

func (c *ExpoClient) sendBatch(ctx context.Context, batch []Message) ([]Ticket, error) {
	body, err := json.Marshal(batch)
	if err != nil {
		return nil, fmt.Errorf("marshal batch: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, expoPushAPIURL, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Encoding", "gzip, deflate")
	if c.accessToken != "" {
		req.Header.Set("Authorization", "Bearer "+c.accessToken)
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("expo push api returned %d: %s", resp.StatusCode, truncate(string(raw), 512))
	}

	var parsed expoSendResponse
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	if len(parsed.Data) != len(batch) {
		return parsed.Data, fmt.Errorf("expo returned %d tickets for %d messages", len(parsed.Data), len(batch))
	}
	return parsed.Data, nil
}

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