package service

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"github.com/tidbcloud/aisupport/services/oncall/db"
	"github.com/tidbcloud/aisupport/services/oncall/models"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
)

type auth0UserInfoResponse struct {
	Sub           string `json:"sub"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Name          string `json:"name"`
	Nickname      string `json:"nickname"`
}

func allowedEmailDomains() []string {
	raw := strings.TrimSpace(os.Getenv("ALLOWED_EMAIL_DOMAINS"))
	if raw == "" {
		return nil
	}
	parts := strings.FieldsFunc(raw, func(r rune) bool {
		return r == ',' || r == ';' || r == ' ' || r == '\t' || r == '\n'
	})
	res := make([]string, 0, len(parts))
	for _, p := range parts {
		d := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(p)), "@")
		if d != "" {
			res = append(res, d)
		}
	}
	return res
}

func IsEmailAllowed(email string) bool {
	domains := allowedEmailDomains()
	if len(domains) == 0 {
		return true
	}
	e := strings.ToLower(strings.TrimSpace(email))
	idx := strings.LastIndex(e, "@")
	if idx < 0 || idx == len(e)-1 {
		return false
	}
	domain := e[idx+1:]
	for _, d := range domains {
		if domain == d {
			return true
		}
	}
	return false
}

func ValidateAuth0AccessToken(accessToken string) (string, string, error) {
	if strings.TrimSpace(accessToken) == "" {
		return "", "", fmt.Errorf("empty access token")
	}
	domain := strings.TrimSpace(os.Getenv("AUTH0_DOMAIN"))
	if domain == "" {
		return "", "", fmt.Errorf("AUTH0_DOMAIN is not configured")
	}
	userinfoURL := fmt.Sprintf("https://%s/userinfo", strings.TrimPrefix(strings.TrimPrefix(domain, "https://"), "http://"))

	req, err := http.NewRequest(http.MethodGet, userinfoURL, nil)
	if err != nil {
		return "", "", err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", "", err
	}
	if resp.StatusCode != http.StatusOK {
		return "", "", fmt.Errorf("auth0 userinfo failed: status=%d body=%s", resp.StatusCode, string(body))
	}

	var info auth0UserInfoResponse
	if err := json.Unmarshal(body, &info); err != nil {
		return "", "", err
	}
	email := strings.ToLower(strings.TrimSpace(info.Email))
	if email == "" {
		return "", "", fmt.Errorf("auth0 userinfo missing email")
	}
	if !info.EmailVerified {
		return "", "", fmt.Errorf("auth0 email is not verified")
	}
	name := strings.TrimSpace(info.Name)
	if name == "" {
		name = strings.TrimSpace(info.Nickname)
	}
	if name == "" {
		name = strings.Split(email, "@")[0]
	}
	return email, name, nil
}

func getOrCreateUserByEmail(email, name string) (models.User, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	name = strings.TrimSpace(name)
	if name == "" {
		name = strings.Split(email, "@")[0]
	}
	var user models.User
	err := db.DB.Where("email = ?", email).First(&user).Error
	if err != nil {
		user = models.User{Email: email, Name: name}
		if err := db.DB.Create(&user).Error; err != nil {
			return models.User{}, err
		}
		return user, nil
	}
	if strings.TrimSpace(user.Name) == "" && name != "" {
		user.Name = name
		_ = db.DB.Model(&user).Update("name", name).Error
	}
	return user, nil
}

func createSessionForUser(user models.User) (string, error) {
	token, err := generateSessionToken()
	if err != nil {
		return "", err
	}
	session := models.UserSession{
		UserID:    user.ID,
		Token:     token,
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(SessionTTL()),
	}
	if err := db.DB.Create(&session).Error; err != nil {
		return "", err
	}
	return token, nil
}

// getUserTeamAccess splits a user's teams by what they may do there: managed
// (manager — may hand out roles), writable (editor or manager — may change the
// schedule), and readable (any membership).
func getUserTeamAccess(email string) (managed []uint, writable []uint, readable []uint) {
	managed = make([]uint, 0)
	writable = make([]uint, 0)
	readable = make([]uint, 0)

	// Check engineer access
	var engineer models.Engineer
	if err := db.DB.Where("email = ?", email).First(&engineer).Error; err == nil {
		var relations []models.EngineerTeam
		db.DB.Where("engineer_id = ?", engineer.ID).Find(&relations)
		for _, rel := range relations {
			readable = append(readable, rel.TeamID)
			if rel.HasRole(models.TeamRoleEditor) {
				writable = append(writable, rel.TeamID)
			}
			if rel.HasRole(models.TeamRoleManager) {
				managed = append(managed, rel.TeamID)
			}
		}
	}

	return managed, writable, readable
}

func generateSessionToken() (string, error) {
	bytes := make([]byte, 32)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}

const (
	LegacyAPIBasePath = "/api/v1"
	SharedAPIBasePath = "/api/v1/oncall"
)

func RegisterAuthRoutes(r gin.IRouter) {
	r.GET("/loginCB", func(c *gin.Context) {
		frontendURL := strings.TrimRight(strings.TrimSpace(os.Getenv("ONCALL_FRONTEND_URL")), "/")
		if frontendURL == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "ONCALL_FRONTEND_URL is not configured"})
			return
		}

		code := strings.TrimSpace(c.Query("code"))
		state := strings.TrimSpace(c.Query("state"))
		redirectTarget := frontendURL + "/login"
		qp := url.Values{}
		if code != "" {
			qp.Set("code", code)
		}
		if state != "" {
			qp.Set("state", state)
		}
		if encoded := qp.Encode(); encoded != "" {
			redirectTarget += "?" + encoded
		}
		c.Redirect(http.StatusFound, redirectTarget)
	})

	exchangeToken := func(c *gin.Context) {
		var req struct {
			AccessToken string `json:"access_token" binding:"required"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
			return
		}

		email, displayName, err := ValidateAuth0AccessToken(req.AccessToken)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			return
		}
		if !IsEmailAllowed(email) {
			c.JSON(http.StatusForbidden, gin.H{"error": "email domain not allowed"})
			return
		}

		user, err := getOrCreateUserByEmail(email, displayName)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
			return
		}

		sessionToken, err := createSessionForUser(user)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create session"})
			return
		}

		// Primary transport is the HttpOnly cookie; the token is still returned
		// in the body for backward compatibility with non-browser callers.
		SetSessionCookie(c, sessionToken)

		managed, writable, readable := getUserTeamAccess(email)
		c.JSON(http.StatusOK, gin.H{
			"token":             sessionToken,
			"user":              gin.H{"id": user.ID, "email": user.Email, "name": user.Name},
			"managed_team_ids":  managed,
			"writable_team_ids": writable,
			"read_team_ids":     readable,
		})
	}

	getMe := func(c *gin.Context) {
		email, name, err := RequestUserFromContext(c)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			return
		}

		managed, writable, readable := getUserTeamAccess(email)
		isSuperAdmin := IsSuperAdmin(email)
		c.JSON(http.StatusOK, gin.H{
			"email":             email,
			"name":              name,
			"managed_team_ids":  managed,
			"writable_team_ids": writable,
			"read_team_ids":     readable,
			"is_super_admin":    isSuperAdmin,
			"can_create_team":   isSuperAdmin,
		})
	}

	// Logout revokes the server-side session and clears the cookie. It is
	// intentionally tolerant: it always clears the cookie even if the session
	// row is already gone, so a stale browser can still log out cleanly.
	logout := func(c *gin.Context) {
		token, _ := c.Cookie(SessionCookieName)
		if token == "" {
			authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
			if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
				token = strings.TrimSpace(authHeader[7:])
			}
		}
		if token != "" {
			db.DB.Where("token = ?", token).Delete(&models.UserSession{})
		}
		ClearSessionCookie(c)
		c.JSON(http.StatusOK, gin.H{"ok": true})
	}

	for _, basePath := range []string{LegacyAPIBasePath, SharedAPIBasePath} {
		r.POST(basePath+"/auth/token", exchangeToken)
		r.GET(basePath+"/me", getMe)
		r.POST(basePath+"/auth/logout", logout)
	}
}

func IsSuperAdmin(email string) bool {
	email = strings.ToLower(strings.TrimSpace(email))
	if email == "" {
		return false
	}
	var n int64
	if err := db.DB.Model(&models.SuperAdmin{}).Where("email = ?", email).Count(&n).Error; err != nil {
		return false
	}
	return n > 0
}
