package admin

import (
	"crussell/db"
	"crussell/internal/validators"
	"crussell/mw"
	"database/sql"
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"
)

type CustomService struct {
	ID                 string     `json:"id"`
	Name               string     `json:"name"`
	Description        *string    `json:"description,omitempty"`
	Price              float64    `json:"price"`
	DurationMinutes    int        `json:"duration_minutes"`
	MinimumAgeRequired int        `json:"minimum_age_required"`
	Notes              *string    `json:"notes,omitempty"`
	CreatedAt          time.Time  `json:"created_at"`
	CreatedBy          *string    `json:"created_by,omitempty"`
	UsageCount         int        `json:"usage_count"`
	LastUsedAt         *time.Time `json:"last_used_at,omitempty"`
}

type CreateCustomServiceRequest struct {
	Name               string  `json:"name" validate:"required,min=1,max=100"`
	Description        *string `json:"description,omitempty" validate:"omitempty,max=1000"`
	Price              float64 `json:"price" validate:"required,gt=0"`
	DurationMinutes    int     `json:"duration_minutes" validate:"required,gt=0,lte=480"`
	MinimumAgeRequired int     `json:"minimum_age_required"`
	Notes              *string `json:"notes,omitempty"`
}

type UpdateCustomServiceRequest struct {
	Name               *string  `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
	Description        *string  `json:"description,omitempty" validate:"omitempty,max=1000"`
	Price              *float64 `json:"price,omitempty" validate:"omitempty,gt=0"`
	DurationMinutes    *int     `json:"duration_minutes,omitempty" validate:"omitempty,gt=0,lte=480"`
	MinimumAgeRequired *int     `json:"minimum_age_required,omitempty"`
	Notes              *string  `json:"notes,omitempty"`
}

type CustomServiceListResponse struct {
	Services   []CustomService `json:"services"`
	Total      int64           `json:"total"`
	Page       int             `json:"page"`
	PerPage    int             `json:"per_page"`
	NextCursor *string         `json:"next_cursor,omitempty"`
}

// parseCursor splits a "createdAt|id" cursor string into its components.

func GetCustomServices(w http.ResponseWriter, r *http.Request) {
	perPageStr := r.URL.Query().Get("per_page")
	q := r.URL.Query().Get("q")
	popularStr := r.URL.Query().Get("popular")
	cursorStr := r.URL.Query().Get("cursor")

	perPage := 20
	if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
		perPage = pp
	}

	if popularStr != "" {
		n, err := strconv.Atoi(popularStr)
		if err != nil || n <= 0 || n > 20 {
			n = 3
		}
		rows, err := db.Conn.Query(r.Context(), `
			SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
			FROM custom_services
			WHERE usage_count > 0
			ORDER BY usage_count DESC, last_used_at DESC NULLS LAST
			LIMIT $1
		`, n)
		if err != nil {
			http.Error(w, "Failed to fetch custom services: "+err.Error(), http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var services []CustomService
		for rows.Next() {
			var cs CustomService
			if err := rows.Scan(&cs.ID, &cs.Name, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &cs.CreatedAt, &cs.UsageCount); err != nil {
				continue
			}
			services = append(services, cs)
		}
		if err := rows.Err(); err != nil {
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		if services == nil {
			services = []CustomService{}
		}
		_ = json.NewEncoder(w).Encode(services)
		return
	}

	var dataQuery string
	var dataArgs []any

	if q != "" {
		dataQuery = `
			SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
			FROM custom_services
			WHERE name ILIKE $1 OR description ILIKE $1
		`
		dataArgs = []any{"%" + q + "%"}

		dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1)
		dataArgs = append(dataArgs, perPage+1)
	} else {
		dataQuery = `
			SELECT id, name, price, duration_minutes, minimum_age_required, created_at, usage_count
			FROM custom_services
		`

		// Cursor-based pagination
		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			dataQuery += " WHERE (created_at, id) < ($1, $2)"
			dataArgs = append(dataArgs, cursorCreatedAt, cursorID)
		}
		dataQuery += " ORDER BY created_at DESC, id DESC LIMIT $" + strconv.Itoa(len(dataArgs)+1)
		dataArgs = append(dataArgs, perPage+1)
	}

	rows, err := db.Conn.Query(r.Context(), dataQuery, dataArgs...)
	if err != nil {
		http.Error(w, "Failed to fetch custom services: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var services []CustomService
	var total int64

	for rows.Next() {
		var cs CustomService
		if err := rows.Scan(&cs.ID, &cs.Name, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &cs.CreatedAt, &cs.UsageCount); err != nil {
			continue
		}
		services = append(services, cs)
	}
	if err := rows.Err(); err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Run count query ONLY after consuming the data query result set,
	// so pgx does not return "conn busy" on the same transaction.
	if q != "" {
		var countTotal int64
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services WHERE name ILIKE $1 OR description ILIKE $1", "%"+q+"%").Scan(&countTotal)
		total = countTotal
	} else {
		var countTotal int64
//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM custom_services").Scan(&countTotal)
		total = countTotal
	}

	// nextCursor is set only when we fetched perPage+1 items, proving a next page exists.
	var nextCursor *string
	if len(services) > perPage {
		services = services[:perPage]
		last := services[len(services)-1]
		cursor := last.CreatedAt.Format(time.RFC3339) + "|" + last.ID
		nextCursor = &cursor
	}

	if services == nil {
		services = []CustomService{}
	}
	_ = json.NewEncoder(w).Encode(CustomServiceListResponse{
		Services:   services,
		Total:      total,
		PerPage:    perPage,
		NextCursor: nextCursor,
	})
}

func CreateCustomService(w http.ResponseWriter, r *http.Request) {
	var req CreateCustomServiceRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if req.Name == "" {
		http.Error(w, "Name is required", http.StatusBadRequest)
		return
	}
	if req.Price <= 0 {
		http.Error(w, "Price must be greater than 0", http.StatusBadRequest)
		return
	}
	if req.DurationMinutes <= 0 || req.DurationMinutes > 480 {
		http.Error(w, "Duration must be between 1 and 480 minutes", http.StatusBadRequest)
		return
	}
	if req.MinimumAgeRequired < 0 || req.MinimumAgeRequired > 100 {
		http.Error(w, "Minimum age must be between 0 and 100", http.StatusBadRequest)
		return
	}

	var createdBy *string
	if userID, ok := r.Context().Value(mw.UserIDKey).(string); ok {
		createdBy = &userID
	}

	query := `
		INSERT INTO custom_services (name, description, price, duration_minutes, minimum_age_required, notes, created_by)
		VALUES ($1, $2, $3, $4, $5, $6, $7)
		RETURNING id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
	`

	var cs CustomService
	var desc, notes, createdByDB sql.NullString
	var lastUsedAt sql.NullTime

	err := db.Conn.QueryRow(r.Context(), query, req.Name, req.Description, req.Price, req.DurationMinutes, req.MinimumAgeRequired, req.Notes, createdBy).Scan(
		&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &notes, &cs.CreatedAt, &createdByDB, &cs.UsageCount, &lastUsedAt,
	)
	if err != nil {
		http.Error(w, "Failed to create custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}

	if desc.Valid {
		cs.Description = &desc.String
	}
	if notes.Valid {
		cs.Notes = &notes.String
	}
	if createdByDB.Valid {
		cs.CreatedBy = &createdByDB.String
	}
	if lastUsedAt.Valid {
		cs.LastUsedAt = &lastUsedAt.Time
	}

	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(cs)
}

func GetCustomService(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" || !validators.IsValidID(id) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

	var cs CustomService
	var desc, notes, createdBy sql.NullString
	var lastUsedAt sql.NullTime

	err := db.Conn.QueryRow(r.Context(), `
		SELECT id, name, description, price, duration_minutes, minimum_age_required, notes, created_at, created_by, usage_count, last_used_at
		FROM custom_services WHERE id = $1
	`, id).Scan(&cs.ID, &cs.Name, &desc, &cs.Price, &cs.DurationMinutes, &cs.MinimumAgeRequired, &notes, &cs.CreatedAt, &createdBy, &cs.UsageCount, &lastUsedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, "Failed to fetch custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}

	if desc.Valid {
		cs.Description = &desc.String
	}
	if notes.Valid {
		cs.Notes = &notes.String
	}
	if createdBy.Valid {
		cs.CreatedBy = &createdBy.String
	}
	if lastUsedAt.Valid {
		cs.LastUsedAt = &lastUsedAt.Time
	}

	_ = json.NewEncoder(w).Encode(cs)
}

func UpdateCustomService(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" || !validators.IsValidID(id) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

	var req UpdateCustomServiceRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	updates := make(map[string]any)
	if req.Name != nil {
		updates["name"] = *req.Name
	}
	if req.Description != nil {
		updates["description"] = *req.Description
	}
	if req.Price != nil {
		updates["price"] = *req.Price
	}
	if req.DurationMinutes != nil {
		updates["duration_minutes"] = *req.DurationMinutes
	}
	if req.MinimumAgeRequired != nil {
		updates["minimum_age_required"] = *req.MinimumAgeRequired
	}
	if req.Notes != nil {
		updates["notes"] = *req.Notes
	}

	if len(updates) == 0 {
		http.Error(w, "No fields to update", http.StatusBadRequest)
		return
	}

	// Whitelist validation: only allow known column names to prevent SQL injection
	// via dynamic map keys used as column identifiers.
	var allowedCustomServiceFields = map[string]bool{
		"name":                 true,
		"description":          true,
		"price":                true,
		"duration_minutes":     true,
		"minimum_age_required": true,
		"notes":                true,
	}
	for field := range updates {
		if !allowedCustomServiceFields[field] {
			http.Error(w, "Invalid field: "+field, http.StatusBadRequest)
			return
		}
	}

	setClauses := make([]string, 0, len(updates))
	args := make([]any, 0, len(updates)+1)
	argIdx := 1
	for field, val := range updates {
		setClauses = append(setClauses, field+" = $"+strconv.Itoa(argIdx))
		args = append(args, val)
		argIdx++
	}
	args = append(args, id)

	query := "UPDATE custom_services SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + strconv.Itoa(argIdx)

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	result, err := tx.Exec(r.Context(), query, args...)
	if err != nil {
		http.Error(w, "Failed to update custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if result.RowsAffected() == 0 {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

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

	_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service updated"})
}

func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" || !validators.IsValidID(id) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		http.Error(w, "Failed to start transaction", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var name, desc, notes sql.NullString
	var price float64
	var durationMinutes int
	var minimumAgeRequired int
	var createdBy sql.NullString

	err = tx.QueryRow(r.Context(), `
		SELECT name, description, price, duration_minutes, minimum_age_required, notes, created_by
		FROM custom_services WHERE id = $1
	`, id).Scan(&name, &desc, &price, &durationMinutes, &minimumAgeRequired, &notes, &createdBy)
	if errors.Is(err, pgx.ErrNoRows) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, "Failed to fetch custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}

	var newServiceID string
	err = tx.QueryRow(r.Context(), `
		INSERT INTO services (name, description, price, duration_minutes, minimum_age_required, created_by)
		VALUES ($1, $2, $3, $4, $5, $6)
		RETURNING id
	`, name.String, desc, price, durationMinutes, minimumAgeRequired, createdBy).Scan(&newServiceID)
	if err != nil {
		if err.Error() == "pq: duplicate key value violates unique constraint" || err.Error() == "duplicate key value violates unique constraint" {
			http.Error(w, "A service with this name already exists", http.StatusConflict)
			return
		}
		http.Error(w, "Failed to create service: "+err.Error(), http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(r.Context(), `
		INSERT INTO booking_services (booking_id, service_id, override_price, override_duration_minutes)
		SELECT booking_id, $1, override_price, override_duration_minutes
		FROM booking_custom_services WHERE custom_service_id = $2
	`, newServiceID, id)
	if err != nil {
		http.Error(w, "Failed to migrate booking references: "+err.Error(), http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(r.Context(), `DELETE FROM booking_custom_services WHERE custom_service_id = $1`, id)
	if err != nil {
		http.Error(w, "Failed to clean up booking references: "+err.Error(), http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
	if err != nil {
		http.Error(w, "Failed to delete custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		http.Error(w, "Failed to commit transaction", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(map[string]string{
		"message":           "Custom service promoted to regular service",
		"new_service_id":    newServiceID,
		"custom_service_id": id,
	})
}

func DeleteCustomService(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" || !validators.IsValidID(id) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

	var usageCount int
	err := db.Conn.QueryRow(r.Context(), `SELECT usage_count FROM custom_services WHERE id = $1`, id).Scan(&usageCount)
	if errors.Is(err, pgx.ErrNoRows) {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, "Failed to check usage: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if usageCount > 0 {
		http.Error(w, "Cannot delete: this custom service has been used in bookings. Promote it first.", http.StatusConflict)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	result, err := tx.Exec(r.Context(), `DELETE FROM custom_services WHERE id = $1`, id)
	if err != nil {
		http.Error(w, "Failed to delete custom service: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if result.RowsAffected() == 0 {
		http.Error(w, "Custom service not found", http.StatusNotFound)
		return
	}

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

	_ = json.NewEncoder(w).Encode(map[string]string{"message": "Custom service deleted"})
}

func joinStrings(strs []string, sep string) string {
	if len(strs) == 0 {
		return ""
	}
	var result strings.Builder
	result.WriteString(strs[0])
	for _, s := range strs[1:] {
		result.WriteString(sep + s)
	}
	return result.String()
}
