package web

import (
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"

	"backend/internal/auth"
	"backend/internal/db/sqlc"
	"backend/templates"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/gorilla/csrf"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
)

// TasksDeps holds dependencies for all task handlers.
type TasksDeps struct {
	Queries *sqlc.Queries
}

// validTaskStatuses is the set of accepted status string values.
var validTaskStatuses = map[string]sqlc.TaskStatus{
	"todo":        sqlc.TaskStatusTodo,
	"in_progress": sqlc.TaskStatusInProgress,
	"in_review":   sqlc.TaskStatusInReview,
	"done":        sqlc.TaskStatusDone,
}

// parseTaskStatus parses a status string and returns the sqlc.TaskStatus. If
// the string is not a valid status, it defaults to TaskStatusTodo.
func parseTaskStatus(s string) sqlc.TaskStatus {
	if ts, ok := validTaskStatuses[s]; ok {
		return ts
	}
	return sqlc.TaskStatusTodo
}

func loadEtapesForTaskForm(w http.ResponseWriter, r *http.Request, q *sqlc.Queries, tabloID uuid.UUID) ([]sqlc.Etape, bool) {
	etapes, err := q.ListEtapesByTablo(r.Context(), tabloID)
	if err != nil {
		slog.Default().Error("tasks form: ListEtapesByTablo failed", "tablo_id", tabloID, "err", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return nil, false
	}
	if etapes == nil {
		etapes = []sqlc.Etape{}
	}
	return etapes, true
}

// loadOwnedTabloForTask is the shared preamble for all /tablos/{id}/tasks/{task_id}*
// handlers. It calls loadOwnedTablo for tablo ownership verification, then parses
// the {task_id} URL param and fetches the task (verifying it belongs to the tablo).
// Returns (tablo, task, user, true) on success. On failure it writes the appropriate
// HTTP response and returns false; callers must return immediately.
func loadOwnedTabloForTask(w http.ResponseWriter, r *http.Request, deps TasksDeps) (sqlc.Tablo, sqlc.Task, *auth.User, bool) {
	// Re-use TablosDeps to call the existing loadOwnedTablo helper.
	tablo, user, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
	if !ok {
		return sqlc.Tablo{}, sqlc.Task{}, nil, false
	}

	taskID, err := uuid.Parse(chi.URLParam(r, "task_id"))
	if err != nil {
		http.NotFound(w, r)
		return sqlc.Tablo{}, sqlc.Task{}, nil, false
	}

	task, err := deps.Queries.GetTaskByID(r.Context(), sqlc.GetTaskByIDParams{
		ID:      taskID,
		TabloID: tablo.ID,
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.NotFound(w, r)
			return sqlc.Tablo{}, sqlc.Task{}, nil, false
		}
		slog.Default().Error("tasks: GetTaskByID failed", "id", taskID, "err", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return sqlc.Tablo{}, sqlc.Task{}, nil, false
	}

	return tablo, task, user, true
}

// TaskNewFormHandler handles GET /tablos/{id}/tasks/new?status={status}.
// Returns the TaskCreateForm fragment for HTMX insertion into the add-task slot.
func TaskNewFormHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		statusStr := r.URL.Query().Get("status")
		status := parseTaskStatus(statusStr)
		etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID)
		if !ok {
			return
		}
		filter := templates.EtapeFilter{Kind: templates.EtapeFilterAll}
		if rawEtape := strings.TrimSpace(r.URL.Query().Get("etape")); rawEtape != "" {
			if rawEtape == "unassigned" {
				filter.Kind = templates.EtapeFilterUnassigned
			} else if etapeID, err := uuid.Parse(rawEtape); err == nil {
				if _, err := deps.Queries.GetEtapeByID(r.Context(), sqlc.GetEtapeByIDParams{ID: etapeID, TabloID: tablo.ID}); err == nil {
					filter = templates.EtapeFilter{Kind: templates.EtapeFilterEtape, EtapeID: etapeID}
				}
			}
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TaskCreateFormFragment(
			tablo.ID,
			status,
			templates.TaskCreateForm{EtapeID: filter.TaskEtapeIDValue()},
			templates.TaskCreateErrors{},
			csrf.Token(r),
			filter,
			etapes,
		).Render(r.Context(), w)
	}
}

// TaskCancelNewHandler handles GET /tablos/{id}/tasks/cancel-new?status={status}.
// Returns the AddTaskTrigger fragment to restore the "+ Add task" button.
func TaskCancelNewHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		statusStr := r.URL.Query().Get("status")
		status := parseTaskStatus(statusStr)
		filter := templates.EtapeFilter{Kind: templates.EtapeFilterAll}
		if rawEtape := strings.TrimSpace(r.URL.Query().Get("etape")); rawEtape == "unassigned" {
			filter.Kind = templates.EtapeFilterUnassigned
		} else if rawEtape != "" {
			if etapeID, err := uuid.Parse(rawEtape); err == nil {
				filter = templates.EtapeFilter{Kind: templates.EtapeFilterEtape, EtapeID: etapeID}
			}
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.AddTaskTrigger(tablo.ID, status, csrf.Token(r), filter).Render(r.Context(), w)
	}
}

