package auth

import (
	"context"
	"crypto/md5"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/email"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
	"github.com/go-chi/chi/v5"
)

type Handler struct {
	db       *sql.DB
	cfg      *config.Config
	sessions *SessionStore
	hub      *gateway.Hub
	mailer   *email.Mailer
}

func NewHandler(db *sql.DB, cfg *config.Config, hub *gateway.Hub, mailer *email.Mailer) *Handler {
	return &Handler{
		db:       db,
		cfg:      cfg,
		sessions: NewSessionStore(db, cfg),
		hub:      hub,
		mailer:   mailer,
	}
}

func (h *Handler) RegisterPublicRoutes(r chi.Router) {
	r.Post("/register", h.Register)
	r.Post("/login/password", h.Login)
	r.Post("/logout", h.Logout)
	r.Post("/verify-email", h.VerifyEmail)
	r.Post("/request-password-reset", h.RequestPasswordReset)
	r.Post("/reset-password", h.ResetPassword)
}

func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
	r.Post("/auth/request-verification", h.RequestVerification)
	r.Post("/auth/admin/announce", h.AdminAnnounce)
	r.Get("/auth/me", h.Me)
	r.Patch("/auth/me", h.UpdateProfile)
	r.Put("/auth/me/password", h.ChangePassword)
	r.Get("/users/{userID}/profile", h.GetPublicProfile)
	r.Get("/users/me/blocks", h.ListBlocks)
	r.Post("/users/me/blocks", h.BlockUser)
	r.Delete("/users/me/blocks/{userID}", h.UnblockUser)
	r.Get("/users/me/availability", h.GetMyAvailability)
	r.Put("/users/me/availability", h.PutMyAvailability)
}

// @Summary Get public user profile
// @Description Get public profile for any user
// @Tags users
// @Produce json
// @Param userID path string true "User ID"
// @Success 200 {object} PublicUserProfile
// @Router /users/{userID}/profile [get]
func (h *Handler) GetPublicProfile(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "userID")
	profile, err := h.getPublicProfile(r.Context(), userID)
	if err != nil {
		http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(profile)
}

func (h *Handler) getPublicProfile(ctx context.Context, userID string) (PublicUserProfile, error) {
	var p PublicUserProfile
	var socialLinks []byte
	err := h.db.QueryRowContext(ctx, `
		SELECT id, username, display_name, email, avatar, bio, accent_color, status, status_text, created_at,
			   banner_url, tagline, pronouns, social_links
		FROM users WHERE id = $1
	`, userID).Scan(&p.ID, &p.Username, &p.DisplayName, &p.Email, &p.Avatar, &p.Bio, &p.AccentColor, &p.Status, &p.StatusText, &p.CreatedAt, &p.BannerURL, &p.Tagline, &p.Pronouns, &socialLinks)
	if err != nil {
		return p, err
	}
	if len(socialLinks) > 0 {
		_ = json.Unmarshal(socialLinks, &p.SocialLinks)
	}
	p.Badges = h.getUserBadges(ctx, userID)
	return p, nil
}

func (h *Handler) getUserBadges(ctx context.Context, userID string) []badgeResponse {
	rows, err := h.db.QueryContext(ctx, `
		SELECT b.id, b.name, b.icon, b.description, b.server_id
		FROM badges b
		JOIN user_badges ub ON ub.badge_id = b.id
		WHERE ub.user_id = $1
	`, userID)
	if err != nil {
		return nil
	}
	defer rows.Close()
	badges := make([]badgeResponse, 0)
	for rows.Next() {
		var b badgeResponse
		var serverID sql.NullString
		if err := rows.Scan(&b.ID, &b.Name, &b.Icon, &b.Description, &serverID); err != nil {
			continue
		}
		if serverID.Valid {
			b.ServerID = serverID.String
		}
		badges = append(badges, b)
	}
	return badges
}

// @Summary List blocked users
// @Description List users the current user has blocked
// @Tags users
// @Produce json
// @Success 200 {array} blockResponse
// @Router /users/me/blocks [get]
func (h *Handler) ListBlocks(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}
	rows, err := h.db.QueryContext(r.Context(), `
		SELECT u.id, u.username, b.created_at::text
		FROM blocks b
		JOIN users u ON u.id = b.blocked_id
		WHERE b.blocker_id = $1
	`, userID)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	defer rows.Close()
	blocks := make([]blockResponse, 0)
	for rows.Next() {
		var b blockResponse
		if err := rows.Scan(&b.ID, &b.Username, &b.CreatedAt); err != nil {
			continue
		}
		blocks = append(blocks, b)
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(blocks)
}

