package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/smtp"
	"os"

	"github.com/edgedb/edgedb-go"
	"github.com/gofiber/fiber/v2"
)

type DiscoveryDocument struct {
	UserInfoEndpoint string `json:"userinfo_endpoint"`
}

type UserProfile struct {
	Email        string `json:"email"`
	Name         string `json:"name"`
	AvatarGitHub string `json:"avatar_url"`
	AvatarGoogle string `json:"picture"`
}

type TokenResponse struct {
	AuthToken     string `json:"auth_token"`
	IdentityID    string `json:"identity_id"`
	ProviderToken string `json:"provider_token"`
}

func getGoogleUserProfile(providerToken string) (string, string, string) {
	// Fetch the discovery document
	resp, err := http.Get("https://accounts.google.com/.well-known/openid-configuration")
	if err != nil {
		fmt.Println("Error fetching discovery document")
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Println("Error fetching discovery document")
		panic(resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Error reading discovery document")
		panic(err)
	}

	var discoveryDocument DiscoveryDocument
	if err := json.Unmarshal(body, &discoveryDocument); err != nil {
		fmt.Println("Error unmarshalling discovery document")
		panic(err)
	}

	// Fetch the user profile
	req, err := http.NewRequest("GET", discoveryDocument.UserInfoEndpoint, nil)
	if err != nil {
		fmt.Println("Error fetching user profile")
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+providerToken)
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err = client.Do(req)
	if err != nil {
		fmt.Println("Error fetching user profile")
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		panic("Error fetching user profile")
	}

	body, err = io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Error reading user profile")
		panic(err)
	}

	var userProfile UserProfile
	if err := json.Unmarshal(body, &userProfile); err != nil {
		fmt.Println("Error unmarshalling user profile")
		panic(err)
	}

	return userProfile.Email, userProfile.Name, userProfile.AvatarGoogle
}

func getGitHubUserProfile(providerToken string) (string, string, string) {
	// Create the request to fetch the user profile
	req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
	if err != nil {
		fmt.Println("failed to create request: user profile")
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+providerToken)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("failed to execute request")
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		fmt.Println("failed to fetch user profile: status code")
		panic(resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("failed to read response body")
		panic(err)
	}

	var userProfile UserProfile
	if err := json.Unmarshal(body, &userProfile); err != nil {
		fmt.Println("failed to unmarshal user profile")
		panic(err)
	}

	return userProfile.Email, userProfile.Name, userProfile.AvatarGitHub
}

func generatePKCE() (string, string) {
	verifier_source := make([]byte, 32)
	_, err := rand.Read(verifier_source)
	if err != nil {
		fmt.Println("failed to generate PKCE")
		panic(err)
	}

	verifier := base64.RawURLEncoding.EncodeToString(verifier_source)
	challenge := sha256.Sum256([]byte(verifier))
	return verifier, base64.RawURLEncoding.EncodeToString(challenge[:])
}

