package oauth

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"gitea.local/admin/hspguard/internal/repository"
	"gitea.local/admin/hspguard/internal/types"
	"gitea.local/admin/hspguard/internal/util"
	"gitea.local/admin/hspguard/internal/web"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

type ApiToken struct {
	Token      string
	Expiration float64
}

type ApiTokens struct {
	ID      ApiToken
	Access  ApiToken
	Refresh ApiToken
}

func (h *OAuthHandler) signApiTokens(user *repository.User, apiService *repository.ApiService, nonce *string) (*ApiTokens, error) {
	accessExpiresIn := 15 * time.Minute
	accessExpiresAt := time.Now().Add(accessExpiresIn)

	accessClaims := types.ApiClaims{
		Permissions: []string{},
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    h.cfg.Uri,
			Subject:   apiService.ClientID,
			Audience:  jwt.ClaimStrings{apiService.ClientID},
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ExpiresAt: jwt.NewNumericDate(accessExpiresAt),
		},
	}

	access, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
	if err != nil {
		return nil, err
	}

	var roles = []string{"user"}

	if user.IsAdmin {
		roles = append(roles, "admin")
	}

	idExpiresIn := 15 * time.Minute
	idExpiresAt := time.Now().Add(idExpiresIn)

	idClaims := types.IdTokenClaims{
		Email:         user.Email,
		EmailVerified: user.EmailVerified,
		Name:          user.FullName,
		Picture:       user.ProfilePicture,
		Nonce:         nonce,
		Roles:         roles,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    h.cfg.Uri,
			Subject:   user.ID.String(),
			Audience:  jwt.ClaimStrings{apiService.ClientID},
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ExpiresAt: jwt.NewNumericDate(idExpiresAt),
		},
	}

	idToken, err := util.SignJwtToken(idClaims, h.cfg.Jwt.PrivateKey)
	if err != nil {
		return nil, err
	}

	refreshExpiresIn := 24 * time.Hour
	refreshExpiresAt := time.Now().Add(refreshExpiresIn)

	refreshClaims := types.ApiRefreshClaims{
		UserID: user.ID.String(),
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    h.cfg.Uri,
			Subject:   apiService.ClientID,
			Audience:  jwt.ClaimStrings{apiService.ClientID},
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ExpiresAt: jwt.NewNumericDate(refreshExpiresAt),
		},
	}

	refresh, err := util.SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey)
	if err != nil {
		return nil, err
	}

	return &ApiTokens{
		ID: ApiToken{
			Token:      idToken,
			Expiration: idExpiresIn.Seconds(),
		},
		Access: ApiToken{
			Token:      access,
			Expiration: accessExpiresIn.Seconds(),
		},
		Refresh: ApiToken{
			Token:      refresh,
			Expiration: refreshExpiresIn.Seconds(),
		},
	}, nil
}

