package ocss

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/gorilla/schema"
	"github.com/rs/zerolog/log"
	"golang.org/x/crypto/bcrypt"
)

const OperatorContextKey = "operator"

type Handler interface {
	// IV
	GetDriverJourneys(ctx context.Context, departureLat float64, departureLng float64, arrivalLat float64, arrivalLng float64, departureDate time.Time, timeDelta *time.Duration, departureRadius *float64, arrivalRadius *float64, count *int64) ([]DriverJourney, error)
	GetPassengerJourneys(ctx context.Context, departureLat float64, departureLng float64, arrivalLat float64, arrivalLng float64, departureDate time.Time, timeDelta *time.Duration, departureRadius *float64, arrivalRadius *float64, count *int64) ([]PassengerJourney, error)
	GetDriverRegularTrips(ctx context.Context, departureLat float64, departureLng float64, arrivalLat float64, arrivalLng float64, departureTimeOfDay string, departureWeekDays *[]string, timeDelta *time.Duration, departureRadius *float64, arrivalRadius *float64, minDepartureDate *time.Time, maxDepartureDate *time.Time, count *int64) ([]DriverTrip, error)
	GetPassengerRegularTrips(ctx context.Context, departureLat float64, departureLng float64, arrivalLat float64, arrivalLng float64, departureTimeOfDay string, departureWeekDays *[]string, timeDelta *time.Duration, departureRadius *float64, arrivalRadius *float64, minDepartureDate *time.Time, maxDepartureDate *time.Time, count *int64) ([]PassengerTrip, error)

	//Booking by API
	PostBookings(ctx context.Context, booking Booking) (*Booking, error)
	PatchBookings(ctx context.Context, bookingId string, status BookingStatus, message *string) error
	GetBookings(ctx context.Context, bookingId string) (*Booking, error)

	// Webhooks
	PostBookingEvents(ctx context.Context, event CarpoolBookingEvent) error

	// Status
	Status() error
}

type AuthorizedOperator struct {
	Operator string
	ApiKey   string // encoded using bcrypt
}

type GetDriverJourneysRequest struct {
	DepartureLat    float64  `schema:"departureLat,required"`
	DepartureLng    float64  `schema:"departureLng,required"`
	ArrivalLat      float64  `schema:"arrivalLat,required"`
	ArrivalLng      float64  `schema:"arrivalLng,required"`
	DepartureDate   int64    `schema:"departureDate,required"`
	TimeDelta       *int64   `schema:"timeDelta"`
	DepartureRadius *float64 `schema:"departureRadius"`
	ArrivalRadius   *float64 `schema:"arrivalRadius"`
	Count           *int64   `schema:"count"`
}

type GetPassengerJourneysRequest struct {
	DepartureLat    float64  `schema:"departureLat,required"`
	DepartureLng    float64  `schema:"departureLng,required"`
	ArrivalLat      float64  `schema:"arrivalLat,required"`
	ArrivalLng      float64  `schema:"arrivalLng,required"`
	DepartureDate   int64    `schema:"departureDate,required"`
	TimeDelta       *int64   `schema:"timeDelta"`
	DepartureRadius *float64 `schema:"departureRadius"`
	ArrivalRadius   *float64 `schema:"arrivalRadius"`
	Count           *int64   `schema:"count"`
}

type GetDriverRegularTripsRequest struct {
	DepartureLat       float64   `schema:"departureLat,required"`
	DepartureLng       float64   `schema:"departureLng,required"`
	ArrivalLat         float64   `schema:"arrivalLat,required"`
	ArrivalLng         float64   `schema:"arrivalLng,required"`
	DepartureTimeOfDay string    `schema:"departureTimeOfDay,required"`
	DepartureWeekDays  *[]string `schema:"departureWeekdays"`
	TimeDelta          *int64    `schema:"timeDelta"`
	DepartureRadius    *float64  `schema:"departureRadius"`
	ArrivalRadius      *float64  `schema:"arrivalRadius"`
	MinDepartureDate   *int64    `schema:"minDepartureDate"`
	MaxDepartureDate   *int64    `schema:"maxDepartureDate"`
	Count              *int64    `schema:"count"`
}

type GetPassengerRegularTripsRequest struct {
	DepartureLat       float64   `schema:"departureLat,required"`
	DepartureLng       float64   `schema:"departureLng,required"`
	ArrivalLat         float64   `schema:"arrivalLat,required"`
	ArrivalLng         float64   `schema:"arrivalLng,required"`
	DepartureTimeOfDay string    `schema:"departureTimeOfDay,required"`
	DepartureWeekDays  *[]string `schema:"departureWeekdays"`
	TimeDelta          *int64    `schema:"timeDelta"`
	DepartureRadius    *float64  `schema:"departureRadius"`
	ArrivalRadius      *float64  `schema:"arrivalRadius"`
	MinDepartureDate   *int64    `schema:"minDepartureDate"`
	MaxDepartureDate   *int64    `schema:"maxDepartureDate"`
	Count              *int64    `schema:"count"`
}