func handleUiSignIn(c *fiber.Ctx) error {
	verifier, challenge := generatePKCE()

	c.Cookie(&fiber.Cookie{
		Name:     "jade-edgedb-pkce-verifier",
		Value:    verifier,
		HTTPOnly: true,
		Path:     "/",
		Secure:   true,
	})

	return c.Redirect(fmt.Sprintf("%s/ui/signin?challenge=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), challenge), fiber.StatusTemporaryRedirect)
}

func handleCallbackSignup(c *fiber.Ctx) error {
	if c.Query("verification_email_sent_at") != "" {
		return c.Redirect("/")
	}

	code := c.Query("code")
	if code == "" {
		err := c.Query("error")
		fmt.Println("OAuth callback is missing 'code'. OAuth provider responded with error")
		panic(err)
	}

	verifier := c.Cookies("jade-edgedb-pkce-verifier", "")
	if verifier == "" {
		return c.SendString("Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?")
	}

	codeExchangeURL := fmt.Sprintf("%s/token?code=%s&verifier=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), code, verifier)
	resp, err := http.Get(codeExchangeURL)
	if err != nil {
		fmt.Println("Error exchanging code for access token")
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != fiber.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		fmt.Println("Error exchanging code for access token")
		panic(string(body))
	}

	var tokenResponse TokenResponse
	err = json.NewDecoder(resp.Body).Decode(&tokenResponse)
	if err != nil {
		fmt.Println("Error decoding auth server response")
		panic(err)
	}

	c.Cookie(&fiber.Cookie{
		Name:     "jade-edgedb-auth-token",
		Value:    tokenResponse.AuthToken,
		HTTPOnly: true,
		Path:     "/",
		Secure:   true,
	})

	// Get the issuer of the identity
	var identity Identity
	identityUUID, err := edgedb.ParseUUID(tokenResponse.IdentityID)
	if err != nil {
		fmt.Println("Error parsing UUID")
		panic(err)
	}
	err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
		SELECT ext::auth::Identity {
			issuer
		} FILTER .id = <uuid>$0
	`, &identity, identityUUID)
	if err != nil {
		fmt.Println("Error fetching identity")
		panic(err)
	}

	var (
		providerEmail  string
		providerName   string
		providerAvatar string
	)

	// Get the email and name from the provider
	if identity.Issuer == "https://accounts.google.com" {
		providerEmail, providerName, providerAvatar = getGoogleUserProfile(tokenResponse.ProviderToken)
	} else if identity.Issuer == "https://github.com" {
		providerEmail, providerName, providerAvatar = getGitHubUserProfile(tokenResponse.ProviderToken) // Work !!!!
	}

	stripCustID := CreateNewStripeCustomer(providerName, providerEmail)

	err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}).Execute(edgeCtx, `
	INSERT User {
		stripe_id := <str>$0,
		email := <str>$1,
		name := <str>$2,
		avatar := <str>$3,
		setting := (
			INSERT Setting {
				default_model := "gpt-3.5-turbo"
			}
		),
		identity := (SELECT ext::auth::Identity FILTER .id = <uuid>$4)
	  }
	`, stripCustID, providerEmail, providerName, providerAvatar, identityUUID)
	if err != nil {
		fmt.Println("Error creating user")
		panic(err)
	}

	err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}).Execute(edgeCtx, `
		INSERT Conversation {
			name := 'Default',
			user := global currentUser,
			position := 1
		}`)
	if err != nil {
		fmt.Println("Error creating default conversation")
		panic(err)
	}

	return c.Redirect("/", fiber.StatusPermanentRedirect)
}

func handleCallback(c *fiber.Ctx) error {
	code := c.Query("code")
	if code == "" {
		fmt.Println("OAuth callback is missing 'code'. OAuth provider responded with error")
		return c.Render("error", fiber.Map{"Text": "Error: OAuth provider responded with an error. Please contact the support or try later."}, "layouts/main")
	}

	verifier := c.Cookies("jade-edgedb-pkce-verifier", "")
	if verifier == "" {
		return c.Render("error", fiber.Map{"Text": "Error: No verifier cookie found. Please make sure to use the same devide and browser when login for the first time."}, "layouts/main")
	}

	codeExchangeURL := fmt.Sprintf("%s/token?code=%s&verifier=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), code, verifier)
	resp, err := http.Get(codeExchangeURL)
	if err != nil {
		return c.Render("error", fiber.Map{"Text": "Internal JADE error code "}, "layouts/main")
	}
	defer resp.Body.Close()

	if resp.StatusCode != fiber.StatusOK {
		return c.Render("error", fiber.Map{"Text": "Response Status is not OK"}, "layouts/main")
	}

	var tokenResponse TokenResponse
	err = json.NewDecoder(resp.Body).Decode(&tokenResponse)
	if err != nil {
		return c.Render("error", fiber.Map{"Text": "Can't decode response"}, "layouts/main")
	}

	c.Cookie(&fiber.Cookie{
		Name:     "jade-edgedb-auth-token",
		Value:    tokenResponse.AuthToken,
		HTTPOnly: true,
		Path:     "/",
		Secure:   true,
		SameSite: "Strict",
	})

	return c.Redirect("/", fiber.StatusPermanentRedirect)
}

func handleSignOut(c *fiber.Ctx) error {
	c.ClearCookie("jade-edgedb-auth-token")
	return c.Redirect("/", fiber.StatusTemporaryRedirect)
}

func handleEmailVerification(c *fiber.Ctx) error {
	return c.Render("error", fiber.Map{"Text": "Email auth not yet implemented"}, "layouts/main")
}

func SendNoreplyEmail(to string, subject string, content string) {
	auth := smtp.PlainAuth("", "noreply@bouvai.com", os.Getenv("NOREPLY_APP_PASSWORD"), "smtp.gmail.com")

	msg := []byte("To: " + to + "\r\n" +

		"Subject: " + subject + "\r\n" +

		"\r\n" +

		content + "\r\n")

	err := smtp.SendMail("smtp.gmail.com:587", auth, "noreply@bouvai.com", []string{to}, msg)
	if err != nil {
		panic(err)
	}
}