// @Summary Block a user
// @Description Block a user by ID
// @Tags users
// @Accept json
// @Success 204
// @Router /users/me/blocks [post]
func (h *Handler) BlockUser(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}
	var req struct {
		UserID string `json:"user_id"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID == "" {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}
	_, err := h.db.ExecContext(r.Context(), `
		INSERT INTO blocks (blocker_id, blocked_id) VALUES ($1, $2) ON CONFLICT DO NOTHING
	`, userID, req.UserID)
	if err != nil {
		http.Error(w, `{"error":"failed to block user"}`, http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// @Summary Unblock a user
// @Description Unblock a user by ID
// @Tags users
// @Success 204
// @Router /users/me/blocks/{userID} [delete]
func (h *Handler) UnblockUser(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}
	targetID := chi.URLParam(r, "userID")
	_, err := h.db.ExecContext(r.Context(), `DELETE FROM blocks WHERE blocker_id = $1 AND blocked_id = $2`, userID, targetID)
	if err != nil {
		http.Error(w, `{"error":"failed to unblock user"}`, http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

type registerRequest struct {
	Email       string `json:"email"`
	Username    string `json:"username"`
	DisplayName string `json:"display_name"`
	Password    string `json:"password"`
}

type loginRequest struct {
	Email    string `json:"email"`
	Username string `json:"username"`
	Password string `json:"password"`
}

type updateProfileRequest struct {
	AvatarURL   *string `json:"avatar_url"`
	DisplayName *string `json:"display_name"`
	Bio         *string `json:"bio"`
	AccentColor *string `json:"accent_color"`
	StatusText  *string `json:"status_text"`
	Status      *string `json:"status"`
	BannerURL   *string `json:"banner_url"`
	Tagline     *string `json:"tagline"`
	Pronouns    *string `json:"pronouns"`
	SocialLinks *[]struct {
		Platform string `json:"platform"`
		URL      string `json:"url"`
	} `json:"social_links"`
}

type PublicUserProfile struct {
	UserProfile
	BannerURL   string                   `json:"banner_url"`
	Tagline     string                   `json:"tagline"`
	Pronouns    string                   `json:"pronouns"`
	SocialLinks []map[string]interface{} `json:"social_links"`
	Badges      []badgeResponse          `json:"badges"`
}

type badgeResponse struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Icon        string `json:"icon"`
	Description string `json:"description"`
	ServerID    string `json:"server_id,omitempty"`
}

type blockResponse struct {
	ID        string `json:"id"`
	Username  string `json:"username"`
	CreatedAt string `json:"created_at"`
}

type changePasswordRequest struct {
	CurrentPassword string `json:"current_password"`
	NewPassword     string `json:"new_password"`
}

type UserProfile struct {
	ID          string `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"display_name"`
	Email       string `json:"email"`
	Avatar      string `json:"avatar"`
	Bio         string `json:"bio"`
	AccentColor string `json:"accent_color"`
	Status      string `json:"status"`
	StatusText  string `json:"status_text"`
	CreatedAt   string `json:"created_at"`
}

func gravatarURL(email string) string {
	email = strings.ToLower(strings.TrimSpace(email))
	hash := md5.Sum([]byte(email))
	return fmt.Sprintf("https://www.gravatar.com/avatar/%s?d=identicon&s=256", hex.EncodeToString(hash[:]))
}

// @Summary Register a new user
// @Description Create a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param body body registerRequest true "Registration data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /auth/register [post]
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	var req registerRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}

	if req.Email == "" || req.Username == "" || req.Password == "" {
		http.Error(w, `{"error":"missing fields"}`, http.StatusBadRequest)
		return
	}

	hash, err := HashPassword(req.Password)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}

	displayName := req.DisplayName
	if displayName == "" {
		displayName = req.Username
	}

	var userID string
	err = h.db.QueryRowContext(r.Context(), `
		INSERT INTO users (username, display_name, email, password_hash, avatar)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id
	`, req.Username, displayName, req.Email, hash, gravatarURL(req.Email)).Scan(&userID)
	if err != nil {
		http.Error(w, `{"error":"user exists"}`, http.StatusConflict)
		return
	}

	token, err := h.sessions.Create(r.Context(), userID)
	if err != nil {
		http.Error(w, `{"error":"session error"}`, http.StatusInternalServerError)
		return
	}

	h.setSessionCookie(w, token)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"id": userID})
}

// @Summary Login with email and password
// @Description Authenticate with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param body body loginRequest true "Login credentials"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/login/password [post]
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	var req loginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}

	login := strings.TrimSpace(req.Email)
	if login == "" {
		login = strings.TrimSpace(req.Username)
	}
	if login == "" || req.Password == "" {
		http.Error(w, `{"error":"email/username and password required"}`, http.StatusBadRequest)
		return
	}

	var userID, hash string
	err := h.db.QueryRowContext(r.Context(), `
		SELECT id, password_hash FROM users WHERE LOWER(email) = LOWER($1) OR LOWER(username) = LOWER($1)
	`, login).Scan(&userID, &hash)
	if err != nil {
		http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
		return
	}

	ok, err := VerifyPassword(req.Password, hash)
	if err != nil || !ok {
		http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
		return
	}

	token, err := h.sessions.Create(r.Context(), userID)
	if err != nil {
		http.Error(w, `{"error":"session error"}`, http.StatusInternalServerError)
		return
	}

	h.setSessionCookie(w, token)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"id": userID})
}

// @Summary Logout
// @Description End the current session
// @Tags auth
// @Success 204
// @Router /auth/logout [post]
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie(h.cfg.Session.CookieName)
	if err == nil {
		h.sessions.Delete(r.Context(), cookie.Value)
	}

	http.SetCookie(w, &http.Cookie{
		Name:     h.cfg.Session.CookieName,
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
	})

	w.WriteHeader(http.StatusNoContent)
}

// @Summary Get current user profile
// @Description Get the authenticated user's profile
// @Tags auth
// @Produce json
// @Security SessionAuth
// @Success 200 {object} UserProfile
// @Failure 401 {object} map[string]string
// @Router /auth/me [get]
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
	cookie, _ := r.Cookie(h.cfg.Session.CookieName)
	if cookie != nil {
		fmt.Printf("Me Request: Cookie present! value=%s\n", cookie.Value)
	} else {
		fmt.Printf("Me Request: NO COOKIE!\n")
	}
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		fmt.Printf("Me Request: NO USER ID IN CONTEXT\n")
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	user, err := h.getProfile(r.Context(), userID)
	if err != nil {
		http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

// @Summary Update current user profile
// @Description Update the authenticated user's profile
// @Tags auth
// @Accept json
// @Produce json
// @Security SessionAuth
// @Param body body updateProfileRequest true "Profile update data"
// @Success 200 {object} UserProfile
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/me [patch]
func (h *Handler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	var req updateProfileRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}

	// Fetch existing user to merge fields
	var existing struct {
		DisplayName string
		Bio         string
		AccentColor string
		StatusText  string
		Status      string
		BannerURL   string
		Tagline     string
		Pronouns    string
		AvatarURL   string
		SocialLinks []struct {
			Platform string `json:"platform"`
			URL      string `json:"url"`
		}
	}
	var socialLinksBytes []byte
	var bannerURL, tagline, pronouns, avatar sql.NullString

	err := h.db.QueryRowContext(r.Context(), `
		SELECT display_name, bio, accent_color, status_text, status, banner_url, tagline, pronouns, avatar, social_links
		FROM users WHERE id = $1
	`, userID).Scan(&existing.DisplayName, &existing.Bio, &existing.AccentColor, &existing.StatusText, &existing.Status, &bannerURL, &tagline, &pronouns, &avatar, &socialLinksBytes)
	
	if err != nil {
		http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
		return
	}

	existing.BannerURL = bannerURL.String
	existing.Tagline = tagline.String
	existing.Pronouns = pronouns.String
	existing.AvatarURL = avatar.String
	if len(socialLinksBytes) > 0 {
		json.Unmarshal(socialLinksBytes, &existing.SocialLinks)
	}

	// Helper to merge strings
	mergeStr := func(ptr *string, old string) string {
		if ptr != nil {
			return *ptr
		}
		return old
	}

	newBio := mergeStr(req.Bio, existing.Bio)
	newStatusText := mergeStr(req.StatusText, existing.StatusText)
	newDisplayName := mergeStr(req.DisplayName, existing.DisplayName)
	newTagline := mergeStr(req.Tagline, existing.Tagline)
	newPronouns := mergeStr(req.Pronouns, existing.Pronouns)
	newAccentColor := mergeStr(req.AccentColor, existing.AccentColor)
	newStatus := mergeStr(req.Status, existing.Status)
	newBannerURL := mergeStr(req.BannerURL, existing.BannerURL)
	newAvatarURL := mergeStr(req.AvatarURL, existing.AvatarURL)

	if len(newBio) > 250 {
		http.Error(w, `{"error":"bio too long"}`, http.StatusBadRequest)
		return
	}
	if len(newStatusText) > 128 {
		http.Error(w, `{"error":"status text too long"}`, http.StatusBadRequest)
		return
	}
	if len(newDisplayName) > 32 {
		http.Error(w, `{"error":"display name too long"}`, http.StatusBadRequest)
		return
	}
	if len(newTagline) > 128 {
		http.Error(w, `{"error":"tagline too long"}`, http.StatusBadRequest)
		return
	}
	if len(newPronouns) > 40 {
		http.Error(w, `{"error":"pronouns too long"}`, http.StatusBadRequest)
		return
	}
	if newAccentColor != "" && len(newAccentColor) != 7 {
		http.Error(w, `{"error":"accent color must be 7 char hex"}`, http.StatusBadRequest)
		return
	}
	if newStatus != "" && !isValidStatus(newStatus) {
		http.Error(w, `{"error":"invalid status"}`, http.StatusBadRequest)
		return
	}

	var socialLinksJSON []byte
	if req.SocialLinks != nil {
		socialLinksJSON, _ = json.Marshal(*req.SocialLinks)
	} else {
		socialLinksJSON, _ = json.Marshal(existing.SocialLinks)
	}

	_, err = h.db.ExecContext(r.Context(), `
		UPDATE users
		SET display_name = $2, bio = $3, accent_color = $4, status_text = $5, status = $6,
			banner_url = $7, tagline = $8, pronouns = $9, social_links = $10, avatar = $11
		WHERE id = $1
	`, userID, newDisplayName, newBio, newAccentColor, newStatusText, newStatus, newBannerURL, newTagline, newPronouns, socialLinksJSON, newAvatarURL)
	if err != nil {
		http.Error(w, `{"error":"update failed"}`, http.StatusInternalServerError)
		return
	}

	if req.Status != nil && h.hub != nil {
		user, err := h.getProfile(r.Context(), userID)
		if err == nil {
			h.hub.SetUserStatus(userID, *req.Status)
			h.hub.BroadcastPresence(userID, user.Username, *req.Status)
		}
	}

	user, err := h.getProfile(r.Context(), userID)
	if err != nil {
		http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

func isValidStatus(status string) bool {
	switch status {
	case "online", "idle", "dnd", "offline":
		return true
	}
	return false
}

func (h *Handler) getProfile(ctx context.Context, userID string) (UserProfile, error) {
	var user UserProfile
	return user, h.db.QueryRowContext(ctx, `
		SELECT id, username, display_name, email, avatar, bio, accent_color, status, status_text, created_at
		FROM users WHERE id = $1
	`, userID).Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.Avatar, &user.Bio, &user.AccentColor, &user.Status, &user.StatusText, &user.CreatedAt)
}

func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
	SetSessionCookie(w, h.cfg.Session.CookieName, token, h.cfg.Session.Duration)
}

// ChangePassword handles PUT /auth/me/password.
func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	var req changePasswordRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}

	if req.CurrentPassword == "" || req.NewPassword == "" {
		http.Error(w, `{"error":"current and new password required"}`, http.StatusBadRequest)
		return
	}

	if len(req.NewPassword) < 8 {
		http.Error(w, `{"error":"password must be at least 8 characters"}`, http.StatusBadRequest)
		return
	}

	// Verify current password
	var currentHash string
	err := h.db.QueryRowContext(r.Context(), "SELECT password_hash FROM users WHERE id = $1", userID).Scan(&currentHash)
	if err != nil {
		http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
		return
	}

	verified, _ := VerifyPassword(req.CurrentPassword, currentHash)
	if !verified {
		http.Error(w, `{"error":"current password is incorrect"}`, http.StatusUnauthorized)
		return
	}

	// Hash new password
	newHash, err := HashPassword(req.NewPassword)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}

	_, err = h.db.ExecContext(r.Context(), "UPDATE users SET password_hash = $1 WHERE id = $2", newHash, userID)
	if err != nil {
		http.Error(w, `{"error":"failed to update password"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
}
