package push

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

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

const fcmMessagingScope = "https://www.googleapis.com/auth/firebase.messaging"

// fcmClient sends notifications through the FCM HTTP v1 API.
type fcmClient struct {
	projectID   string
	tokenSource oauth2.TokenSource
	http        *http.Client
}

// newFCMClient returns a configured client, or nil (no error) when FCM is not
// configured. An error is returned only when provided credentials are invalid.
func newFCMClient(cfg Config) (*fcmClient, error) {
	raw := strings.TrimSpace(cfg.FCMServiceAccountJSON)
	if raw == "" {
		return nil, nil
	}

	creds, err := google.CredentialsFromJSON(context.Background(), []byte(raw), fcmMessagingScope)
	if err != nil {
		return nil, fmt.Errorf("parse fcm service account: %w", err)
	}

	projectID := strings.TrimSpace(cfg.FCMProjectID)
	if projectID == "" {
		projectID = creds.ProjectID
	}
	if projectID == "" {
		// Fall back to parsing the JSON directly for project_id.
		var sa struct {
			ProjectID string `json:"project_id"`
		}
		_ = json.Unmarshal([]byte(raw), &sa)
		projectID = strings.TrimSpace(sa.ProjectID)
	}
	if projectID == "" {
		return nil, fmt.Errorf("fcm project id missing (set FCM_PROJECT_ID or include project_id in service account)")
	}

	return &fcmClient{
		projectID:   projectID,
		tokenSource: creds.TokenSource,
		http:        &http.Client{Timeout: 10 * time.Second},
	}, nil
}

type fcmMessage struct {
	Message fcmMessageBody `json:"message"`
}

type fcmMessageBody struct {
	Token        string            `json:"token"`
	Notification fcmNotification   `json:"notification"`
	Data         map[string]string `json:"data,omitempty"`
}

type fcmNotification struct {
	Title string `json:"title,omitempty"`
	Body  string `json:"body,omitempty"`
}

func (c *fcmClient) send(ctx context.Context, deviceToken string, n Notification) error {
	accessToken, err := c.tokenSource.Token()
	if err != nil {
		return fmt.Errorf("fcm access token: %w", err)
	}

	payload := fcmMessage{
		Message: fcmMessageBody{
			Token: deviceToken,
			Notification: fcmNotification{
				Title: n.Title,
				Body:  n.Body,
			},
			Data: n.Data,
		},
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	url := fmt.Sprintf("https://fcm.googleapis.com/v1/projects/%s/messages:send", c.projectID)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken.AccessToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return nil
	}

	respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	// A 404 (NOT_FOUND) or UNREGISTERED error means the token is dead.
	if resp.StatusCode == http.StatusNotFound || fcmIsUnregistered(respBody) {
		return errTokenUnregistered
	}
	return fmt.Errorf("fcm send status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}

func fcmIsUnregistered(body []byte) bool {
	var parsed struct {
		Error struct {
			Status  string `json:"status"`
			Details []struct {
				ErrorCode string `json:"errorCode"`
			} `json:"details"`
		} `json:"error"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		return false
	}
	if parsed.Error.Status == "NOT_FOUND" {
		return true
	}
	for _, d := range parsed.Error.Details {
		if d.ErrorCode == "UNREGISTERED" {
			return true
		}
	}
	return false
}
