package web

import (
	"errors"
	"log/slog"
	"net/http"
	"regexp"
	"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"
)

// hexColorRE matches CSS hex colors: #RGB or #RRGGBB (case-insensitive).
var hexColorRE = regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)

// isValidCSSColor returns true when s is a valid hex color string.
func isValidCSSColor(s string) bool {
	return hexColorRE.MatchString(s)
}

// TablosDeps holds dependencies for all tablo handlers.
// Introduced in Plan 01 as a stub to allow handlers_tablos_test.go to compile.
// Plan 02 adds the actual handler implementations.
type TablosDeps struct {
	Queries *sqlc.Queries
}

// TablosListHandler handles GET / for authenticated users.
// Fetches all tablos for the current user newest-first and renders TablosDashboard.
// Returns the empty-state via TablosEmptyState when the user has no tablos (TABLO-01).
func TablosListHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		_, user, _ := auth.Authed(r.Context())

		tabloRows, err := deps.Queries.ListTablosByUserWithDiscussionUnread(r.Context(), user.ID)
		if err != nil {
			slog.Default().Error("tablos list: query failed", "user_id", user.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		if tabloRows == nil {
			tabloRows = []sqlc.ListTablosByUserWithDiscussionUnreadRow{}
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w)
	}
}

// TablosNewHandler handles GET /tablos/new.
// Returns the create form fragment for HTMX insertion into #create-form-slot.
// Works without HX-Request too (falls back to rendering the fragment as a full response).
func TablosNewHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloCreateFormFragment(
			templates.TabloCreateForm{},
			templates.TabloCreateErrors{},
			csrf.Token(r),
		).Render(r.Context(), w)
	}
}

// TablosCreateHandler handles POST /tablos.
// Validates, inserts the tablo, and either:
//   - HTMX path: sets HX-Retarget + HX-Reswap and renders TabloCardWithOOBFormClear (200)
//   - Non-HTMX path: 303 redirect to / (TABLO-06 degrade-gracefully)
//
// Security invariants:
//   - Form values read via r.PostFormValue only (gorilla/csrf consumes r.Body — Pitfall 2)
//   - User extracted from RequireAuth-gated context (T-03-02-01)
//   - Title validates non-empty and <= 255 chars (T-03-02-04)
//   - Description and Color inserted as pgtype.Text{Valid: s != ""} (T-03-02-03)
//   - On validation error: 422 + fragment (HTMX) or 422 + full page (non-HTMX)
func TablosCreateHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		_, user, _ := auth.Authed(ctx)

		// 1. Read form values — always r.PostFormValue, never r.Body (Pitfall 2).
		title := strings.TrimSpace(r.PostFormValue("title"))
		description := r.PostFormValue("description")
		color := strings.TrimSpace(r.PostFormValue("color"))

		var errs templates.TabloCreateErrors

		// 2. Validate title (UI-SPEC Validation copy).
		if title == "" {
			errs.Title = "Title is required."
		} else if len(title) > 255 {
			errs.Title = "Title must be 255 characters or fewer."
		}

		// Validate color: accept only hex #RGB or #RRGGBB format (WR-04).
		if color != "" && !isValidCSSColor(color) {
			errs.Color = "Color must be a valid hex color (e.g. #6366f1)."
		}

		if errs.Title != "" || errs.Color != "" {
			renderTabloCreateError(w, r, templates.TabloCreateForm{
				Title:       title,
				Description: description,
				Color:       color,
			}, errs, http.StatusUnprocessableEntity, deps)
			return
		}

		// 3. Build insert params with nullable pgtype.Text for description and color.
		params := sqlc.InsertTabloParams{
			UserID:      user.ID,
			Title:       title,
			Description: pgtype.Text{String: description, Valid: description != ""},
			Color:       pgtype.Text{String: color, Valid: color != ""},
		}

		tablo, err := deps.Queries.InsertTablo(ctx, params)
		if err != nil {
			slog.Default().Error("tablos create: insert failed", "user_id", user.ID, "err", err)
			errs.General = "Something went wrong. Please try again."
			renderTabloCreateError(w, r, templates.TabloCreateForm{
				Title:       title,
				Description: description,
				Color:       color,
			}, errs, http.StatusInternalServerError, deps)
			return
		}

		// 4. Success response.
		// HTMX: dual-target swap — prepend card to #tablos-list + OOB clear form slot.
		// Non-HTMX: 303 redirect to / (Pitfall 9 — NOT 302).
		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			w.Header().Set("HX-Retarget", "#tablos-list")
			w.Header().Set("HX-Reswap", "afterbegin")
			_ = templates.TabloCardWithOOBFormClear(tablo, csrf.Token(r)).Render(ctx, w)
			return
		}
		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

