package scheduling

import (
	"encoding/json"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"crussell/db"
	"crussell/internal/validators"
)

type ExceptionalHours struct {
	ID        int    `json:"id"`
	GroupID   int    `json:"groupId"`
	Weekday   int    `json:"weekday" validate:"gte=0,lte=6"`
	StartTime string `json:"startTime" validate:"required"`
	EndTime   string `json:"endTime" validate:"required"`
	IsOpen    bool   `json:"isOpen"`
}

type ExceptionalGroup struct {
	ID          int                `json:"id"`
	Name        string             `json:"name" validate:"required"`
	Description string             `json:"description" validate:"required"`
	Hours       []ExceptionalHours `json:"hours,omitempty" validate:"required,min=7,max=7,dive"`
	WeekStarts  []string           `json:"weekStarts,omitempty" validate:"required,min=1,dive,required"`
}

// --- List Groups with Hours and Applications ---
func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Conn.Query(r.Context(), `
		SELECT id, name, description
		FROM exceptional_working_hours_groups
		ORDER BY id DESC
	`)
	if err != nil {
		http.Error(w, "failed to fetch groups", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	// Collect all groups first, then close rows to avoid "conn busy" when
	// the context carries a test transaction (single connection).
	var groups []ExceptionalGroup
	for rows.Next() {
		var g ExceptionalGroup
		if err := rows.Scan(&g.ID, &g.Name, &g.Description); err != nil {
			rows.Close()
			http.Error(w, "failed to scan group", http.StatusInternalServerError)
			return
		}
		groups = append(groups, g)
	}
	rows.Close() // explicit close before subsequent queries (hours/apps below); defer covers error path

	if err := rows.Err(); err != nil {
		http.Error(w, "error iterating groups", http.StatusInternalServerError)
		return
	}

	// Load hours and applications for each group (separate queries after rows are closed)
	for i := range groups {
		// Load 7-day hours
		hoursRows, err := db.Conn.Query(r.Context(), `
			SELECT id, weekday, start_time::text, end_time, is_open
			FROM exceptional_working_hours
			WHERE group_id=$1 ORDER BY weekday
		`, groups[i].ID)
		if err != nil {
			http.Error(w, "failed to fetch group hours", http.StatusInternalServerError)
			return
		}

		for hoursRows.Next() {
			var h ExceptionalHours
			if err := hoursRows.Scan(&h.ID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil {
				hoursRows.Close()
				http.Error(w, "failed to scan hours", http.StatusInternalServerError)
				return
			}
			h.GroupID = groups[i].ID
			groups[i].Hours = append(groups[i].Hours, h)
		}
		hoursRows.Close()

		if err := hoursRows.Err(); err != nil {
			http.Error(w, "error iterating hours", http.StatusInternalServerError)
			return
		}

		// Load applied week starts
		weekRows, err := db.Conn.Query(r.Context(), `
			SELECT week_start
			FROM exceptional_group_applications
			WHERE group_id=$1 ORDER BY week_start
		`, groups[i].ID)
		if err != nil {
			http.Error(w, "failed to fetch applications", http.StatusInternalServerError)
			return
		}

		for weekRows.Next() {
			var weekStart time.Time
			if err := weekRows.Scan(&weekStart); err != nil {
				weekRows.Close()
				http.Error(w, "failed to scan week_start", http.StatusInternalServerError)
				return
			}
			groups[i].WeekStarts = append(groups[i].WeekStarts, weekStart.Format("2006-01-02"))
		}
		weekRows.Close()

		if err := weekRows.Err(); err != nil {
			http.Error(w, "error iterating applications", http.StatusInternalServerError)
			return
		}
	}


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

// --- Create Group with Hours and Applications (bulk) ---
func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
	var g ExceptionalGroup
	if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
		http.Error(w, "invalid payload", http.StatusBadRequest)
		return
	}

	if err := validators.Validate.Struct(&g); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// M8
	// L5

	if len(g.Hours) != 7 {
		http.Error(w, "must provide exactly 7 weekday entries (0-6)", http.StatusBadRequest)
		return
	}

	// Validate weekdays and parse week_starts
	weekdaysSeen := make(map[int]bool)
	for _, h := range g.Hours {
		if h.Weekday < 0 || h.Weekday > 6 {
			http.Error(w, "weekday must be 0-6", http.StatusBadRequest)
			return
		}
		if weekdaysSeen[h.Weekday] {
			http.Error(w, "duplicate weekday entries", http.StatusBadRequest)
			return
		}
		weekdaysSeen[h.Weekday] = true
		if !isValidTime15Min(h.StartTime) {
			http.Error(w, "start_time must be in 15-minute intervals (00, 15, 30, 45)", http.StatusBadRequest)
			return
		}
		if !isValidTime15Min(h.EndTime) {
			http.Error(w, "end_time must be in 15-minute intervals (00, 15, 30, 45)", http.StatusBadRequest)
			return
		}
	}

	var parsedWeeks []time.Time
	for _, ws := range g.WeekStarts {
		weekStart, err := time.Parse("2006-01-02", ws)
		if err != nil {
			http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest)
			return
		}
		if weekStart.Weekday() != time.Monday {
			http.Error(w, "week_start must be a Monday", http.StatusBadRequest)
			return
		}
		parsedWeeks = append(parsedWeeks, weekStart)
	}

	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)
    }
}()

	// Create group
	err = tx.QueryRow(r.Context(), `
		INSERT INTO exceptional_working_hours_groups (name, description)
		VALUES ($1, $2) RETURNING id
	`, g.Name, g.Description).Scan(&g.ID)
	if err != nil {
		http.Error(w, "failed to create group", http.StatusInternalServerError)
		return
	}

	// Insert hours and collect their IDs
	inputHours := g.Hours
	g.Hours = []ExceptionalHours{} // Clear and rebuild with IDs
	for _, h := range inputHours {
		var id int
		err := tx.QueryRow(r.Context(), `
			INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
			VALUES ($1, $2, $3, $4, $5)
			RETURNING id
		`, g.ID, h.Weekday, h.StartTime, h.EndTime, h.IsOpen).Scan(&id)
		if err != nil {
			http.Error(w, "failed to insert hours", http.StatusInternalServerError)
			return
		}

		h.ID = id
		h.GroupID = g.ID
		g.Hours = append(g.Hours, h)
	}

	// Insert applications
	if _, err := tx.Exec(r.Context(), `
		INSERT INTO exceptional_group_applications (group_id, week_start)
		SELECT $1, unnest($2::date[])
	`, g.ID, parsedWeeks); err != nil {
		http.Error(w, "failed to insert applications", http.StatusInternalServerError)
		return
	}

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


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

