package cmd

import (
	"axolotl/store"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/spf13/cobra"
)

var loginCmd = &cobra.Command{
	Use:   "login",
	Short: "Authenticate with the remote server via OIDC",
	Run: func(cmd *cobra.Command, args []string) {
		rc, ok := cfg.GetRemoteConfig()
		if !ok {
			fmt.Fprintln(os.Stderr, "no remote server configured; set remote.host in your config")
			os.Exit(1)
		}
		base := fmt.Sprintf("http://%s:%d", rc.Host, rc.Port)

		sessionID := tryDeviceFlow(base)
		if sessionID == "" {
			sessionID = tryCallbackFlow(base)
		}

		pollForToken(base, sessionID)
	},
}

// tryDeviceFlow attempts the device authorization flow. Returns a session ID
// on success, or "" if the server does not support it.
func tryDeviceFlow(base string) string {
	resp, err := http.Post(base+"/auth/device/start", "application/json", nil)
	if err != nil {
		return ""
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return ""
	}
	var start struct {
		SessionID               string `json:"session_id"`
		UserCode                string `json:"user_code"`
		VerificationURI         string `json:"verification_uri"`
		VerificationURIComplete string `json:"verification_uri_complete"`
	}
	json.NewDecoder(resp.Body).Decode(&start)
	if start.SessionID == "" {
		return ""
	}

	uri := start.VerificationURI
	if start.VerificationURIComplete != "" {
		uri = start.VerificationURIComplete
	}
	fmt.Printf("To sign in, open this URL in any browser:\n\n  %s\n\nThen enter this code: %s\n\nWaiting for authentication...\n", uri, start.UserCode)
	return start.SessionID
}

// tryCallbackFlow initiates the traditional callback-based OIDC flow.
// Exits the process on failure.
func tryCallbackFlow(base string) string {
	resp, err := http.Post(base+"/auth/start", "application/json", nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to contact server: %v\n", err)
		os.Exit(1)
	}
	var start struct {
		URL       string `json:"url"`
		SessionID string `json:"session_id"`
	}
	json.NewDecoder(resp.Body).Decode(&start)
	resp.Body.Close()

	if start.URL == "" {
		fmt.Fprintln(os.Stderr, "server did not return an auth URL; is OIDC configured on the server?")
		os.Exit(1)
	}

	fmt.Printf("Open this URL in your browser:\n\n  %s\n\nWaiting for login...\n", start.URL)
	return start.SessionID
}

// pollForToken polls the server until the login completes or times out.
func pollForToken(base, sessionID string) {
	deadline := time.Now().Add(5 * time.Minute)
	for time.Now().Before(deadline) {
		time.Sleep(2 * time.Second)

		resp, err := http.Get(fmt.Sprintf("%s/auth/poll?session_id=%s", base, sessionID))
		if err != nil {
			continue
		}
		if resp.StatusCode == http.StatusAccepted {
			resp.Body.Close()
			continue
		}
		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			fmt.Fprintln(os.Stderr, "login failed")
			os.Exit(1)
		}

		var result struct {
			Token    string `json:"token"`
			Username string `json:"username"`
		}
		json.NewDecoder(resp.Body).Decode(&result)
		resp.Body.Close()

		session, err := store.LoadSession()
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to save session: %v\n", err)
			os.Exit(1)
		}
		session.Token = result.Token
		if err := session.Save(); err != nil {
			fmt.Fprintf(os.Stderr, "failed to save session: %v\n", err)
			os.Exit(1)
		}
		fmt.Printf("Logged in as %s\n", result.Username)
		return
	}

	fmt.Fprintln(os.Stderr, "login timed out")
	os.Exit(1)
}

func init() {
	rootCmd.AddCommand(loginCmd)
}