// loadOwnedTablo is a shared preamble for all /tablos/{id}* handlers.
// It:
//  1. Parses the {id} URL param as a UUID → 404 on parse failure (T-03-03-03).
//  2. Fetches the tablo by ID → 404 on pgx.ErrNoRows, 500 on other errors.
//  3. Checks ownership → 404 (not 403) when tablo.UserID != user.ID (T-03-03-01, D-04).
//
// Returns (tablo, user, true) on success. On failure the helper writes the
// appropriate HTTP response and returns (zero, nil, false); callers must return
// immediately when ok is false.
func loadOwnedTablo(w http.ResponseWriter, r *http.Request, deps TablosDeps) (sqlc.Tablo, *auth.User, bool) {
	_, user, _ := auth.Authed(r.Context())

	// Step 1: parse the URL parameter.
	tabloID, err := uuid.Parse(chi.URLParam(r, "id"))
	if err != nil {
		http.NotFound(w, r)
		return sqlc.Tablo{}, nil, false
	}

	// Step 2: fetch from DB — user_id filter pushes ownership into the query (WR-01).
	tablo, err := deps.Queries.GetTabloByID(r.Context(), sqlc.GetTabloByIDParams{ID: tabloID, UserID: user.ID})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.NotFound(w, r)
			return sqlc.Tablo{}, nil, false
		}
		slog.Default().Error("tablos: GetTabloByID failed", "id", tabloID, "err", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return sqlc.Tablo{}, nil, false
	}

	return tablo, user, true
}

// TabloDetailHandler handles GET /tablos/{id}.
// Renders the tablo detail page for the authenticated owner; 404 for non-owner
// and invalid UUIDs (TABLO-03, T-03-03-01, T-03-03-03).
// Also fetches the tablo's tasks for the kanban board (Plan 02).
func TabloDetailHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, user, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		// Fetch tasks for the kanban board. On error, log and use empty slice —
		// the tablo itself is valid so we still render the page (Plan 02, T-04-07).
		tasks, err := deps.Queries.ListTasksByTablo(r.Context(), tablo.ID)
		if err != nil {
			slog.Default().Error("tablos detail: ListTasksByTablo failed", "tablo_id", tablo.ID, "err", err)
			tasks = []sqlc.Task{}
		}
		if tasks == nil {
			tasks = []sqlc.Task{}
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(r.Context(), w)
	}
}

// TabloEditTitleHandler handles GET /tablos/{id}/edit-title.
// Returns the title edit fragment for HTMX outerHTML swap.
func TabloEditTitleHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloTitleEditFragment(tablo, templates.TabloUpdateErrors{}, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TabloShowTitleHandler handles GET /tablos/{id}/show-title.
// Returns the title display fragment — used by the "Discard changes" path.
func TabloShowTitleHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloTitleDisplay(tablo, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TabloEditDescHandler handles GET /tablos/{id}/edit-desc.
// Returns the description edit fragment for HTMX outerHTML swap.
func TabloEditDescHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloDescEditFragment(tablo, templates.TabloUpdateErrors{}, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TabloShowDescHandler handles GET /tablos/{id}/show-desc.
// Returns the description display fragment — used by the "Discard changes" path.
func TabloShowDescHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloDescDisplay(tablo, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TabloUpdateHandler handles POST /tablos/{id}.
// Validates, updates the DB row (title + description), and returns the
// matching display fragment based on the hidden _zone field.
//
// Security invariants:
//   - Form values read via r.PostFormValue only (gorilla/csrf body — Pitfall 2)
//   - Title validated non-empty and <= 255 chars (T-03-03-05)
//   - Description written as pgtype.Text{Valid: s != ""} (T-03-03-06)
//   - HTMX path: 200 + display fragment; non-HTMX path: 303 to /tablos/{id}
func TabloUpdateHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Capture user from loadOwnedTablo for non-HTMX error path (CR-02).
		tablo, user, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		ctx := r.Context()

		// Read form values — always r.PostFormValue (Pitfall 2).
		title := strings.TrimSpace(r.PostFormValue("title"))
		description := r.PostFormValue("description")
		zone := r.PostFormValue("_zone") // "title" or "desc"

		var errs templates.TabloUpdateErrors

		// Validate title.
		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.TabloTitleEditFragment(tablo, errs, csrf.Token(r)).Render(ctx, w)
				return
			}
			// Non-HTMX: render full detail page with errors surfaced using the
			// authenticated user (not nil) to avoid broken layout (CR-02).
			// Fetch tasks for the kanban board; use empty slice on error.
			tasks, tasksErr := deps.Queries.ListTasksByTablo(ctx, tablo.ID)
			if tasksErr != nil {
				slog.Default().Error("tablos update: ListTasksByTablo failed", "tablo_id", tablo.ID, "err", tasksErr)
				tasks = []sqlc.Task{}
			}
			if tasks == nil {
				tasks = []sqlc.Task{}
			}
			_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(ctx, w)
			return
		}

		// Update the DB row — pass tablo.Color to preserve it across title/desc edits (CR-01).
		updated, err := deps.Queries.UpdateTablo(ctx, sqlc.UpdateTabloParams{
			ID:          tablo.ID,
			Title:       title,
			Description: pgtype.Text{String: description, Valid: description != ""},
			Color:       tablo.Color,
		})
		if err != nil {
			slog.Default().Error("tablos update: query failed", "id", tablo.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// Success: return appropriate display fragment or redirect.
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		if r.Header.Get("HX-Request") == "true" {
			// Render the display fragment matching the zone that was edited.
			if zone == "desc" {
				_ = templates.TabloDescDisplay(updated, csrf.Token(r)).Render(ctx, w)
				return
			}
			// Default to title display.
			_ = templates.TabloTitleDisplay(updated, csrf.Token(r)).Render(ctx, w)
			return
		}

		// Non-HTMX: 303 redirect to detail page so GET renders fresh state.
		http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
	}
}

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

// TabloDeleteCancelHandler handles GET /tablos/{id}/delete-cancel.
// Returns the delete button fragment, restoring the original button after
// the user cancels the confirmation dialog.
func TabloDeleteCancelHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.TabloDeleteButtonFragment(tablo, csrf.Token(r)).Render(r.Context(), w)
	}
}

