package main

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/gmail/v1"
)

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.SetOutput(os.Stdout)

	var (
		clientID     = flag.String("client-id", strings.TrimSpace(os.Getenv("EMAIL_MONITOR_OAUTH_CLIENT_ID")), "OAuth client id")
		clientSecret = flag.String("client-secret", strings.TrimSpace(os.Getenv("EMAIL_MONITOR_OAUTH_CLIENT_SECRET")), "OAuth client secret")
		listen       = flag.String("listen", "127.0.0.1:8085", "Local callback listener (host:port)")
	)
	flag.Parse()

	if *clientID == "" || *clientSecret == "" {
		log.Fatalf("missing client id/secret: provide -client-id/-client-secret or set EMAIL_MONITOR_OAUTH_CLIENT_ID/EMAIL_MONITOR_OAUTH_CLIENT_SECRET")
	}

	ln, err := net.Listen("tcp", *listen)
	if err != nil {
		log.Fatalf("failed to listen on %s: %v", *listen, err)
	}
	defer ln.Close()

	redirectURL := fmt.Sprintf("http://%s/callback", ln.Addr().String())

	oauthCfg := &oauth2.Config{
		ClientID:     *clientID,
		ClientSecret: *clientSecret,
		Endpoint:     google.Endpoint,
		RedirectURL:  redirectURL,
		Scopes: []string{
			gmail.GmailReadonlyScope,
			gmail.GmailMetadataScope,
		},
	}

	state, err := randomState()
	if err != nil {
		log.Fatalf("failed to generate state: %v", err)
	}

	resultCh := make(chan *oauth2.Token, 1)
	errCh := make(chan error, 1)

	mux := http.NewServeMux()
	mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("state") != state {
			http.Error(w, "state mismatch", http.StatusBadRequest)
			errCh <- fmt.Errorf("state mismatch")
			return
		}
		code := r.URL.Query().Get("code")
		if code == "" {
			http.Error(w, "missing code", http.StatusBadRequest)
			errCh <- fmt.Errorf("missing code")
			return
		}

		tok, err := oauthCfg.Exchange(r.Context(), code)
		if err != nil {
			http.Error(w, "token exchange failed", http.StatusInternalServerError)
			errCh <- fmt.Errorf("token exchange failed: %w", err)
			return
		}
		fmt.Fprintln(w, "OK. You can close this tab.")
		resultCh <- tok
	})

	srv := &http.Server{
		Handler:      mux,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		select {
		case <-sigCh:
			_ = srv.Shutdown(context.Background())
			os.Exit(1)
		case <-ctx.Done():
			return
		}
	}()

	go func() {
		_ = srv.Serve(ln)
	}()

	url := oauthCfg.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
	log.Println("Open this URL in a browser to authorize:")
	log.Println(url)
	log.Printf("Waiting for OAuth callback at %s ...", redirectURL)

	select {
	case tok := <-resultCh:
		if tok == nil {
			log.Fatalf("received empty token")
		}
		if tok.RefreshToken == "" {
			log.Fatalf("no refresh_token returned. Try revoking the app in Google Account -> Security -> Third-party access, then re-run")
		}

		fmt.Println("\nSet these env vars:")
		fmt.Printf("EMAIL_MONITOR_OAUTH_CLIENT_ID=%s\n", *clientID)
		fmt.Printf("EMAIL_MONITOR_OAUTH_CLIENT_SECRET=%s\n", *clientSecret)
		fmt.Printf("EMAIL_MONITOR_OAUTH_REFRESH_TOKEN=%s\n", tok.RefreshToken)
		fmt.Println("\n(Keep the refresh token secret.)")

		_ = srv.Shutdown(context.Background())
		return
	case err := <-errCh:
		log.Fatalf("oauth flow failed: %v", err)
	}
}

func randomState() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}
