package invite

import (
	"crypto/rand"
	"database/sql"
	"encoding/json"
	"errors"
	"math/big"
	"net/http"
	"time"

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

const codeLength = 8
const codeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

type Handler struct {
	db *sql.DB
}

func NewHandler(db *sql.DB) *Handler {
	return &Handler{db: db}
}

func (h *Handler) RegisterRoutes(r chi.Router) {
	r.Post("/servers/{serverID}/invites", h.Create)
	r.Get("/invites/{code}", h.Get)
	r.Post("/invites/{code}/join", h.Join)
}

type createInviteRequest struct {
	ExpiresHours *int `json:"expires_hours"`
	MaxUses      *int `json:"max_uses"`
}

type inviteResponse struct {
	ID         string  `json:"id"`
	Code       string  `json:"code"`
	ServerID   string  `json:"server_id"`
	ServerName string  `json:"server_name"`
	CreatorID  string  `json:"created_by"`
	MaxUses    *int    `json:"max_uses"`
	UseCount   int     `json:"use_count"`
	ExpiresAt  *string `json:"expires_at"`
	CreatedAt  string  `json:"created_at"`
}

// generateCode generates a cryptographically random alphanumeric code of the given length.
func generateCode(length int) (string, error) {
	result := make([]byte, length)
	max := big.NewInt(int64(len(codeChars)))
	for i := 0; i < length; i++ {
		n, err := rand.Int(rand.Reader, max)
		if err != nil {
			return "", err
		}
		result[i] = codeChars[n.Int64()]
	}
	return string(result), nil
}

// @Summary Create an invite
// @Description Create an invite code for a server
// @Tags invites
// @Accept json
// @Produce json
// @Security SessionAuth
// @Param serverID path string true "Server ID"
// @Param body body createInviteRequest false "Invite options"
// @Success 201 {object} inviteResponse
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /servers/{serverID}/invites [post]
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
	serverID := chi.URLParam(r, "serverID")
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	// Verify user is a member of the server
	var exists bool
	err := h.db.QueryRowContext(r.Context(), `
		SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)
	`, userID, serverID).Scan(&exists)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	if !exists {
		http.Error(w, `{"error":"not a member of this server"}`, http.StatusForbidden)
		return
	}

	var req createInviteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		// Allow empty body
		req = createInviteRequest{}
	}

	// Generate a unique code (retry on collision)
	var code string
	for i := 0; i < 5; i++ {
		code, err = generateCode(codeLength)
		if err != nil {
			http.Error(w, `{"error":"failed to generate invite code"}`, http.StatusInternalServerError)
			return
		}

		// Check uniqueness
		var dup bool
		err = h.db.QueryRowContext(r.Context(), `
			SELECT EXISTS(SELECT 1 FROM invites WHERE code = $1)
		`, code).Scan(&dup)
		if err != nil {
			http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
			return
		}
		if !dup {
			break
		}
		if i == 4 {
			http.Error(w, `{"error":"failed to generate unique code"}`, http.StatusInternalServerError)
			return
		}
	}

	// Compute expiration
	var expiresAt *time.Time
	if req.ExpiresHours != nil && *req.ExpiresHours > 0 {
		t := time.Now().Add(time.Duration(*req.ExpiresHours) * time.Hour)
		expiresAt = &t
	}

	var maxUses *int
	if req.MaxUses != nil && *req.MaxUses > 0 {
		maxUses = req.MaxUses
	}

	var invite inviteResponse
	var expiresAtStr sql.NullString
	var createdAtStr sql.NullString

	err = h.db.QueryRowContext(r.Context(), `
		INSERT INTO invites (code, server_id, created_by, max_uses, expires_at)
		VALUES ($1, $2, $3, $4, $5)
		RETURNING id, code, server_id, created_by, max_uses, use_count, expires_at::text, created_at::text
	`, code, serverID, userID, maxUses, expiresAt).Scan(
		&invite.ID, &invite.Code, &invite.ServerID, &invite.CreatorID,
		&invite.MaxUses, &invite.UseCount, &expiresAtStr, &createdAtStr,
	)
	if err != nil {
		http.Error(w, `{"error":"failed to create invite"}`, http.StatusInternalServerError)
		return
	}
	if expiresAtStr.Valid {
		invite.ExpiresAt = &expiresAtStr.String
	}
	invite.CreatedAt = createdAtStr.String

	// Fetch server name
	err = h.db.QueryRowContext(r.Context(), `
		SELECT name FROM servers WHERE id = $1
	`, serverID).Scan(&invite.ServerName)
	if err != nil {
		// Non-fatal: just leave server_name empty
		invite.ServerName = ""
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(invite)
}