type PatchBookingsRequest struct {
	BookingId string `json:"bookingId"`
	Status    string `json:"status"`
	Message   string `json:"message"`
}

type Server struct {
	Handler             Handler
	AuthorizedOperators []AuthorizedOperator
}

func NewServer(handler Handler) *Server {
	return &Server{
		Handler: handler,
	}
}

func (s *Server) FindApiKey(key string) (operator string, err error) {
	for _, o := range s.AuthorizedOperators {
		if e := bcrypt.CompareHashAndPassword([]byte(o.ApiKey), []byte(key)); e == nil {
			return o.Operator, nil
		}
	}

	return "", errors.New("operator not found")
}

func (s *Server) AddOperator(operator string, apiKey string) error {
	encryptedKey, err := bcrypt.GenerateFromPassword([]byte(apiKey), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	s.AuthorizedOperators = append(s.AuthorizedOperators, AuthorizedOperator{
		Operator: operator,
		ApiKey:   string(encryptedKey),
	})

	return nil
}

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	apiKey := r.Header.Get("X-Api-Key")
	operator, err := s.FindApiKey(apiKey)
	if err != nil {
		log.Error().Err(err).Msg("unauthorized")
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	ctx := context.WithValue(r.Context(), OperatorContextKey, operator)

	p := strings.Split(r.URL.Path, "/")[1:]
	n := len(p)
	switch {
	case r.Method == "GET" && n == 1 && p[0] == "driver_journeys":
		s.getDriverJourneys(w, r.WithContext(ctx))
		return
	case r.Method == "GET" && n == 1 && p[0] == "passenger_journeys":
		s.getPassengerJourneys(w, r.WithContext(ctx))
		return
	case r.Method == "GET" && n == 1 && p[0] == "driver_regular_trips":
		s.getDriverRegularTrips(w, r.WithContext(ctx))
		return
	case r.Method == "GET" && n == 1 && p[0] == "passenger_regular_trips":
		s.getPassengerRegularTrips(w, r.WithContext(ctx))
		return
	case r.Method == "POST" && n == 1 && p[0] == "bookings":
		s.postBookings(w, r.WithContext(ctx))
		return
	case r.Method == "PATCH" && n == 2 && p[0] == "bookings":
		s.patchBookings(w, p[1], r.WithContext(ctx))
		return
	case r.Method == "GET" && n == 2 && p[0] == "bookings":
		s.getBookings(w, p[1], r.WithContext(ctx))
		return
	// case r.Method == "GET" && n == 1 && p[0] == "status":
	// 	s.getStatus(w, r.WithContext(ctx))
	// 	return
	default:
		w.WriteHeader(http.StatusNotFound)
	}
}

func (s *Server) getDriverJourneys(w http.ResponseWriter, r *http.Request) {
	var request GetDriverJourneysRequest

	var decoder = schema.NewDecoder()

	if err := decoder.Decode(&request, r.URL.Query()); err != nil {
		log.Error().Err(err).Msg("could not parse the request")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	departureDate := time.Unix(request.DepartureDate, 0)
	var timeDelta *time.Duration
	timeDelta = nil
	if request.TimeDelta != nil {
		duration := time.Duration(*request.TimeDelta)
		timeDelta = &duration
	}

	driverJourneys, err := s.Handler.GetDriverJourneys(
		r.Context(),
		request.DepartureLat,
		request.DepartureLng,
		request.ArrivalLat,
		request.ArrivalLng,
		departureDate,
		timeDelta,
		request.DepartureRadius,
		request.ArrivalRadius,
		request.Count)

	if err != nil {
		log.Error().Err(err).Msg("GetDriverJourneys failed")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, driverJourneys, http.StatusOK)
}

func (s *Server) getPassengerJourneys(w http.ResponseWriter, r *http.Request) {
	var request GetPassengerJourneysRequest

	var decoder = schema.NewDecoder()

	if err := decoder.Decode(&request, r.URL.Query()); err != nil {
		log.Error().Err(err).Msg("could not parse the request")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	departureDate := time.Unix(request.DepartureDate, 0)
	var timeDelta *time.Duration
	timeDelta = nil
	if request.TimeDelta != nil {
		duration := time.Duration(*request.TimeDelta)
		timeDelta = &duration
	}

	passengerJourneys, err := s.Handler.GetPassengerJourneys(
		r.Context(),
		request.DepartureLat,
		request.DepartureLng,
		request.ArrivalLat,
		request.ArrivalLng,
		departureDate,
		timeDelta,
		request.DepartureRadius,
		request.ArrivalRadius,
		request.Count)

	if err != nil {
		log.Error().Err(err).Msg("GetPassengerJourneys failed")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, passengerJourneys, http.StatusOK)
}

func (s *Server) getDriverRegularTrips(w http.ResponseWriter, r *http.Request) {
	var request GetDriverRegularTripsRequest

	var decoder = schema.NewDecoder()

	if err := decoder.Decode(&request, r.URL.Query()); err != nil {
		log.Error().Err(err).Msg("could not parse the request")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	var minDepartureDate *time.Time
	if request.MinDepartureDate != nil {
		d := time.Unix(*request.MinDepartureDate, 0)
		minDepartureDate = &d
	}

	var maxDepartureDate *time.Time
	if request.MinDepartureDate != nil {
		d := time.Unix(*request.MinDepartureDate, 0)
		maxDepartureDate = &d
	}

	var timeDelta *time.Duration
	timeDelta = nil
	if request.TimeDelta != nil {
		duration := time.Duration(*request.TimeDelta)
		timeDelta = &duration
	}

	driverJourneys, err := s.Handler.GetDriverRegularTrips(
		r.Context(),
		request.DepartureLat,
		request.DepartureLng,
		request.ArrivalLat,
		request.ArrivalLng,
		request.DepartureTimeOfDay,
		request.DepartureWeekDays,
		timeDelta,
		request.DepartureRadius,
		request.ArrivalRadius,
		minDepartureDate,
		maxDepartureDate,
		request.Count)

	if err != nil {
		log.Error().Err(err).Msg("GetDriverRegularTrips failed")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, driverJourneys, http.StatusOK)
}

func (s *Server) getPassengerRegularTrips(w http.ResponseWriter, r *http.Request) {
	var request GetPassengerRegularTripsRequest

	var decoder = schema.NewDecoder()

	if err := decoder.Decode(&request, r.URL.Query()); err != nil {
		log.Error().Err(err).Msg("could not parse the request")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	var minDepartureDate *time.Time
	if request.MinDepartureDate != nil {
		d := time.Unix(*request.MinDepartureDate, 0)
		minDepartureDate = &d
	}

	var maxDepartureDate *time.Time
	if request.MinDepartureDate != nil {
		d := time.Unix(*request.MinDepartureDate, 0)
		maxDepartureDate = &d
	}

	var timeDelta *time.Duration
	timeDelta = nil
	if request.TimeDelta != nil {
		duration := time.Duration(*request.TimeDelta)
		timeDelta = &duration
	}

	passengerTrips, err := s.Handler.GetPassengerRegularTrips(
		r.Context(),
		request.DepartureLat,
		request.DepartureLng,
		request.ArrivalLat,
		request.ArrivalLng,
		request.DepartureTimeOfDay,
		request.DepartureWeekDays,
		timeDelta,
		request.DepartureRadius,
		request.ArrivalRadius,
		minDepartureDate,
		maxDepartureDate,
		request.Count)

	if err != nil {
		log.Error().Err(err).Msg("GetPassengerRegularTrips failed")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, passengerTrips, http.StatusOK)
}

func (s *Server) postBookings(w http.ResponseWriter, r *http.Request) {
	var request Booking

	var decoder = json.NewDecoder(r.Body)

	if err := decoder.Decode(&request); err != nil {
		log.Error().Err(err).Msg("PostBookings - could not parse the request ")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	response, err := s.Handler.PostBookings(
		r.Context(),
		request,
	)
	if err != nil {
		log.Error().Err(err).Msg("error in PostBookings")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, response, http.StatusOK)
}

func (s *Server) patchBookings(w http.ResponseWriter, bookingId string, r *http.Request) {
	var request PatchBookingsRequest

	var decoder = json.NewDecoder(r.Body)
	if err := decoder.Decode(&request); err != nil {
		log.Error().Err(err).Msg("PatchBookings - could not parse the request ")
		badRequest(w, fmt.Errorf("could not parse the request : %s", err))
		return
	}

	err := s.Handler.PatchBookings(
		r.Context(),
		bookingId,
		BookingConfirmed,
		&request.Message,
	)

	if err != nil {
		log.Error().Err(err).Msg("error in PatchBookings")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, map[string]any{}, http.StatusOK)

}

func (s *Server) getBookings(w http.ResponseWriter, bookingId string, r *http.Request) {

	log.Debug().Str("booking id", bookingId).Msg("GetBooking request")

	booking, err := s.Handler.GetBookings(
		r.Context(),
		bookingId,
	)

	if err != nil {
		log.Error().Err(err).Msg("error in GetBookings")
		jsonError(w, err, http.StatusInternalServerError)
		return
	}

	jsonResponse(w, booking, http.StatusOK)

}

func jsonResponse(w http.ResponseWriter, response any, statuscode int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statuscode)
	err := json.NewEncoder(w).Encode(response)
	if err != nil {
		log.Error().Err(err).Msg("json error encoding issue")
	}
}

func jsonError(w http.ResponseWriter, err error, statuscode int) {
	jsonResponse(w, map[string]any{"error": err.Error()}, statuscode)
}

func badRequest(w http.ResponseWriter, err error) {
	jsonError(w, err, http.StatusBadRequest)
}