package web

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

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

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

type EventsDeps struct {
	Queries *sqlc.Queries
}

func parseCalendarMonth(raw string, now time.Time) time.Time {
	raw = strings.TrimSpace(raw)
	if raw != "" {
		if parsed, err := time.Parse("2006-01", raw); err == nil {
			return time.Date(parsed.Year(), parsed.Month(), 1, 0, 0, 0, 0, time.Local)
		}
	}
	return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local)
}

func pgDateFromTime(t time.Time) pgtype.Date {
	return pgtype.Date{Time: time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local), Valid: true}
}

func parseEventDate(raw string) (pgtype.Date, bool) {
	parsed, err := time.Parse("2006-01-02", strings.TrimSpace(raw))
	if err != nil {
		return pgtype.Date{}, false
	}
	return pgDateFromTime(parsed), true
}

func parseEventTime(raw string) (pgtype.Time, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return pgtype.Time{}, false
	}
	parsed, err := time.Parse("15:04", raw)
	if err != nil {
		return pgtype.Time{}, false
	}
	micros := int64(parsed.Hour())*int64(time.Hour/time.Microsecond) + int64(parsed.Minute())*int64(time.Minute/time.Microsecond)
	return pgtype.Time{Microseconds: micros, Valid: true}, true
}

func buildEventsCalendar(month time.Time, events []sqlc.Event) templates.EventsCalendar {
	monthStart := time.Date(month.Year(), month.Month(), 1, 0, 0, 0, 0, time.Local)
	monthEnd := monthStart.AddDate(0, 1, -1)
	leading := (int(monthStart.Weekday()) + 6) % 7
	gridStart := monthStart.AddDate(0, 0, -leading)
	days := make([]templates.EventCalendarDay, 0, 42)
	byDate := make(map[string][]sqlc.Event)
	for _, event := range events {
		key := templates.FormatEventDate(event.EventDate)
		byDate[key] = append(byDate[key], event)
	}
	for i := 0; i < 42; i++ {
		day := gridStart.AddDate(0, 0, i)
		key := day.Format("2006-01-02")
		dayEvents := byDate[key]
		more := 0
		if len(dayEvents) > 3 {
			more = len(dayEvents) - 3
		}
		days = append(days, templates.EventCalendarDay{
			Date:      key,
			DayNumber: day.Day(),
			InMonth:   !day.Before(monthStart) && !day.After(monthEnd),
			Events:    dayEvents,
			MoreCount: more,
		})
	}
	return templates.EventsCalendar{
		Month:          monthStart.Format("2006-01"),
		MonthLabel:     monthStart.Format("January 2006"),
		PrevMonth:      monthStart.AddDate(0, -1, 0).Format("2006-01"),
		PrevMonthLabel: monthStart.AddDate(0, -1, 0).Format("January 2006"),
		NextMonth:      monthStart.AddDate(0, 1, 0).Format("2006-01"),
		NextMonthLabel: monthStart.AddDate(0, 1, 0).Format("January 2006"),
		Days:           days,
		WeekdayLabs:    templates.DefaultWeekdayLabels(),
	}
}

func loadEventsCalendar(w http.ResponseWriter, r *http.Request, q *sqlc.Queries, tablo sqlc.Tablo) (templates.EventsCalendar, bool) {
	month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now())
	monthStart := time.Date(month.Year(), month.Month(), 1, 0, 0, 0, 0, time.Local)
	monthEnd := monthStart.AddDate(0, 1, -1)
	events, err := q.ListEventsByTabloRange(r.Context(), sqlc.ListEventsByTabloRangeParams{
		TabloID:     tablo.ID,
		EventDate:   pgDateFromTime(monthStart),
		EventDate_2: pgDateFromTime(monthEnd),
	})
	if err != nil {
		slog.Default().Error("events tab: ListEventsByTabloRange failed", "tablo_id", tablo.ID, "err", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return templates.EventsCalendar{}, false
	}
	return buildEventsCalendar(month, events), true
}

