package auth

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/rs/zerolog/log"
	"golang.org/x/crypto/bcrypt"
	"source.gleipnir.technology/Gleipnir/nidus-sync/platform"
)

type InactiveUser struct{}

func (e InactiveUser) Error() string { return "That user is not active" }

type InvalidCredentials struct{}

func (e InvalidCredentials) Error() string { return "No username with that password exists" }

type InvalidUsername struct{}

func (e InvalidUsername) Error() string { return "That username doesn't exist" }

type NoCredentialsError struct{}

func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }

type AuthenticatedHandler func(http.ResponseWriter, *http.Request, platform.User)
type EnsureAuth struct {
	handler AuthenticatedHandler
}

func AddUserSession(ctx context.Context, user *platform.User) {
	id_str := strconv.Itoa(int(user.ID))
	sessionManager.Put(ctx, "user_id", id_str)
	sessionManager.Put(ctx, "username", user.Username)
	log.Debug().Str("id", id_str).Str("username", user.Username).Msg("added user session")
}
func ImpersonateEnd(ctx context.Context) {
	sessionManager.Put(ctx, "impersonated_user_id", "")
}
func ImpersonateUser(ctx context.Context, target_user_id int) {
	target_user_id_str := strconv.Itoa(int(target_user_id))
	sessionManager.Put(ctx, "impersonated_user_id", target_user_id_str)
}
func ImpersonatedUser(ctx context.Context) *int32 {
	i_str := sessionManager.GetString(ctx, "impersonated_user_id")
	if i_str == "" {
		return nil
	}
	i, err := strconv.Atoi(i_str)
	if err != nil {
		log.Error().Err(err).Str("impersonated_user_id", i_str).Msg("failed to parse impersonated_user_id")
		return nil
	}
	result := int32(i)
	return &result
}
func ImpersonatorID(ctx context.Context) *int32 {
	user_id_str := sessionManager.GetString(ctx, "user_id")
	user_id, err := strconv.Atoi(user_id_str)
	if err != nil {
		log.Error().Err(err).Str("user_id", user_id_str).Msg("failed to parse user_id")
		return nil
	}
	result := int32(user_id)
	return &result

}
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
	ctx := r.Context()
	user_id_str := sessionManager.GetString(ctx, "user_id")
	impersonated_user_id_str := sessionManager.GetString(ctx, "impersonated_user_id")
	if impersonated_user_id_str != "" {
		user_id_str = impersonated_user_id_str
	}
	if user_id_str != "" {
		user_id, err := strconv.Atoi(user_id_str)
		if err != nil {
			return nil, fmt.Errorf("Failed to convert user_id to int: %w", err)
		}
		username := sessionManager.GetString(ctx, "username")
		if user_id > 0 && username != "" {
			user, err := platform.UserByID(ctx, int32(user_id))
			if err != nil {
				return nil, fmt.Errorf("user by ID: %w", err)
			}
			if !user.IsActive {
				return nil, fmt.Errorf("user is inactive")
			}
			return user, nil
		}
	}
	// If we can't get the user from the session try to get from auth headers
	username, password, ok := r.BasicAuth()
	if !ok {
		return nil, &NoCredentialsError{}
	}
	user, err := validateUser(ctx, username, password)
	if err != nil {
		return nil, err
	}
	AddUserSession(ctx, user)
	return user, nil
}

func NewEnsureAuth(handlerToWrap AuthenticatedHandler) *EnsureAuth {
	return &EnsureAuth{handlerToWrap}
}

func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// If this is an API request respond with a more machine-readable error state
	accept := r.Header.Get("Accept")
	/*
		offers := []string{"application/json", "text/html"}

		content_type := NegotiateContent(accept, offers)
	*/
	user, err := GetAuthenticatedUser(r)
	if err != nil || user == nil {
		var msg []byte
		// Don't send authentication headers for browsers because it forces the authentication popup
		requested_with := r.Header.Get("X-Requested-With")
		//log.Debug().Str("x-requested-with", requested_with).Send()
		if !strings.HasPrefix(requested_with, "nidus-web") && accept != "text/event-stream" {
			w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`)
			// Separate return codes for different authentication failures
			if _, ok := err.(*NoCredentialsError); ok {
				log.Info().Msg("No credentials present and no session")
				w.Header().Set("WWW-Authenticate-Error", "no-credentials")
				msg = []byte("Please provide credentials.\n")
			} else if _, ok := err.(*platform.NoUserError); ok {
				w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
				msg = []byte("Invalid credentials provided.\n")
			} else if _, ok := err.(*InvalidCredentials); ok {
				w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
				msg = []byte("Invalid credentials provided.\n")
			}
		}

		w.WriteHeader(401)
		_, err = w.Write(msg)
		if err != nil {
			log.Error().Err(err).Msg("failed to write response")
		}
		return
	}
	ea.handler(w, r, *user)
}
func SigninUser(r *http.Request, username string, password string) (*platform.User, error) {
	user, err := validateUser(r.Context(), username, password)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, errors.New("No matching user")
	}
	AddUserSession(r.Context(), user)
	return user, nil
}

func SignoutUser(r *http.Request, user platform.User) {
	sessionManager.Put(r.Context(), "user_id", "")
	sessionManager.Put(r.Context(), "username", "")
	err := sessionManager.Destroy(r.Context())
	if err != nil {
		log.Error().Err(err).Msg("failed to destroy session for user on signout")
	}
	log.Info().Str("username", user.Username).Int("user_id", (user.ID)).Msg("Ended user session")
}

func SignupUser(ctx context.Context, username string, name string, password string) (*platform.User, error) {
	password_hash, err := HashPassword(password)
	if err != nil {
		return nil, fmt.Errorf("Cannot signup user, failed to create hashed password: %w", err)
	}
	u, err := platform.CreateUser(ctx, username, name, password_hash)
	if err != nil {
		return nil, fmt.Errorf("create user: %s", err)
	}
	return u, nil
}

func HashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	return string(bytes), err
}

func redact(s string) string {
	if len(s) <= 4 {
		return s
	}

	first_two := s[:2]
	last_two := s[len(s)-2:]
	middle_length := len(s) - 4

	return first_two + strings.Repeat("*", middle_length) + last_two
}

func validatePassword(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	if err != nil {
		log.Debug().Err(err).Str("password", password).Str("hash", hash).Msg("!validate password")
	}
	return err == nil
}

func validateUser(ctx context.Context, username string, password string) (*platform.User, error) {
	log.Info().Str("username", username).Msg("begin validateUser. Hashing...")
	start := time.Now()
	passwordHash, err := HashPassword(password)
	if err != nil {
		return nil, fmt.Errorf("Failed to hash password: %w", err)
	}
	t1 := time.Now()
	elapsed := t1.Sub(start)
	log.Info().Int64("elapsed ms", elapsed.Milliseconds()).Msg("calculated hash")
	t2 := time.Now()
	user, err := platform.UserByUsername(ctx, username)
	if err != nil {
		return nil, fmt.Errorf("Failed to query for user: %w", err)
	}
	elapsed = t2.Sub(t1)
	log.Info().Int64("elapsed ms", elapsed.Milliseconds()).Str("username", username).Msg("queried user")
	if user == nil {
		log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username")
		return nil, InvalidUsername{}
	}
	if !user.IsActive {
		return nil, InactiveUser{}
	}
	if !validatePassword(password, user.PasswordHash) {
		log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user")
		return nil, InvalidCredentials{}
	}
	return user, nil
}