// --- Delete Group (cascades to hours and applications) ---
func DeleteExceptionalGroup(w http.ResponseWriter, r *http.Request) {
	// Extract group ID from URL path or query params
	// Assuming you have a router that provides this, e.g. chi, mux, etc.
	// For this example, we'll use query param: DELETE /exceptional-groups?id=123
	idStr := r.URL.Query().Get("id")
	if idStr == "" {
		http.Error(w, "missing id parameter", http.StatusBadRequest)
		return
	}

	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "invalid id parameter", http.StatusBadRequest)
		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 exceptional_working_hours_groups WHERE id=$1
	`, id)
	if err != nil {
		http.Error(w, "failed to delete group", http.StatusInternalServerError)
		return
	}

	rowsAffected := result.RowsAffected()
	if rowsAffected == 0 {
		http.Error(w, "group not found", http.StatusNotFound)
		return
	}

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

	w.WriteHeader(http.StatusNoContent)
}

// --- Update Applied Weeks (replaces all applications for a group) ---
func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
	var req struct {
		GroupID    int      `json:"groupId" validate:"required"`
		WeekStarts []string `json:"weekStarts" validate:"required,min=1,dive,required"`
	}

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

	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// M8
	// L5

	// Validate and parse weeks
	var parsedWeeks []time.Time
	for _, ws := range req.WeekStarts {
		weekStart, err := time.Parse("2006-01-02", ws)
		if err != nil {
			http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest)
			return
		}
		if weekStart.Weekday() != time.Monday {
			http.Error(w, "week_start must be a Monday", http.StatusBadRequest)
			return
		}
		parsedWeeks = append(parsedWeeks, weekStart)
	}

	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)
    }
}()

	// Delete existing applications for this group
	_, err = tx.Exec(r.Context(), `
		DELETE FROM exceptional_group_applications WHERE group_id=$1
	`, req.GroupID)
	if err != nil {
		http.Error(w, "failed to delete existing applications", http.StatusInternalServerError)
		return
	}

	// Insert new applications
	if _, err := tx.Exec(r.Context(), `
		INSERT INTO exceptional_group_applications (group_id, week_start)
		SELECT $1, unnest($2::date[])
	`, req.GroupID, parsedWeeks); err != nil {
		http.Error(w, "failed to insert applications", http.StatusInternalServerError)
		return
	}

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

	w.WriteHeader(http.StatusNoContent)
}
