package push

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

	"github.com/rs/zerolog/log"
	"golang.org/x/oauth2/google"

	"github.com/treytartt/honeydue-api/internal/config"
)

const (
	// fcmV1EndpointFmt is the FCM HTTP v1 API endpoint template.
	fcmV1EndpointFmt = "https://fcm.googleapis.com/v1/projects/%s/messages:send"

	// fcmScope is the OAuth 2.0 scope required for FCM HTTP v1 API.
	fcmScope = "https://www.googleapis.com/auth/firebase.messaging"
)

// FCMClient handles communication with Firebase Cloud Messaging
// using the HTTP v1 API and OAuth 2.0 service account authentication.
type FCMClient struct {
	projectID  string
	endpoint   string
	httpClient *http.Client
}

// --- Request types (FCM v1 API) ---

// fcmV1Request is the top-level request body for the FCM v1 API.
type fcmV1Request struct {
	Message *fcmV1Message `json:"message"`
}

// fcmV1Message represents a single FCM v1 message.
type fcmV1Message struct {
	Token        string            `json:"token"`
	Notification *FCMNotification  `json:"notification,omitempty"`
	Data         map[string]string `json:"data,omitempty"`
	Android      *fcmAndroidConfig `json:"android,omitempty"`
}

// FCMNotification represents the notification payload.
type FCMNotification struct {
	Title string `json:"title,omitempty"`
	Body  string `json:"body,omitempty"`
}

// fcmAndroidConfig provides Android-specific message configuration.
type fcmAndroidConfig struct {
	Priority string `json:"priority,omitempty"`
}

// --- Response types (FCM v1 API) ---

// fcmV1Response is the successful response from the FCM v1 API.
type fcmV1Response struct {
	Name string `json:"name"` // e.g. "projects/myproject/messages/0:1234567890"
}

// fcmV1ErrorResponse is the error response from the FCM v1 API.
type fcmV1ErrorResponse struct {
	Error fcmV1Error `json:"error"`
}

// fcmV1Error contains the structured error details.
type fcmV1Error struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Status  string          `json:"status"`
	Details json.RawMessage `json:"details,omitempty"`
}

// --- Error types ---

// FCMErrorCode represents well-known FCM v1 error codes for programmatic handling.
type FCMErrorCode string

const (
	FCMErrUnregistered     FCMErrorCode = "UNREGISTERED"
	FCMErrQuotaExceeded    FCMErrorCode = "QUOTA_EXCEEDED"
	FCMErrUnavailable      FCMErrorCode = "UNAVAILABLE"
	FCMErrInternal         FCMErrorCode = "INTERNAL"
	FCMErrInvalidArgument  FCMErrorCode = "INVALID_ARGUMENT"
	FCMErrSenderIDMismatch FCMErrorCode = "SENDER_ID_MISMATCH"
	FCMErrThirdPartyAuth   FCMErrorCode = "THIRD_PARTY_AUTH_ERROR"
)

// FCMSendError is a structured error returned when an individual FCM send fails.
type FCMSendError struct {
	Token      string
	StatusCode int
	ErrorCode  FCMErrorCode
	Message    string
}

func (e *FCMSendError) Error() string {
	return fmt.Sprintf("FCM send failed for token %s: %s (status %d, code %s)",
		truncateToken(e.Token), e.Message, e.StatusCode, e.ErrorCode)
}

// IsUnregistered returns true if the device token is no longer valid and should be removed.
func (e *FCMSendError) IsUnregistered() bool {
	return e.ErrorCode == FCMErrUnregistered
}

// --- OAuth 2.0 transport ---

// oauth2BearerTransport is an http.RoundTripper that attaches an OAuth 2.0 Bearer
// token to every outgoing request. The token source handles refresh automatically.
type oauth2BearerTransport struct {
	base        http.RoundTripper
	getToken    func() (string, error)
}

func (t *oauth2BearerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	accessToken, err := t.getToken()
	if err != nil {
		return nil, fmt.Errorf("failed to obtain OAuth 2.0 token for FCM: %w", err)
	}
	r := req.Clone(req.Context())
	r.Header.Set("Authorization", "Bearer "+accessToken)
	return t.base.RoundTrip(r)
}

// --- Client construction ---

