package google

import (
	"context"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"go.uber.org/zap"

	"go.autokitteh.dev/autokitteh/integrations/common"
	"go.autokitteh.dev/autokitteh/integrations/google/calendar"
	"go.autokitteh.dev/autokitteh/integrations/google/drive"
	"go.autokitteh.dev/autokitteh/integrations/google/forms"
	"go.autokitteh.dev/autokitteh/integrations/google/gmail"
	"go.autokitteh.dev/autokitteh/integrations/google/vars"
	"go.autokitteh.dev/autokitteh/integrations/internal/extrazap"
	"go.autokitteh.dev/autokitteh/internal/kittehs"
	"go.autokitteh.dev/autokitteh/sdk/sdkerrors"
	"go.autokitteh.dev/autokitteh/sdk/sdktypes"
)

// handleCalNotification receives and dispatches asynchronous Google Calendar notifications.
func (h handler) handleCalNotification(w http.ResponseWriter, r *http.Request) {
	// Parse event details from the request headers.
	channelID := r.Header.Get("X-Goog-Channel-Id")
	resState := r.Header.Get("X-Goog-Resource-State")

	l := h.logger.With(
		zap.String("urlPath", r.URL.Path),
		zap.String("channelID", channelID),
		zap.String("channelToken", r.Header.Get("X-Goog-Channel-Token")),
		zap.String("resourceID", r.Header.Get("X-Goog-Resource-Id")),
		zap.String("resourceState", resState),
		zap.String("messageNumber", r.Header.Get("X-Goog-Message-Number")),
	)
	if resState == "sync" {
		l.Info("Ignoring Google Calendar watch creation notification")
		return
	}
	l.Info("Received Google Calendar notification")

	// Find all the connection IDs associated with the watch ID.
	ctx := extrazap.AttachLoggerToContext(l, r.Context())
	name := vars.CalendarEventsWatchID
	cids, err := h.vars.FindActiveConnectionIDs(ctx, calendar.IntegrationID, name, channelID)
	if err != nil {
		l.Error("Failed to find connection IDs", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	// Construct the event and dispatch it to all the connections.
	akEvent, err := calendar.ConstructEvent(ctx, h.vars, cids)
	if err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	if err := h.dispatchAsyncEventsToConnections(ctx, cids, akEvent); err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	// Returning immediately without an error = acknowledgement of receipt.
}

// handleDriveNotification receives and dispatches asynchronous Google Drive
// notifications.
func (h handler) handleDriveNotification(w http.ResponseWriter, r *http.Request) {
	// Parse event details from the request headers
	channelID := r.Header.Get("X-Goog-Channel-Id")
	resState := r.Header.Get("X-Goog-Resource-State")

	l := h.logger.With(
		zap.String("urlPath", r.URL.Path),
		zap.String("channelID", channelID),
		zap.String("channelToken", r.Header.Get("X-Goog-Channel-Token")),
		zap.String("resourceID", r.Header.Get("X-Goog-Resource-Id")),
		zap.String("resourceState", resState),
		zap.String("messageNumber", r.Header.Get("X-Goog-Message-Number")),
	)

	if resState == "sync" {
		l.Info("Ignoring Google Drive watch creation notification")
		return
	}
	l.Info("Received Google Drive notification")

	// Find all the connection IDs associated with the watch ID.
	ctx := extrazap.AttachLoggerToContext(l, r.Context())
	name := vars.DriveEventsWatchID
	cids, err := h.vars.FindActiveConnectionIDs(ctx, drive.IntegrationID, name, channelID)
	if err != nil {
		l.Error("Failed to find connection IDs", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	events, err := drive.ConstructEvents(ctx, h.vars, cids)
	l.Warn("Constructed events", zap.Int("events", len(events)))
	if err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	for _, event := range events {
		if err := h.dispatchAsyncEventsToConnections(ctx, cids, event); err != nil {
			common.HTTPError(w, http.StatusInternalServerError)
			return
		}
	}

	// Returning immediately without an error = acknowledgement of receipt.
}

// handleFormsNotification receives and dispatches asynchronous Google Forms
// notifications from a push subscription to a GCP Cloud Pub/Sub topic.
func (h handler) handleFormsNotification(w http.ResponseWriter, r *http.Request) {
	l := h.logger.With(zap.String("urlPath", r.URL.Path)).With(zap.String("eventType", "forms_notification"))
	if !checkRequest(w, r, l) {
		return
	}

	// Parse event details from the request headers.
	eventType := forms.WatchEventType(r.Header.Get("Eventtype"))
	watchID := r.Header.Get("Watchid")

	name := vars.FormResponsesWatchID
	if eventType == forms.WatchSchemaChanges {
		name = vars.FormSchemaWatchID
	}

	l = l.With(
		zap.String("eventType", r.Header.Get("Eventtype")),
		zap.String("formID", r.Header.Get("Formid")),
		zap.String("watchID", watchID),
		zap.String("messageID", r.Header.Get("X-Goog-Pubsub-Message-Id")),
		zap.String("publishTime", r.Header.Get("X-Goog-Pubsub-Publish-Time")),
		zap.String("subscriptionName", r.Header.Get("X-Goog-Pubsub-Subscription-Name")),
	)
	l.Info("received Google Forms notification")

	// Find all the connection IDs associated with the watch ID.
	ctx := extrazap.AttachLoggerToContext(l, r.Context())
	cids, err := h.vars.FindActiveConnectionIDs(ctx, forms.IntegrationID, name, watchID)
	if err != nil {
		l.Error("Failed to find connection IDs", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	// Construct the event and dispatch it to all the connections.
	formsEvent := map[string]any{
		"event_type":   r.Header.Get("Eventtype"),
		"form_id":      r.Header.Get("Formid"),
		"publish_time": r.Header.Get("X-Goog-Pubsub-Publish-Time"),
	}

	akEvent, err := forms.ConstructEvent(ctx, h.vars, formsEvent, cids)
	if err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	if err := h.dispatchAsyncEventsToConnections(ctx, cids, akEvent); err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	// Returning immediately without an error = acknowledgement of receipt.
}

type gmailNotifBody struct {
	EmailAddress string `json:"emailAddress"`
	HistoryID    int    `json:"historyId"`
}

// handleGmailNotification receives and dispatches asynchronous Gmail
// notifications from a push subscription to a GCP Cloud Pub/Sub topic.
func (h handler) handleGmailNotification(w http.ResponseWriter, r *http.Request) {
	l := h.logger.With(zap.String("urlPath", r.URL.Path)).With(zap.String("eventType", "gmail_notification"))
	if !checkRequest(w, r, l) {
		return
	}

	l = l.With(
		zap.String("messageID", r.Header.Get("X-Goog-Pubsub-Message-Id")),
		zap.String("publishTime", r.Header.Get("X-Goog-Pubsub-Publish-Time")),
		zap.String("subscriptionName", r.Header.Get("X-Goog-Pubsub-Subscription-Name")),
	)

	// Parse event details from the JSON body.
	defer r.Body.Close()
	body, err := io.ReadAll(r.Body)
	if err != nil {
		l.Warn("Failed to read request body", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	notif := gmailNotifBody{}
	if err := json.Unmarshal(body, &notif); err != nil {
		l.Warn("Failed to unmarshal request body", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	if notif.EmailAddress == "" {
		l.Error("missing email address in gmail notification, probably misconfigured subscription", zap.String("body", string(body)))
		common.HTTPError(w, http.StatusBadRequest)
		return
	}

	l = l.With(
		zap.String("emailAddress", notif.EmailAddress),
		zap.Int("historyID", notif.HistoryID),
	)

	l.Debug("received gmail notification for: " + notif.EmailAddress)

	// Find all the connection IDs associated with the email address.
	ctx := extrazap.AttachLoggerToContext(l, r.Context())
	l.Debug(fmt.Sprintf("finding connections ids for email %s integrationID %s", notif.EmailAddress, gmail.IntegrationID))
	cids, err := h.vars.FindActiveConnectionIDs(ctx, gmail.IntegrationID, vars.UserEmail, notif.EmailAddress)
	if err != nil {
		l.Error("Failed to find connection IDs", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}
	cidsString := strings.Join(kittehs.Transform(cids, func(cid sdktypes.ConnectionID) string {
		return cid.String()
	}), ",")

	l.Debug(fmt.Sprintf("Found %d connections for gmail %s", len(cids), notif.EmailAddress), zap.String("connectionIDs", cidsString))

	// Construct the event and dispatch it to all the connections.
	gmailEvent := map[string]any{
		"publish_time":  r.Header.Get("X-Goog-Pubsub-Publish-Time"),
		"email_address": notif.EmailAddress,
		"history_id":    notif.HistoryID,
	}

	akEvent, err := gmail.ConstructEvent(ctx, h.vars, gmailEvent, cids)
	if err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	if err := h.dispatchAsyncEventsToConnections(ctx, cids, akEvent); err != nil {
		common.HTTPError(w, http.StatusInternalServerError)
		return
	}

	// Returning immediately without an error = acknowledgement of receipt.
}

func (h handler) dispatchAsyncEventsToConnections(ctx context.Context, cids []sdktypes.ConnectionID, e sdktypes.Event) error {
	l := extrazap.ExtractLoggerFromContext(ctx)

	for _, cid := range cids {
		resp, err := h.dispatch(ctx, e.WithConnectionDestinationID(cid), nil)
		l := l.With(
			zap.String("connectionID", cid.String()),
			zap.String("eventID", resp.EventID.String()),
		)
		if err != nil {
			if errors.Is(err, sdkerrors.ErrResourceExhausted) {
				l.Info("Event dispatch failed due to resource exhaustion")
				continue
			} else {
				l.Error("Event dispatch failed", zap.Error(err))
			}
			return err
		}
		l.Debug("Event dispatched")
	}

	return nil
}

// checkRequest checks the authenticity of the given HTTP POST request.
// If the check fails, it also sets an HTTP error to the impostor client.
func checkRequest(w http.ResponseWriter, r *http.Request, l *zap.Logger) bool {
	// Read the bearer token from the Authorization request header.
	auth := r.Header.Get("Authorization")
	if auth == "" {
		l.Warn("gmail notification check request missing authorization header in Google push notification")
		common.HTTPError(w, http.StatusUnauthorized)
		return false
	}

	if !strings.HasPrefix(auth, "Bearer ") {
		l.Warn("gmail notification check request invalid authorization header in Google push notification")
		common.HTTPError(w, http.StatusUnauthorized)
		return false
	}

	auth = strings.TrimPrefix(auth, "Bearer ")

	// Download and cache Google's OAuth public keys.
	rsaPublicKeys, err := fetchGoogleCerts(r.Context())
	if err != nil {
		l.Error("gmail notification check request failed to fetch Google OAuth certs", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return false
	}

	// Parse the JWT and verify its signature.
	token, err := jwt.Parse(auth, func(t *jwt.Token) (interface{}, error) {
		kid, ok := t.Header["kid"].(string)
		if !ok {
			return nil, errors.New("gmail notification check request missing or invalid kid in token header")
		}

		key, exists := rsaPublicKeys[kid]
		if !exists {
			return nil, fmt.Errorf("gmail notification check request missing google public key not found for kid: %s", kid)
		}
		return key, nil
	})
	if err != nil {
		l.Error("gmail notification check request failed to parse JWT in Google push notification", zap.Error(err))
		common.HTTPError(w, http.StatusInternalServerError)
		return false
	}

	return token.Valid
}

const (
	googleCertsURL = "https://www.googleapis.com/oauth2/v3/certs"
	cacheTimeout   = 24 * time.Hour
)

var (
	cachedPublicKeys map[string]*rsa.PublicKey
	cacheDeadline    = time.Now()
)

// fetchGoogleCerts downloads Google's OAuth public keys and caches them.
func fetchGoogleCerts(ctx context.Context) (map[string]*rsa.PublicKey, error) {
	// Return the cached results if they're still fresh.
	if time.Now().Before(cacheDeadline) {
		return cachedPublicKeys, nil
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, googleCertsURL, nil)
	if err != nil {
		return nil, fmt.Errorf("error creating request for Google OAuth certs: %w", err)
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error fetching Google OAuth certs: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("fetching Google OAuth certs resulted in status %d", resp.StatusCode)
	}

	// Read JSON.
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("error reading Google OAuth certs: %w", err)
	}

	var certs struct {
		Keys []struct {
			KID string `json:"kid"`
			N   string `json:"n"`
			E   string `json:"e"`
		} `json:"keys"`
	}

	// Parse JSON.
	if err := json.Unmarshal(body, &certs); err != nil {
		return nil, fmt.Errorf("error unmarshalling Google OAuth certs: %w", err)
	}

	cachedPublicKeys = make(map[string]*rsa.PublicKey)
	for _, key := range certs.Keys {
		n, err := base64.RawURLEncoding.DecodeString(key.N)
		if err != nil {
			return nil, fmt.Errorf("error decoding modulus %q: %w", key.N, err)
		}

		e, err := base64.RawURLEncoding.DecodeString(key.E)
		if err != nil {
			return nil, fmt.Errorf("error decoding exponent %q: %w", key.N, err)
		}

		pk := &rsa.PublicKey{
			N: new(big.Int).SetBytes(n),
			E: int(new(big.Int).SetBytes(e).Uint64()),
		}
		cachedPublicKeys[key.KID] = pk
	}

	cacheDeadline = time.Now().Add(cacheTimeout)
	return cachedPublicKeys, nil
}
