// query_bind.go binds endpoint query parameters into typed request config.
// Layer: adapters/inbound/httpapi request binding.
package httpapi

import (
	"net/http"
	"strings"
	"time"

	"gitea.maximumdirect.net/ejr/feedapi/bind"
	apierrors "gitea.maximumdirect.net/ejr/feedapi/errors"
	"gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter"
	"gitea.maximumdirect.net/ejr/weatherapi/internal/app"
)

type queryRequest struct {
	Units presenter.Units
}

type precisionQueryRequest struct {
	Units     presenter.Units
	Precision int
	Timezone  *time.Location
}

type timezoneQueryRequest struct {
	Units    presenter.Units
	Timezone *time.Location
}

type outlookQueryRequest struct {
	Units    presenter.Units
	Timezone *time.Location
	Filter   app.OutlookFilter
}

func bindQuery(r *http.Request) (queryRequest, error) {
	normalizeCommonQueryValue(r, "units")
	normalizeCommonQueryValue(r, "format")

	common, err := bind.CommonQueryParams(r, bind.QueryPolicy{
		AllowUnits:    true,
		AllowFormat:   true,
		DefaultUnits:  string(presenter.UnitsMetric),
		RejectUnknown: true,
	})
	if err != nil {
		return queryRequest{}, err
	}

	units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units)))
	if units == "" {
		units = presenter.UnitsMetric
	}
	return queryRequest{Units: units}, nil
}

func bindPrecisionQuery(r *http.Request) (precisionQueryRequest, error) {
	return bindPrecisionQueryInternal(r, false)
}

func bindForecastPrecisionQuery(r *http.Request) (precisionQueryRequest, error) {
	return bindPrecisionQueryInternal(r, true)
}

func bindTimezoneQuery(r *http.Request) (timezoneQueryRequest, error) {
	normalizeCommonQueryValue(r, "units")
	normalizeCommonQueryValue(r, "format")

	common, err := bind.CommonQueryParams(r, bind.QueryPolicy{
		AllowUnits:    true,
		AllowFormat:   true,
		DefaultUnits:  string(presenter.UnitsMetric),
		RejectUnknown: true,
	}, "tz", "TZ")
	if err != nil {
		return timezoneQueryRequest{}, err
	}

	units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units)))
	if units == "" {
		units = presenter.UnitsMetric
	}

	tz, err := parseTimezoneQuery(r)
	if err != nil {
		return timezoneQueryRequest{}, err
	}

	return timezoneQueryRequest{
		Units:    units,
		Timezone: tz,
	}, nil
}

func bindPrecisionQueryInternal(r *http.Request, allowTimezone bool) (precisionQueryRequest, error) {
	normalizeCommonQueryValue(r, "units")
	normalizeCommonQueryValue(r, "format")
	normalizeCommonQueryValue(r, "precision")

	allowedExtra := []string{"precision"}
	if allowTimezone {
		allowedExtra = append(allowedExtra, "tz", "TZ")
	}

	common, err := bind.CommonQueryParams(r, bind.QueryPolicy{
		AllowUnits:    true,
		AllowFormat:   true,
		DefaultUnits:  string(presenter.UnitsMetric),
		RejectUnknown: true,
	}, allowedExtra...)
	if err != nil {
		return precisionQueryRequest{}, err
	}

	precision, err := bind.OptionalInt(r, "precision", 0)
	if err != nil {
		return precisionQueryRequest{}, err
	}
	if err := bind.MinInt(precision, 0, "precision"); err != nil {
		return precisionQueryRequest{}, err
	}
	if err := bind.MaxInt(precision, 2, "precision"); err != nil {
		return precisionQueryRequest{}, err
	}

	units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units)))
	if units == "" {
		units = presenter.UnitsMetric
	}

	var tz *time.Location
	if allowTimezone {
		tz, err = parseTimezoneQuery(r)
		if err != nil {
			return precisionQueryRequest{}, err
		}
	}

	return precisionQueryRequest{
		Units:     units,
		Precision: precision,
		Timezone:  tz,
	}, nil
}

func bindOutlookQuery(r *http.Request) (outlookQueryRequest, error) {
	return bindOutlookQueryInternal(r, true)
}

func bindOutlookLocationQuery(r *http.Request) (outlookQueryRequest, error) {
	return bindOutlookQueryInternal(r, false)
}

func bindOutlookQueryInternal(r *http.Request, allowContainsLocation bool) (outlookQueryRequest, error) {
	normalizeCommonQueryValue(r, "units")
	normalizeCommonQueryValue(r, "format")
	normalizeCommonQueryValue(r, "outlookType")

	allowedExtra := []string{"tz", "TZ", "day", "outlookType"}
	if allowContainsLocation {
		allowedExtra = append(allowedExtra, "containsLocation")
	}

	common, err := bind.CommonQueryParams(r, bind.QueryPolicy{
		AllowUnits:    true,
		AllowFormat:   true,
		DefaultUnits:  string(presenter.UnitsMetric),
		RejectUnknown: true,
	}, allowedExtra...)
	if err != nil {
		return outlookQueryRequest{}, err
	}

	units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units)))
	if units == "" {
		units = presenter.UnitsMetric
	}

	tz, err := parseTimezoneQuery(r)
	if err != nil {
		return outlookQueryRequest{}, err
	}

	filter, err := bindOutlookFilter(r, allowContainsLocation)
	if err != nil {
		return outlookQueryRequest{}, err
	}

	return outlookQueryRequest{
		Units:    units,
		Timezone: tz,
		Filter:   filter,
	}, nil
}

func bindOutlookFilter(r *http.Request, allowContainsLocation bool) (app.OutlookFilter, error) {
	var filter app.OutlookFilter

	if strings.TrimSpace(r.URL.Query().Get("day")) != "" {
		day, err := bind.OptionalInt(r, "day", 0)
		if err != nil {
			return app.OutlookFilter{}, err
		}
		if day < 1 || day > 3 {
			return app.OutlookFilter{}, apierrors.InvalidParameter("day must be one of [1, 2, 3]")
		}
		filter.Day = &day
	}

	outlookType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("outlookType")))
	if outlookType != "" {
		switch outlookType {
		case "categorical", "tornado", "hail", "wind":
			filter.OutlookType = outlookType
		default:
			return app.OutlookFilter{}, apierrors.InvalidParameter("outlookType must be one of [categorical, tornado, hail, wind]")
		}
	}

	if strings.TrimSpace(r.URL.Query().Get("containsLocation")) != "" {
		if !allowContainsLocation {
			return app.OutlookFilter{}, apierrors.InvalidParameter("containsLocation is not allowed on this endpoint")
		}
		containsLocation, err := bind.OptionalBool(r, "containsLocation", false)
		if err != nil {
			return app.OutlookFilter{}, err
		}
		filter.ContainsLocation = &containsLocation
	}

	return filter, nil
}