// NewFCMClient creates a new FCM client using the HTTP v1 API with OAuth 2.0
// service account authentication. It accepts either a path to a service account
// JSON file or the raw JSON content directly via config.
func NewFCMClient(cfg *config.PushConfig) (*FCMClient, error) {
	if cfg.FCMProjectID == "" {
		return nil, fmt.Errorf("FCM project ID not configured (set FCM_PROJECT_ID)")
	}

	credJSON, err := resolveServiceAccountJSON(cfg)
	if err != nil {
		return nil, err
	}

	// Create OAuth 2.0 credentials with the FCM messaging scope.
	// The google library handles automatic token refresh.
	creds, err := google.CredentialsFromJSON(context.Background(), credJSON, fcmScope)
	if err != nil {
		return nil, fmt.Errorf("failed to parse FCM service account credentials: %w", err)
	}

	// Build an HTTP client that automatically attaches and refreshes OAuth tokens.
	transport := &oauth2BearerTransport{
		base: http.DefaultTransport,
		getToken: func() (string, error) {
			tok, err := creds.TokenSource.Token()
			if err != nil {
				return "", err
			}
			return tok.AccessToken, nil
		},
	}

	httpClient := &http.Client{
		Timeout:   15 * time.Second,
		Transport: transport,
	}

	endpoint := fmt.Sprintf(fcmV1EndpointFmt, cfg.FCMProjectID)

	log.Info().
		Str("project_id", cfg.FCMProjectID).
		Str("endpoint", endpoint).
		Msg("FCM v1 client initialized with OAuth 2.0")

	return &FCMClient{
		projectID:  cfg.FCMProjectID,
		endpoint:   endpoint,
		httpClient: httpClient,
	}, nil
}

// resolveServiceAccountJSON returns the service account JSON bytes from config.
func resolveServiceAccountJSON(cfg *config.PushConfig) ([]byte, error) {
	if cfg.FCMServiceAccountJSON != "" {
		return []byte(cfg.FCMServiceAccountJSON), nil
	}
	if cfg.FCMServiceAccountPath != "" {
		data, err := os.ReadFile(cfg.FCMServiceAccountPath)
		if err != nil {
			return nil, fmt.Errorf("failed to read FCM service account file %s: %w", cfg.FCMServiceAccountPath, err)
		}
		return data, nil
	}
	return nil, fmt.Errorf("FCM service account not configured (set FCM_SERVICE_ACCOUNT_PATH or FCM_SERVICE_ACCOUNT_JSON)")
}

// --- Sending ---

// Send sends a push notification to Android devices via the FCM HTTP v1 API.
// The v1 API requires one request per device token, so this iterates over all tokens.
// The method signature is kept identical to the previous legacy implementation
// so callers do not need to change.
func (c *FCMClient) Send(ctx context.Context, tokens []string, title, message string, data map[string]string) error {
	if len(tokens) == 0 {
		return nil
	}

	var sendErrors []error
	successCount := 0

	for _, token := range tokens {
		err := c.sendOne(ctx, token, title, message, data)
		if err != nil {
			log.Error().
				Err(err).
				Str("token", truncateToken(token)).
				Msg("FCM v1 notification failed")
			sendErrors = append(sendErrors, err)
			continue
		}

		successCount++
		log.Debug().
			Str("token", truncateToken(token)).
			Msg("FCM v1 notification sent successfully")
	}

	log.Info().
		Int("total", len(tokens)).
		Int("success", successCount).
		Int("failed", len(sendErrors)).
		Msg("FCM v1 batch send complete")

	if len(sendErrors) > 0 && successCount == 0 {
		return fmt.Errorf("all FCM notifications failed: first error: %w", sendErrors[0])
	}

	return nil
}

// sendOne sends a single FCM v1 message to one device token.
func (c *FCMClient) sendOne(ctx context.Context, token, title, message string, data map[string]string) error {
	reqBody := fcmV1Request{
		Message: &fcmV1Message{
			Token: token,
			Notification: &FCMNotification{
				Title: title,
				Body:  message,
			},
			Data: data,
			Android: &fcmAndroidConfig{
				Priority: "HIGH",
			},
		},
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("failed to marshal FCM v1 message: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("failed to create FCM v1 request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send FCM v1 request: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read FCM v1 response body: %w", err)
	}

	if resp.StatusCode == http.StatusOK {
		return nil
	}

	// Parse the error response for structured error information.
	return parseFCMV1Error(token, resp.StatusCode, respBody)
}

// parseFCMV1Error extracts a structured FCMSendError from the v1 API error response.
func parseFCMV1Error(token string, statusCode int, body []byte) *FCMSendError {
	var errResp fcmV1ErrorResponse
	if err := json.Unmarshal(body, &errResp); err != nil {
		return &FCMSendError{
			Token:      token,
			StatusCode: statusCode,
			Message:    fmt.Sprintf("unparseable error response: %s", string(body)),
		}
	}

	return &FCMSendError{
		Token:      token,
		StatusCode: statusCode,
		ErrorCode:  FCMErrorCode(errResp.Error.Status),
		Message:    errResp.Error.Message,
	}
}