// TabloDeleteHandler handles POST /tablos/{id}/delete.
// Hard-deletes the tablo and redirects to /:
//   - HTMX: 200 + HX-Redirect: / header (HTMX client-side navigation)
//   - Non-HTMX: 303 to / (Pitfall 9 — POST/Redirect/GET, NOT 302)
//
// Security: loadOwnedTablo enforces auth + ownership; CSRF middleware validates
// the _csrf field before this handler runs (T-03-03-09).
func TabloDeleteHandler(deps TablosDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, deps)
		if !ok {
			return
		}

		// DeleteTablo includes user_id in the WHERE clause for defense-in-depth (WR-02).
		if err := deps.Queries.DeleteTablo(r.Context(), sqlc.DeleteTabloParams{ID: tablo.ID, UserID: tablo.UserID}); err != nil {
			slog.Default().Error("tablos delete: query failed", "id", tablo.ID, "err", err)
			// On HTMX request, render a meaningful fragment instead of plain-text 500
			// to avoid leaving the delete zone in a broken DOM state (CR-03).
			if r.Header.Get("HX-Request") == "true" {
				w.Header().Set("Content-Type", "text/html; charset=utf-8")
				w.WriteHeader(http.StatusInternalServerError)
				_ = templates.TabloDeleteConfirmFragment(tablo, csrf.Token(r)).Render(r.Context(), w)
				return
			}
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("HX-Redirect", "/")
			w.WriteHeader(http.StatusOK)
			return
		}
		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

// renderTabloCreateError writes a tablo create validation-error response.
// For HTMX requests it renders only the form fragment; for plain requests it
// renders the full dashboard (mirrors the auth handler pattern).
func renderTabloCreateError(w http.ResponseWriter, r *http.Request, form templates.TabloCreateForm, errs templates.TabloCreateErrors, status int, deps TablosDeps) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	if r.Header.Get("HX-Request") == "true" {
		_ = templates.TabloCreateFormFragment(form, errs, csrf.Token(r)).Render(r.Context(), w)
		return
	}
	// Non-HTMX: render full dashboard with errs embedded in the form.
	// Fetch the user's tablos so the list is still accurate on re-render.
	_, user, _ := auth.Authed(r.Context())
	tabloRows, fetchErr := deps.Queries.ListTablosByUserWithDiscussionUnread(r.Context(), user.ID)
	if fetchErr != nil {
		slog.Default().Error("renderTabloCreateError: list fetch failed", "user_id", user.ID, "err", fetchErr)
		tabloRows = []sqlc.ListTablosByUserWithDiscussionUnreadRow{}
	}
	// Render full page — form fragment is not embedded in the full page by default;
	// for the non-HTMX error case we redirect so the user sees their list intact
	// and can try again (simpler than threading form state through the full page).
	_ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w)
}
