package web

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strings"
	"time"

	"infinite-noodle/internal/assets"
	"infinite-noodle/internal/noodle"
)

const sessionCookieName = "infinite_noodle_session"

var errInvalidCredentials = errors.New("invalid credentials")

type Auth struct {
	db     *noodle.Database
	secret []byte
}

type loginPageData struct {
	Error string
}

type authUser struct {
	Username string
	Role     string
}

func NewAuth(db *noodle.Database) (*Auth, error) {
	secret := make([]byte, 32)
	if _, err := rand.Read(secret); err != nil {
		return nil, err
	}
	return &Auth{
		db:     db,
		secret: secret,
	}, nil
}

func HashPassword(password string) string {
	sum := sha256.Sum256([]byte(password))
	return hex.EncodeToString(sum[:])
}

func (a *Auth) EnsureDefaultUser() error {
	users := a.db.GetAllUsers()
	if len(users) == 0 {
		user := noodle.User{
			Id:           a.db.MakeID(),
			Username:     "admin",
			Role:         noodle.UserRoleAdmin,
			PasswordHash: HashPassword("admin"),
			CreatedAt:    time.Now().UTC(),
		}
		if err := a.db.AddUser(user); err != nil {
			return err
		}

		log.Printf("Created default user username=%q password=%q", user.Username, "admin")
		return nil
	}

	var hasAdmin bool
	for _, user := range users {
		updated := false
		if user.Role == "" {
			user.Role = noodle.UserRoleRegular
			updated = true
		}
		if user.Username == "admin" {
			user.Role = noodle.UserRoleAdmin
			updated = true
		}
		if updated {
			if err := a.db.UpdateUser(user); err != nil {
				return err
			}
		}
		if user.Role == noodle.UserRoleAdmin {
			hasAdmin = true
		}
	}
	if hasAdmin {
		return nil
	}

	user := noodle.User{
		Id:           a.db.MakeID(),
		Username:     "admin",
		Role:         noodle.UserRoleAdmin,
		PasswordHash: HashPassword("admin"),
		CreatedAt:    time.Now().UTC(),
	}
	if err := a.db.AddUser(user); err != nil {
		return err
	}

	log.Printf("Created fallback admin user username=%q password=%q", user.Username, "admin")
	return nil
}

func (a *Auth) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		if _, ok := a.CurrentUser(req); !ok {
			http.Redirect(w, req, "/login", http.StatusSeeOther)
			return
		}
		next(w, req)
	}
}

func (a *Auth) RequireAdmin(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		user, ok := a.CurrentUser(req)
		if !ok {
			http.Redirect(w, req, "/login", http.StatusSeeOther)
			return
		}
		if user.Role != noodle.UserRoleAdmin {
			http.Error(w, "admin access required", http.StatusForbidden)
			return
		}
		next(w, req)
	}
}

func (a *Auth) CurrentUser(req *http.Request) (authUser, bool) {
	cookie, err := req.Cookie(sessionCookieName)
	if err != nil {
		return authUser{}, false
	}

	username, ok := a.verifyCookieValue(cookie.Value)
	if !ok {
		return authUser{}, false
	}

	user, err := a.db.GetUserByUsername(username)
	if err != nil || user.Username == "" {
		return authUser{}, false
	}
	role := user.Role
	if role == "" {
		role = noodle.UserRoleRegular
	}

	return authUser{
		Username: user.Username,
		Role:     role,
	}, true
}

func (a *Auth) HandleLogin() http.HandlerFunc {
	tmpl, err := template.ParseFS(assets.FS, "templates/*.html")
	if err != nil {
		log.Fatalf("Error parsing templates: %v", err)
	}

	return func(w http.ResponseWriter, req *http.Request) {
		if req.Method == http.MethodGet {
			if _, ok := a.CurrentUser(req); ok {
				http.Redirect(w, req, "/", http.StatusSeeOther)
				return
			}
			if err := tmpl.ExecuteTemplate(w, "login.html", loginPageData{}); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}

		if req.Method != http.MethodPost {
			http.Error(w, "", http.StatusMethodNotAllowed)
			return
		}

		if err := req.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		username := strings.TrimSpace(req.FormValue("username"))
		password := req.FormValue("password")
		if username == "" || password == "" {
			w.WriteHeader(http.StatusUnauthorized)
			if err := tmpl.ExecuteTemplate(w, "login.html", loginPageData{Error: "Username and password are required."}); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}

		if err := a.Login(w, username, password); err != nil {
			w.WriteHeader(http.StatusUnauthorized)
			if err := tmpl.ExecuteTemplate(w, "login.html", loginPageData{Error: "Invalid username or password."}); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}

		http.Redirect(w, req, "/", http.StatusSeeOther)
	}
}

func (a *Auth) HandleLogout() http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		if req.Method != http.MethodPost {
			http.Error(w, "", http.StatusMethodNotAllowed)
			return
		}

		a.ClearSession(w)
		http.Redirect(w, req, "/login", http.StatusSeeOther)
	}
}

func (a *Auth) Login(w http.ResponseWriter, username, password string) error {
	user, err := a.db.GetUserByUsername(username)
	if err != nil {
		return errInvalidCredentials
	}

	if user.PasswordHash != HashPassword(password) {
		return errInvalidCredentials
	}

	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    a.cookieValue(username),
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int((24 * time.Hour).Seconds()),
	})
	return nil
}

func (a *Auth) ClearSession(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   -1,
	})
}

func (a *Auth) cookieValue(username string) string {
	mac := hmac.New(sha256.New, a.secret)
	mac.Write([]byte(username))
	sig := mac.Sum(nil)
	payload := fmt.Sprintf("%s:%s", username, hex.EncodeToString(sig))
	return base64.RawURLEncoding.EncodeToString([]byte(payload))
}

func (a *Auth) verifyCookieValue(value string) (string, bool) {
	decoded, err := base64.RawURLEncoding.DecodeString(value)
	if err != nil {
		return "", false
	}

	parts := strings.SplitN(string(decoded), ":", 2)
	if len(parts) != 2 || parts[0] == "" {
		return "", false
	}

	expected := a.cookieValue(parts[0])
	return parts[0], hmac.Equal([]byte(expected), []byte(value))
}