// @Summary Get invite info
// @Description Get information about an invite by code
// @Tags invites
// @Produce json
// @Param code path string true "Invite code"
// @Success 200 {object} inviteResponse
// @Failure 404 {object} map[string]string
// @Router /invites/{code} [get]
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
	code := chi.URLParam(r, "code")

	var invite inviteResponse
	var expiresAtStr sql.NullString
	var createdAtStr sql.NullString

	err := h.db.QueryRowContext(r.Context(), `
		SELECT i.id, i.code, i.server_id, COALESCE(s.name, ''), i.created_by,
			   i.max_uses, i.use_count, i.expires_at::text, i.created_at::text
		FROM invites i
		LEFT JOIN servers s ON s.id = i.server_id
		WHERE i.code = $1
	`, code).Scan(
		&invite.ID, &invite.Code, &invite.ServerID, &invite.ServerName, &invite.CreatorID,
		&invite.MaxUses, &invite.UseCount, &expiresAtStr, &createdAtStr,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, `{"error":"invite not found"}`, http.StatusNotFound)
		} else {
			http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		}
		return
	}
	if expiresAtStr.Valid {
		invite.ExpiresAt = &expiresAtStr.String
	}
	invite.CreatedAt = createdAtStr.String

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

// @Summary Join server via invite
// @Description Join a server using an invite code
// @Tags invites
// @Produce json
// @Security SessionAuth
// @Param code path string true "Invite code"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /invites/{code}/join [post]
func (h *Handler) Join(w http.ResponseWriter, r *http.Request) {
	code := chi.URLParam(r, "code")
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	// Fetch the invite
	var inviteID, serverID string
	var maxUses *int
	var useCount int
	var expiresAt sql.NullTime

	err := h.db.QueryRowContext(r.Context(), `
		SELECT id, server_id, max_uses, use_count, expires_at
		FROM invites WHERE code = $1
	`, code).Scan(&inviteID, &serverID, &maxUses, &useCount, &expiresAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, `{"error":"invite not found"}`, http.StatusNotFound)
		} else {
			http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		}
		return
	}

	// Check expiration
	if expiresAt.Valid && time.Now().After(expiresAt.Time) {
		http.Error(w, `{"error":"invite has expired"}`, http.StatusBadRequest)
		return
	}

	// Check max uses
	if maxUses != nil && useCount >= *maxUses {
		http.Error(w, `{"error":"invite has reached maximum uses"}`, http.StatusBadRequest)
		return
	}

	// Check if already a member
	var alreadyMember bool
	err = h.db.QueryRowContext(r.Context(), `
		SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)
	`, userID, serverID).Scan(&alreadyMember)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	if alreadyMember {
		http.Error(w, `{"error":"already a member of this server"}`, http.StatusConflict)
		return
	}

	// Use a transaction to insert member and increment use_count atomically
	tx, err := h.db.BeginTx(r.Context(), nil)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	defer tx.Rollback()

	_, err = tx.ExecContext(r.Context(), `
		INSERT INTO members (user_id, server_id) VALUES ($1, $2)
	`, userID, serverID)
	if err != nil {
		http.Error(w, `{"error":"failed to join server"}`, http.StatusInternalServerError)
		return
	}

	_, err = tx.ExecContext(r.Context(), `
		UPDATE invites SET use_count = use_count + 1 WHERE id = $1
	`, inviteID)
	if err != nil {
		http.Error(w, `{"error":"failed to update invite"}`, http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(); err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]string{
		"server_id": serverID,
		"message":   "successfully joined server",
	})
}