// TaskCreateHandler handles POST /tablos/{id}/tasks.
// Validates title, inserts the task at max+100 position, and returns the new
// TaskCard fragment (HTMX) or redirects to the tablo detail page (non-HTMX).
//
// Security invariants:
//   - title validated non-empty, max 255 chars (T-04-04)
//   - status validated against known TaskStatus constants (T-04-05)
//   - tablo ownership verified by loadOwnedTablo (T-04-07)
func TaskCreateHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		ctx := r.Context()

		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		title := strings.TrimSpace(r.PostFormValue("title"))
		statusStr := r.PostFormValue("status")
		status := parseTaskStatus(statusStr)
		submittedEtapeID := lastPostFormValue(r, "etape_id")
		etapeID, _, err := parseOwnedEtapeID(r, deps.Queries, tablo.ID, submittedEtapeID)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				http.NotFound(w, r)
				return
			}
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}
		filter := templates.EtapeFilter{Kind: templates.EtapeFilterAll}
		if etapeID.Valid {
			filter = templates.EtapeFilter{Kind: templates.EtapeFilterEtape, EtapeID: uuid.UUID(etapeID.Bytes)}
		}
		etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID)
		if !ok {
			return
		}

		var errs templates.TaskCreateErrors
		if title == "" {
			errs.Title = "Title is required"
		} else if len(title) > 255 {
			errs.Title = "Title must be 255 characters or fewer"
		}

		if errs.Title != "" {
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			if r.Header.Get("HX-Request") == "true" {
				w.Header().Set("HX-Retarget", "#add-task-slot-"+statusStr)
				w.Header().Set("HX-Reswap", "innerHTML")
			}
			w.WriteHeader(http.StatusUnprocessableEntity)
			if r.Header.Get("HX-Request") == "true" {
				_ = templates.TaskCreateFormFragment(
					tablo.ID,
					status,
					templates.TaskCreateForm{Title: title, Status: statusStr, EtapeID: submittedEtapeID},
					errs,
					csrf.Token(r),
					filter,
					etapes,
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		// Compute next position: max + 100
		maxPos, err := deps.Queries.MaxPositionByTabloAndStatus(ctx, sqlc.MaxPositionByTabloAndStatusParams{
			TabloID: tablo.ID,
			Status:  status,
		})
		if err != nil {
			slog.Default().Error("tasks create: MaxPositionByTabloAndStatus failed", "tablo_id", tablo.ID, "err", err)
			errs.General = "Something went wrong. Please try again."
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusInternalServerError)
			if r.Header.Get("HX-Request") == "true" {
				_ = templates.TaskCreateFormFragment(
					tablo.ID,
					status,
					templates.TaskCreateForm{Title: title, Status: statusStr, EtapeID: submittedEtapeID},
					errs,
					csrf.Token(r),
					filter,
					etapes,
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		const maxAllowedPosition = int32(2_000_000_000)
		if maxPos > maxAllowedPosition-100 {
			errs.General = "Column has too many tasks."
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			if r.Header.Get("HX-Request") == "true" {
				w.Header().Set("HX-Retarget", "#add-task-slot-"+statusStr)
				w.Header().Set("HX-Reswap", "innerHTML")
				w.WriteHeader(http.StatusUnprocessableEntity)
				_ = templates.TaskCreateFormFragment(
					tablo.ID,
					status,
					templates.TaskCreateForm{Title: title, Status: statusStr, EtapeID: submittedEtapeID},
					errs,
					csrf.Token(r),
					filter,
					etapes,
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		task, err := deps.Queries.InsertTask(ctx, sqlc.InsertTaskParams{
			TabloID:     tablo.ID,
			Title:       title,
			Description: pgtype.Text{Valid: false},
			Status:      status,
			Position:    maxPos + 100,
			EtapeID:     etapeID,
		})
		if err != nil {
			slog.Default().Error("tasks create: InsertTask failed", "tablo_id", tablo.ID, "err", err)
			errs.General = "Something went wrong. Please try again."
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusInternalServerError)
			if r.Header.Get("HX-Request") == "true" {
				_ = templates.TaskCreateFormFragment(
					tablo.ID,
					status,
					templates.TaskCreateForm{Title: title, Status: statusStr, EtapeID: submittedEtapeID},
					errs,
					csrf.Token(r),
					filter,
					etapes,
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		// HTMX: set retarget/reswap headers and return combined card+OOB fragment.
		if r.Header.Get("HX-Request") == "true" {
			_, refreshedEtapes, refreshedCounts, refreshedFilter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
			if !ok {
				return
			}
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.Header().Set("HX-Reswap", "beforeend")
			w.Header().Set("HX-Retarget", "#column-"+string(status))
			_ = templates.TaskCardOOB(status, task, tablo.ID, csrf.Token(r), refreshedFilter, refreshedEtapes, refreshedCounts).Render(ctx, w)
			return
		}
		http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
	}
}

func lastPostFormValue(r *http.Request, key string) string {
	values := r.PostForm[key]
	if len(values) == 0 {
		return ""
	}
	return values[len(values)-1]
}

// TaskShowHandler handles GET /tablos/{id}/tasks/{task_id}/show.
// Returns the TaskCard fragment — used by the cancel paths after edit or delete-confirm.
func TaskShowHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TaskCard(tablo.ID, task, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TaskDeleteConfirmHandler handles GET /tablos/{id}/tasks/{task_id}/delete-confirm.
// Returns the delete confirmation fragment for HTMX outerHTML swap.
func TaskDeleteConfirmHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TaskDeleteConfirmFragment(tablo.ID, task, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TaskDeleteHandler handles POST /tablos/{id}/tasks/{task_id}/delete.
// Hard-deletes the task and returns:
//   - HTMX: 200 + empty div with id="task-{task_id}" (TASK-06)
//   - Non-HTMX: 303 redirect to /tablos/{id}
//
// Security: loadOwnedTabloForTask verifies tablo ownership AND task-to-tablo
// binding (T-04-03 — tablo_id in WHERE clause).
func TaskDeleteHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
		if !ok {
			return
		}

		if err := deps.Queries.DeleteTask(r.Context(), sqlc.DeleteTaskParams{
			ID:      task.ID,
			TabloID: tablo.ID,
		}); err != nil {
			slog.Default().Error("tasks delete: DeleteTask failed", "id", task.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		if r.Header.Get("HX-Request") == "true" {
			_, refreshedEtapes, refreshedCounts, refreshedFilter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
			if !ok {
				return
			}
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			// Return empty zone div so HTMX removes the card from the DOM (TASK-06).
			_ = templates.TaskCardGone(task.ID, tablo.ID, csrf.Token(r), refreshedFilter, refreshedEtapes, refreshedCounts).Render(r.Context(), w)
			return
		}
		http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
	}
}

// TaskEditHandler handles GET /tablos/{id}/tasks/{task_id}/edit.
// Returns 200 + TaskEditFragment HTML pre-filled with existing task data (TASK-03).
// Security: loadOwnedTabloForTask verifies ownership; non-owners receive 404.
func TaskEditHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
		if !ok {
			return
		}
		etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TaskEditFragment(
			tablo.ID,
			task,
			etapes,
			templates.TaskUpdateForm{
				Title:       task.Title,
				Description: task.Description.String,
			},
			templates.TaskUpdateErrors{},
			csrf.Token(r),
		).Render(r.Context(), w)
	}
}

// TaskUpdateHandler handles POST /tablos/{id}/tasks/{task_id}.
// Validates title and description, updates title+description in DB, and returns:
//   - HTMX: 200 + TaskCard fragment (success) or 422 + TaskEditFragment (validation error)
//   - Non-HTMX: 303 redirect to /tablos/{id}
//
// Security invariants:
//   - title validated non-empty, max 255 chars (T-04-04)
//   - existing Status and Position preserved — only title+description change (T-04-12)
//   - tablo+task ownership verified by loadOwnedTabloForTask (T-04-03)
func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
		if !ok {
			return
		}
		ctx := r.Context()

		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		title := strings.TrimSpace(r.PostFormValue("title"))
		description := strings.TrimSpace(r.PostFormValue("description"))
		etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID)
		if !ok {
			return
		}
		etapeID, _, err := parseOwnedEtapeID(r, deps.Queries, tablo.ID, r.PostFormValue("etape_id"))
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				http.NotFound(w, r)
				return
			}
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		var errs templates.TaskUpdateErrors
		if title == "" {
			errs.Title = "Title is required."
		} else if len(title) > 255 {
			errs.Title = "Title must be 255 characters or fewer."
		}

		if errs.Title != "" {
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusUnprocessableEntity)
			if r.Header.Get("HX-Request") == "true" {
				_ = templates.TaskEditFragment(
					tablo.ID,
					task,
					etapes,
					templates.TaskUpdateForm{Title: title, Description: description},
					errs,
					csrf.Token(r),
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		// Preserve existing Status and Position — only title+description change (T-04-12).
		updated, err := deps.Queries.UpdateTask(ctx, sqlc.UpdateTaskParams{
			ID:          task.ID,
			Title:       title,
			Description: pgtype.Text{String: description, Valid: description != ""},
			Status:      task.Status,
			Position:    task.Position,
			EtapeID:     etapeID,
		})
		if err != nil {
			slog.Default().Error("tasks update: UpdateTask failed", "id", task.ID, "err", err)
			errs.General = "Something went wrong. Please try again."
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.WriteHeader(http.StatusInternalServerError)
			if r.Header.Get("HX-Request") == "true" {
				_ = templates.TaskEditFragment(
					tablo.ID,
					task,
					etapes,
					templates.TaskUpdateForm{Title: title, Description: description},
					errs,
					csrf.Token(r),
				).Render(ctx, w)
				return
			}
			http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
			return
		}

		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			_ = templates.TaskCard(tablo.ID, updated, csrf.Token(r)).Render(ctx, w)
			return
		}
		http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
	}
}

// TaskReorderHandler handles POST /tablos/{id}/tasks/reorder.
// Reads task_id + task_col (array form fields) to update task status and position in DB,
// then returns the updated KanbanBoard outerHTML.
//
// Security invariants:
//   - tablo ownership verified by loadOwnedTablo (T-04-10)
//   - each task fetched via GetTaskByID before update — title/description preserved (T-04-08)
//   - invalid task UUIDs silently skipped (D-05 last-write-wins)
//   - task_col values validated as known TaskStatus via parseTaskStatus (T-04-09)
func TaskReorderHandler(deps TasksDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		ctx := r.Context()

		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}

		taskIDs := r.Form["task_id"]
		taskCols := r.Form["task_col"]

		// Support both array-based (Sortable.js) and single-value form submissions.
		// Single-value fields ("status" and "position") are used by the existing
		// test scaffold — fall back to them when task_col array is absent.
		if len(taskCols) == 0 {
			// Single-task reorder: task_id + status + optional position.
			if len(taskIDs) == 0 {
				taskIDs = r.Form["task_id"]
			}
			statusVal := r.FormValue("status")
			positionVal := r.FormValue("position")
			taskCols = make([]string, len(taskIDs))
			for i := range taskCols {
				taskCols[i] = statusVal
			}

			if len(taskIDs) == 1 && positionVal != "" {
				// Single-task update with explicit position.
				taskID, err := uuid.Parse(taskIDs[0])
				if err == nil {
					existing, err := deps.Queries.GetTaskByID(ctx, sqlc.GetTaskByIDParams{
						ID:      taskID,
						TabloID: tablo.ID,
					})
					if err == nil {
						newStatus := parseTaskStatus(taskCols[0])
						var newPos int32
						if _, scanErr := fmt.Sscanf(positionVal, "%d", &newPos); scanErr != nil || newPos <= 0 {
							newPos = 100
						}
						if _, err := deps.Queries.UpdateTask(ctx, sqlc.UpdateTaskParams{
							ID:          existing.ID,
							Title:       existing.Title,
							Description: existing.Description,
							Status:      newStatus,
							Position:    newPos,
							EtapeID:     existing.EtapeID,
						}); err != nil {
							slog.Default().Error("tasks reorder: UpdateTask failed (single)",
								"task_id", existing.ID, "err", err)
							http.Error(w, "internal server error", http.StatusInternalServerError)
							return
						}
					}
				}
				tasks, _, _, filter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
				if !ok {
					return
				}
				w.Header().Set("Content-Type", "text/html; charset=utf-8")
				_ = templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter).Render(ctx, w)
				return
			}
		}

		if len(taskIDs) != len(taskCols) {
			http.Error(w, "mismatched task_id and task_col arrays", http.StatusBadRequest)
			return
		}

		// Process each task: fetch existing row (T-04-08), update status+position only.
		for i, rawID := range taskIDs {
			taskID, err := uuid.Parse(rawID)
			if err != nil {
				// Invalid UUID — skip silently (D-05).
				continue
			}
			newStatus := parseTaskStatus(taskCols[i])
			newPos := int32((i + 1) * 100)

			existing, err := deps.Queries.GetTaskByID(ctx, sqlc.GetTaskByIDParams{
				ID:      taskID,
				TabloID: tablo.ID,
			})
			if err != nil {
				// Task not found or belongs to different tablo — skip (T-04-10).
				continue
			}

			// Preserve title+description; only update status+position (T-04-08).
			if _, err := deps.Queries.UpdateTask(ctx, sqlc.UpdateTaskParams{
				ID:          existing.ID,
				Title:       existing.Title,
				Description: existing.Description,
				Status:      newStatus,
				Position:    newPos,
				EtapeID:     existing.EtapeID,
			}); err != nil {
				slog.Default().Error("tasks reorder: UpdateTask failed",
					"task_id", existing.ID, "err", err)
				http.Error(w, "internal server error", http.StatusInternalServerError)
				return
			}
		}

		tasks, _, _, filter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
		if !ok {
			return
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter).Render(ctx, w)
	}
}