func loadOwnedEvent(w http.ResponseWriter, r *http.Request, deps EventsDeps) (sqlc.Tablo, sqlc.Event, bool) {
	tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
	if !ok {
		return sqlc.Tablo{}, sqlc.Event{}, false
	}
	eventID, err := uuid.Parse(chi.URLParam(r, "event_id"))
	if err != nil {
		http.NotFound(w, r)
		return sqlc.Tablo{}, sqlc.Event{}, false
	}
	event, err := deps.Queries.GetEventByID(r.Context(), sqlc.GetEventByIDParams{
		ID:      eventID,
		TabloID: tablo.ID,
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.NotFound(w, r)
			return sqlc.Tablo{}, sqlc.Event{}, false
		}
		slog.Default().Error("events: GetEventByID failed", "tablo_id", tablo.ID, "event_id", eventID, "err", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return sqlc.Tablo{}, sqlc.Event{}, false
	}
	return tablo, event, true
}

func renderEventsTab(w http.ResponseWriter, r *http.Request, q *sqlc.Queries, tablo sqlc.Tablo) {
	calendar, ok := loadEventsCalendar(w, r, q, tablo)
	if !ok {
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if r.Header.Get("HX-Request") == "true" {
		w.Header().Set("HX-Retarget", "#events-tab")
		w.Header().Set("HX-Reswap", "outerHTML")
	}
	_ = templates.EventsTabFragment(tablo, calendar, csrf.Token(r)).Render(r.Context(), w)
}

func TabloEventsTabHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, user, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		calendar, ok := loadEventsCalendar(w, r, deps.Queries, tablo)
		if !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if r.Header.Get("HX-Request") == "true" {
			_ = templates.EventsTabFragment(tablo, calendar, csrf.Token(r)).Render(r.Context(), w)
			return
		}
		eventsSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(r.Context(), user.ID)
		if sidebarErr != nil {
			slog.Default().Error("events: ListTablosByUser failed", "user_id", user.ID, "err", sidebarErr)
			eventsSidebarTablos = []sqlc.Tablo{}
		}
		if eventsSidebarTablos == nil {
			eventsSidebarTablos = []sqlc.Tablo{}
		}
		_ = templates.TabloDetailPage(user, csrf.Token(r), "", eventsSidebarTablos, tablo, nil, nil,
			templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, calendar,
			templates.DiscussionTabData{}, "events",
			tablo.Title,
			[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
		).Render(r.Context(), w)
	}
}

func EventNewFormHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now()).Format("2006-01")
		form := templates.EventCreateForm{EventDate: strings.TrimSpace(r.URL.Query().Get("date"))}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.EventCreateFormFragment(tablo.ID, form, templates.EventCreateErrors{}, csrf.Token(r), month).Render(r.Context(), w)
	}
}

func EventCancelNewHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if _, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries}); !ok {
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_, _ = w.Write([]byte(""))
	}
}

func EventCreateHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
		if !ok {
			return
		}
		form, params, errs, valid := parseEventCreateForm(r, tablo.ID)
		if !valid {
			month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now()).Format("2006-01")
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			if r.Header.Get("HX-Request") == "true" {
				w.Header().Set("HX-Retarget", "#event-form-slot")
				w.Header().Set("HX-Reswap", "innerHTML")
			}
			w.WriteHeader(http.StatusUnprocessableEntity)
			_ = templates.EventCreateFormFragment(tablo.ID, form, errs, csrf.Token(r), month).Render(r.Context(), w)
			return
		}
		if _, err := deps.Queries.CreateEvent(r.Context(), params); err != nil {
			slog.Default().Error("events create: CreateEvent failed", "tablo_id", tablo.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		renderEventsTab(w, r, deps.Queries, tablo)
	}
}

func EventEditFormHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, event, ok := loadOwnedEvent(w, r, deps)
		if !ok {
			return
		}
		month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now()).Format("2006-01")
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.EventEditFormFragment(tablo.ID, event, templates.EventFormFromEvent(event), templates.EventCreateErrors{}, csrf.Token(r), month).Render(r.Context(), w)
	}
}

func EventUpdateHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, event, ok := loadOwnedEvent(w, r, deps)
		if !ok {
			return
		}
		form, createParams, errs, valid := parseEventCreateForm(r, tablo.ID)
		month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now()).Format("2006-01")
		if !valid {
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			if r.Header.Get("HX-Request") == "true" {
				w.Header().Set("HX-Retarget", "#event-form-slot")
				w.Header().Set("HX-Reswap", "innerHTML")
			}
			w.WriteHeader(http.StatusUnprocessableEntity)
			_ = templates.EventEditFormFragment(tablo.ID, event, form, errs, csrf.Token(r), month).Render(r.Context(), w)
			return
		}
		if _, err := deps.Queries.UpdateEvent(r.Context(), sqlc.UpdateEventParams{
			ID:          event.ID,
			TabloID:     tablo.ID,
			Title:       createParams.Title,
			EventDate:   createParams.EventDate,
			StartTime:   createParams.StartTime,
			EndTime:     createParams.EndTime,
			Description: createParams.Description,
			Location:    createParams.Location,
		}); err != nil {
			slog.Default().Error("events update: UpdateEvent failed", "tablo_id", tablo.ID, "event_id", event.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		renderEventsTab(w, r, deps.Queries, tablo)
	}
}

func EventDeleteConfirmHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, event, ok := loadOwnedEvent(w, r, deps)
		if !ok {
			return
		}
		month := parseCalendarMonth(r.URL.Query().Get("month"), time.Now()).Format("2006-01")
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.EventDeleteConfirmFragment(tablo.ID, event, csrf.Token(r), month).Render(r.Context(), w)
	}
}

func EventDeleteHandler(deps EventsDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tablo, event, ok := loadOwnedEvent(w, r, deps)
		if !ok {
			return
		}
		if err := deps.Queries.DeleteEvent(r.Context(), sqlc.DeleteEventParams{
			ID:      event.ID,
			TabloID: tablo.ID,
		}); err != nil {
			slog.Default().Error("events delete: DeleteEvent failed", "tablo_id", tablo.ID, "event_id", event.ID, "err", err)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		renderEventsTab(w, r, deps.Queries, tablo)
	}
}

func parseEventCreateForm(r *http.Request, tabloID uuid.UUID) (templates.EventCreateForm, sqlc.CreateEventParams, templates.EventCreateErrors, bool) {
	title := strings.TrimSpace(r.PostFormValue("title"))
	eventDate := strings.TrimSpace(r.PostFormValue("event_date"))
	startTime := strings.TrimSpace(r.PostFormValue("start_time"))
	endTime := strings.TrimSpace(r.PostFormValue("end_time"))
	location := strings.TrimSpace(r.PostFormValue("location"))
	description := strings.TrimSpace(r.PostFormValue("description"))
	form := templates.EventCreateForm{
		Title:       title,
		EventDate:   eventDate,
		StartTime:   startTime,
		EndTime:     endTime,
		Location:    location,
		Description: description,
	}
	var errs templates.EventCreateErrors
	if title == "" {
		errs.Title = "Title is required."
	} else if len(title) > 255 {
		errs.Title = "Title must be 255 characters or fewer."
	}
	dateValue, ok := parseEventDate(eventDate)
	if !ok {
		errs.EventDate = "Date is required."
	}
	startValue, ok := parseEventTime(startTime)
	if !ok {
		errs.StartTime = "Start time is required."
	}
	var endValue pgtype.Time
	if endTime != "" {
		var endOK bool
		endValue, endOK = parseEventTime(endTime)
		if !endOK {
			errs.EndTime = "End time must use HH:MM."
		} else if startValue.Valid && endValue.Microseconds <= startValue.Microseconds {
			errs.EndTime = "End time must be after the start time."
		}
	}
	valid := errs.Title == "" && errs.EventDate == "" && errs.StartTime == "" && errs.EndTime == ""
	params := sqlc.CreateEventParams{
		TabloID:     tabloID,
		Title:       title,
		EventDate:   dateValue,
		StartTime:   startValue,
		EndTime:     endValue,
		Description: pgtype.Text{String: description, Valid: description != ""},
		Location:    pgtype.Text{String: location, Valid: location != ""},
	}
	return form, params, errs, valid
}