func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
	log.Println("[OAUTH] New request to token endpoint")

	authHeader := r.Header.Get("Authorization")
	if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") {
		w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Decode credentials
	payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic "))
	if err != nil {
		http.Error(w, "Invalid auth encoding", http.StatusBadRequest)
		return
	}

	var clientId string
	var clientSecret string

	parts := strings.SplitN(string(payload), ":", 2)
	if len(parts) != 2 {
		http.Error(w, "Unauthorized", http.StatusForbidden)
		return
	}

	clientId = parts[0]
	clientSecret = parts[1]

	log.Printf("Some client is trying to exchange code with id: %s and secret: %s\n", clientId, clientSecret)

	// Parse the form data
	err = r.ParseForm()
	if err != nil {
		http.Error(w, "Failed to parse form", http.StatusBadRequest)
		return
	}

	grantType := r.FormValue("grant_type")
	redirectUri := r.FormValue("redirect_uri")

	log.Printf("Redirect URI is %s\n", redirectUri)

	switch grantType {
	case "authorization_code":
		code := r.FormValue("code")

		fmt.Printf("Code received: %s\n", code)

		session, err := h.cache.GetAuthCode(r.Context(), code)
		if err != nil {
			log.Printf("ERR: Failed to find session under the code %s: %v\n", code, err)
			web.Error(w, "no session found under this auth code", http.StatusNotFound)
			return
		}

		log.Printf("DEBUG: Fetched code session: %#v\n", session)

		apiService, err := h.repo.GetApiServiceCID(r.Context(), session.ClientID)
		if err != nil {
			log.Printf("ERR: Could not find API service with client %s: %v\n", session.ClientID, err)
			web.Error(w, "service is not registered", http.StatusForbidden)
			return
		}

		if session.ClientID != clientId {
			web.Error(w, "invalid auth", http.StatusUnauthorized)
			return
		}

		user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(session.UserID))
		if err != nil {
			web.Error(w, "requested user not found", http.StatusNotFound)
			return
		}

		tokens, err := h.signApiTokens(&user, &apiService, &session.Nonce)
		if err != nil {
			log.Println("ERR: Failed to sign api tokens:", err)
			web.Error(w, "failed to sign tokens", http.StatusInternalServerError)
			return
		}

		type Response struct {
			IdToken      string  `json:"id_token"`
			TokenType    string  `json:"token_type"`
			AccessToken  string  `json:"access_token"`
			Email        string  `json:"email"`
			RefreshToken string  `json:"refresh_token"`
			ExpiresIn    float64 `json:"expires_in"`
			// TODO: add scope (RFC 8693 $2)
		}

		response := Response{
			IdToken:      tokens.ID.Token,
			TokenType:    "Bearer",
			AccessToken:  tokens.Access.Token,
			RefreshToken: tokens.Refresh.Token,
			ExpiresIn:    tokens.Access.Expiration,
			Email:        user.Email,
		}

		log.Printf("sending following response: %#v\n", response)

		w.Header().Set("Content-Type", "application/json")
		encoder := json.NewEncoder(w)
		if err := encoder.Encode(response); err != nil {
			web.Error(w, "failed to encode response", http.StatusInternalServerError)
		}
	case "refresh_token":
		refreshToken := r.FormValue("refresh_token")

		var claims types.ApiRefreshClaims

		token, err := util.VerifyToken(refreshToken, h.cfg.Jwt.PublicKey, &claims)
		if err != nil || !token.Valid {
			http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
			return
		}

		expire, err := claims.GetExpirationTime()
		if err != nil {
			web.Error(w, "failed to retrieve enough info from the token", http.StatusInternalServerError)
			return
		}

		if time.Now().After(expire.Time) {
			web.Error(w, "token is expired", http.StatusUnauthorized)
			return
		}

		userID, err := uuid.Parse(claims.UserID)
		if err != nil {
			web.Error(w, "invalid user credentials in refresh token", http.StatusBadRequest)
			return
		}
		user, err := h.repo.FindUserId(r.Context(), userID)

		apiService, err := h.repo.GetApiServiceCID(r.Context(), claims.Subject)
		if err != nil {
			web.Error(w, "api service is not registered", http.StatusUnauthorized)
			return
		}

		tokens, err := h.signApiTokens(&user, &apiService, nil)

		type Response struct {
			IdToken      string  `json:"id_token"`
			TokenType    string  `json:"token_type"`
			AccessToken  string  `json:"access_token"`
			RefreshToken string  `json:"refresh_token"`
			ExpiresIn    float64 `json:"expires_in"`
		}

		response := Response{
			IdToken:      tokens.ID.Token,
			TokenType:    "Bearer",
			AccessToken:  tokens.Access.Token,
			RefreshToken: tokens.Refresh.Token,
			ExpiresIn:    tokens.Access.Expiration,
		}

		log.Printf("DEBUG: refresh - sending following response: %#v\n", response)

		w.Header().Set("Content-Type", "application/json")
		encoder := json.NewEncoder(w)
		if err := encoder.Encode(response); err != nil {
			web.Error(w, "failed to encode response", http.StatusInternalServerError)
		}
	default:
		web.Error(w, "unsupported grant type", http.StatusBadRequest)
	}
}
