/*
 * SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package season

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/jackc/pgtype"
	"github.com/jackc/pgx/v4"

	"dev.tandem.ws/tandem/camper/pkg/auth"
	"dev.tandem.ws/tandem/camper/pkg/database"
	"dev.tandem.ws/tandem/camper/pkg/form"
	httplib "dev.tandem.ws/tandem/camper/pkg/http"
	"dev.tandem.ws/tandem/camper/pkg/locale"
	"dev.tandem.ws/tandem/camper/pkg/template"
	"dev.tandem.ws/tandem/camper/pkg/uuid"
)

const UnsetColor = 0xd1d0df

type AdminHandler struct {
}

func NewAdminHandler() *AdminHandler {
	return &AdminHandler{}
}

func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var head string
		head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

		switch head {
		case "":
			switch r.Method {
			case http.MethodGet:
				serveSeasonIndex(w, r, user, company, conn)
			case http.MethodPost:
				addSeason(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
			}
		case "new":
			switch r.Method {
			case http.MethodGet:
				f := newSeasonForm(company)
				f.MustRender(w, r, user, company)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		case "order":
			switch r.Method {
			case http.MethodPost:
				orderSeasons(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodPost)
			}
		case "range":
			switch r.Method {
			case http.MethodGet:
				serveSeasonCalendar(w, r, user, company, conn)
			case http.MethodPut:
				updateSeasonCalendar(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
			}
		default:
			if !uuid.Valid(head) {
				http.NotFound(w, r)
				return
			}
			f := newSeasonForm(company)
			if err := f.FillFromDatabase(r.Context(), conn, head); err != nil {
				if database.ErrorIsNotFound(err) {
					http.NotFound(w, r)
					return
				}
				panic(err)
			}

			head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
			switch head {
			case "":
				switch r.Method {
				case http.MethodGet:
					f.MustRender(w, r, user, company)
				case http.MethodPut:
					editSeason(w, r, user, company, conn, f)
				default:
					httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
				}
			default:
				http.NotFound(w, r)
			}
		}
	}
}

func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	seasons, err := collectSeasonEntries(r.Context(), company, conn)
	if err != nil {
		panic(err)
	}
	calendar, err := CollectSeasonCalendar(r.Context(), company, conn, GetCalendarYear(r.URL.Query()), time.January, 12)
	if err != nil {
		panic(err)
	}
	calendar.Form = newCalendarForm(r.Context(), company, conn)
	page := &seasonIndex{
		Seasons:  seasons,
		Calendar: calendar,
	}
	page.MustRender(w, r, user, company)
}

func GetCalendarYear(query url.Values) int {
	yearStr := strings.TrimSpace(query.Get("year"))
	if yearStr != "" {
		if year, err := strconv.Atoi(yearStr); err == nil {
			return year
		}
	}
	return time.Now().Year()
}

func GetCalendarMonth(query url.Values) time.Month {
	monthStr := strings.TrimSpace(query.Get("month"))
	if monthStr != "" {
		if month, err := strconv.Atoi(monthStr); err == nil {
			return time.Month(month)
		}
	}
	return time.Now().Month()
}

func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) {
	rows, err := conn.Query(ctx, `
		select season.slug
		     , '/admin/seasons/' || season.slug
		     , season.name
		     , to_color(color)::text
		     , active
		from season
		where season.company_id = $1
		order by position, name`, company.ID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var seasons []*seasonEntry
	for rows.Next() {
		entry := &seasonEntry{}
		if err = rows.Scan(&entry.Slug, &entry.URL, &entry.Name, &entry.Color, &entry.Active); err != nil {
			return nil, err
		}
		seasons = append(seasons, entry)
	}

	return seasons, nil
}

type seasonEntry struct {
	ID     int
	Slug   string
	URL    string
	Name   string
	Color  string
	Active bool
}

var LongMonthNames = []string{
	locale.PgettextNoop("January", "month"),
	locale.PgettextNoop("February", "month"),
	locale.PgettextNoop("March", "month"),
	locale.PgettextNoop("April", "month"),
	locale.PgettextNoop("May", "month"),
	locale.PgettextNoop("June", "month"),
	locale.PgettextNoop("July", "month"),
	locale.PgettextNoop("August", "month"),
	locale.PgettextNoop("September", "month"),
	locale.PgettextNoop("October", "month"),
	locale.PgettextNoop("November", "month"),
	locale.PgettextNoop("December", "month"),
}

func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int, firstMonth time.Month, monthCount int) (*Calendar, error) {
	firstDay := time.Date(year, firstMonth, 1, 0, 0, 0, 0, time.UTC)
	lastDay := firstDay.AddDate(0, monthCount, 0).Add(-1 * time.Second)
	rows, err := conn.Query(ctx, `
		select t.day::date
		     , to_color(coalesce(color, $1)) as color
		from generate_series($2, $3, interval '1 day') as t(day)
		    left join season_calendar on season_range @> t.day::date
		    left join season on season.season_id = season_calendar.season_id and company_id = $4
	`, UnsetColor, firstDay.Format(database.ISODateTimeFormat), lastDay.Format(database.ISODateTimeFormat), company.ID)
	if err != nil {
		return nil, err
	}

	var month *Month
	var week Week
	calendar := &Calendar{
		Year: year,
	}
	weekday := int(time.Monday)
	for rows.Next() {
		day := &Day{}
		if err = rows.Scan(&day.Date, &day.Color); err != nil {
			return nil, err
		}
		dayMonth := day.Date.Month()
		if month == nil || month.Month != dayMonth {
			if month != nil {
				for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 {
					week = append(week, &Day{})
				}
				month.Weeks = append(month.Weeks, week)
				calendar.Months = append(calendar.Months, month)
			}
			month = &Month{
				Year:  day.Date.Year(),
				Month: dayMonth,
				Name:  LongMonthNames[dayMonth-1],
			}
			week = Week{}
			weekday = int(time.Monday)
			dayWeekday := int(day.Date.Weekday())
			for ; weekday != dayWeekday; weekday = (weekday + 1) % 7 {
				week = append(week, &Day{})
			}
		}
		week = append(week, day)
		weekday = (weekday + 1) % 7
		if weekday == int(time.Monday) {
			month.Weeks = append(month.Weeks, week)
			week = Week{}
		}
	}
	if month != nil {
		for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 {
			week = append(week, &Day{})
		}
		month.Weeks = append(month.Weeks, week)
		calendar.Months = append(calendar.Months, month)
	}

	return calendar, nil
}

type Calendar struct {
	Year   int
	Months []*Month
	Form   *calendarForm
}

type Month struct {
	Year  int
	Month time.Month
	Name  string
	Weeks []Week
}

type Week []*Day

type Day struct {
	Date  time.Time
	Color string
}

type seasonIndex struct {
	Seasons  []*seasonEntry
	Calendar *Calendar
}

func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdminFiles(w, r, user, company, page, "season/index.gohtml", "season/calendar.gohtml")
}

func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func(context.Context, *database.Tx) error) {
	if err := f.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := user.VerifyCSRFToken(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
		panic(err)
	} else if !ok {
		if !httplib.IsHTMxRequest(r) {
			w.WriteHeader(http.StatusUnprocessableEntity)
		}
		f.MustRender(w, r, user, company)
		return
	}
	tx := conn.MustBegin(r.Context())
	if err := act(r.Context(), tx); err == nil {
		tx.MustCommit(r.Context())
	} else {
		if rErr := tx.Rollback(r.Context()); rErr != nil {
			err = rErr
		}
		panic(err)
	}
	httplib.Redirect(w, r, "/admin/seasons", http.StatusSeeOther)
}

func addSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	f := newSeasonForm(company)
	processSeasonForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
		var err error
		if f.Slug, err = tx.AddSeason(ctx, company.ID, f.Name[f.DefaultLang].Val, f.Color.Val); err != nil {
			return err
		}
		return translateSeason(ctx, tx, company, f)
	})
}

func translateSeason(ctx context.Context, tx *database.Tx, company *auth.Company, f *seasonForm) error {
	for lang := range company.Locales {
		l := lang.String()
		if l == f.DefaultLang {
			continue
		}
		if err := tx.TranslateSeason(ctx, f.Slug, lang, f.Name[l].Val); err != nil {
			return err
		}
	}
	return nil
}

func editSeason(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm) {
	processSeasonForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
		if err := tx.EditSeason(ctx, f.Slug, f.Name[f.DefaultLang].Val, f.Color.Val, f.Active.Checked); err != nil {
			return err
		}
		return translateSeason(ctx, tx, company, f)
	})
}

type seasonForm struct {
	DefaultLang string
	Slug        string
	Active      *form.Checkbox
	Name        form.I18nInput
	Color       *form.Input
}

func newSeasonForm(company *auth.Company) *seasonForm {
	return &seasonForm{
		DefaultLang: company.DefaultLanguage.String(),
		Active: &form.Checkbox{
			Name:    "active",
			Checked: true,
		},
		Name: form.NewI18nInput(company.Locales, "label"),
		Color: &form.Input{
			Name: "season",
		},
	}
}

func (f *seasonForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
	f.Slug = slug
	var name database.RecordArray
	row := conn.QueryRow(ctx, `
		select season.name
		     , to_color(color)::text
		     , active
		     , array_agg((lang_tag, i18n.name))
		from season
		left join season_i18n as i18n using (season_id)
		where slug = $1
		group by season.name
		       , to_color(color)::text
		       , active
	`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, slug)
	if err := row.Scan(&f.Name[f.DefaultLang].Val, &f.Color.Val, &f.Active.Checked, &name); err != nil {
		return err
	}
	if err := f.Name.FillArray(name); err != nil {
		return err
	}
	return nil
}

func (f *seasonForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	f.Active.FillValue(r)
	f.Name.FillValue(r)
	f.Color.FillValue(r)
	return nil
}

func (f *seasonForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
	v := form.NewValidator(l)
	v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty."))
	if v.CheckRequired(f.Color, l.GettextNoop("Color can not be empty.")) {
		if _, err := v.CheckValidColor(ctx, conn, f.Color, l.Gettext("This color is not valid. It must be like #123abc.")); err != nil {
			return false, err
		}
	}
	return v.AllOK, nil
}

func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
}

func orderSeasons(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := user.VerifyCSRFToken(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	slugs := r.PostForm["slug"]
	if len(slugs) > 0 {
		for _, slug := range slugs {
			if !uuid.Valid(slug) {
				w.WriteHeader(http.StatusUnprocessableEntity)
				return
			}
		}
		if err := conn.OrderSeasons(r.Context(), slugs); err != nil {
			panic(err)
		}
	}
	serveSeasonIndex(w, r, user, company, conn)
}

func serveSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	f := newCalendarForm(r.Context(), company, conn)
	f.MustRender(w, r, user, company, conn, GetCalendarYear(r.URL.Query()))
}

func updateSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	f := newCalendarForm(r.Context(), company, conn)
	if ok, err := form.Handle(f, w, r, user); err != nil {
		return
	} else if ok {
		var seasonRange pgtype.Daterange
		if err = seasonRange.Set(fmt.Sprintf("[%s,%s]", f.StartDate.Val, f.EndDate.Val)); err != nil {
			panic(err)
		}
		if f.SeasonID.Val == "" {
			// Nothing to do
		} else if f.SeasonID.Val == "0" {
			conn.MustExec(r.Context(), "select unset_season_range($1)", seasonRange)
		} else {
			conn.MustExec(r.Context(), "select set_season_range($1, $2)", f.SeasonID, seasonRange)
		}
		f.StartDate.Val = ""
		f.EndDate.Val = ""
	}
	f.MustRender(w, r, user, company, conn, GetCalendarYear(r.Form))
}

type calendarForm struct {
	Seasons   []*seasonEntry
	StartDate *form.Input
	EndDate   *form.Input
	SeasonID  *form.Input
}

func newCalendarForm(ctx context.Context, company *auth.Company, conn *database.Conn) *calendarForm {
	return &calendarForm{
		Seasons: mustCollectCalendarSeasons(ctx, company, conn),
		StartDate: &form.Input{
			Name: "start_date",
		},
		EndDate: &form.Input{
			Name: "end_date",
		},
		SeasonID: &form.Input{
			Name: "season_id",
		},
	}
}

func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn *database.Conn) []*seasonEntry {
	rows, err := conn.Query(ctx, `
		select 0 as season_id
		     , $1 as name
		     , to_color($2)::text
		     , true
		     , 0 as position
		union all
		select season_id
		     , name
		     , to_color(color)::text
		     , active
		     , position
		from season
		where company_id = $3
		  and active
		order by position, name`, locale.PgettextNoop("Unset", "action"), UnsetColor, company.ID)
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	var seasons []*seasonEntry
	for rows.Next() {
		entry := &seasonEntry{}
		var sort int
		if err = rows.Scan(&entry.ID, &entry.Name, &entry.Color, &entry.Active, &sort); err != nil {
			panic(err)
		}
		seasons = append(seasons, entry)
	}

	return seasons
}

func (f *calendarForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	f.StartDate.FillValue(r)
	f.EndDate.FillValue(r)
	f.SeasonID.FillValue(r)
	return nil
}

func (f *calendarForm) Valid(l *locale.Locale) bool {
	v := form.NewValidator(l)
	if v.CheckRequired(f.StartDate, l.GettextNoop("Start date can not be empty.")) {
		v.CheckValidDate(f.StartDate, l.GettextNoop("Start date must be a valid date."))
	}
	if v.CheckRequired(f.EndDate, l.GettextNoop("End date can not be empty.")) {
		v.CheckValidDate(f.EndDate, l.GettextNoop("End date must be a valid date."))
	}
	return v.AllOK
}

func (f *calendarForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, year int) {
	calendar, err := CollectSeasonCalendar(r.Context(), company, conn, year, time.January, 12)
	if err != nil {
		panic(err)
	}
	calendar.Form = f
	template.MustRenderAdminNoLayout(w, r, user, company, "season/calendar.gohtml", calendar)
}
