package handlers

import (
	"fmt"

	"github.com/NdoleStudio/httpsms/pkg/repositories"
	"github.com/NdoleStudio/httpsms/pkg/requests"
	"github.com/NdoleStudio/httpsms/pkg/validators"
	"github.com/davecgh/go-spew/spew"

	"github.com/NdoleStudio/httpsms/pkg/services"
	"github.com/NdoleStudio/httpsms/pkg/telemetry"
	"github.com/gofiber/fiber/v3"
	"github.com/palantir/stacktrace"
)

// PhoneHandler handles phone http requests.
type PhoneHandler struct {
	handler
	logger    telemetry.Logger
	tracer    telemetry.Tracer
	service   *services.PhoneService
	validator *validators.PhoneHandlerValidator
}

// NewPhoneHandler creates a new PhoneHandler
func NewPhoneHandler(
	logger telemetry.Logger,
	tracer telemetry.Tracer,
	service *services.PhoneService,
	validator *validators.PhoneHandlerValidator,
) (h *PhoneHandler) {
	return &PhoneHandler{
		logger:    logger.WithService(fmt.Sprintf("%T", h)),
		tracer:    tracer,
		validator: validator,
		service:   service,
	}
}

// RegisterRoutes registers the routes for the PhoneHandler
func (h *PhoneHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) {
	h.register(router, fiber.MethodGet, "/v1/phones", middlewares, h.Index)
	h.register(router, fiber.MethodPut, "/v1/phones", middlewares, h.Upsert)
	h.register(router, fiber.MethodDelete, "/v1/phones/:phoneID", middlewares, h.Delete)
}

// RegisterPhoneAPIKeyRoutes registers the routes for the PhoneHandler
func (h *PhoneHandler) RegisterPhoneAPIKeyRoutes(router fiber.Router, middlewares ...fiber.Handler) {
	h.register(router, fiber.MethodPut, "/v1/phones/fcm-token", middlewares, h.UpsertFCMToken)
}

// Index returns the phones of a user
// @Summary      Get phones of a user
// @Description  Get list of phones which a user has registered on the http sms application
// @Security	 ApiKeyAuth
// @Tags         Phones
// @Accept       json
// @Produce      json
// @Param        skip		query  int  	false	"number of heartbeats to skip"		minimum(0)
// @Param        query		query  string  	false 	"filter phones containing query"
// @Param        limit		query  int  	false	"number of phones to return"		minimum(1)	maximum(20)
// @Success      200 		{object}	responses.PhonesResponse
// @Failure      400		{object}	responses.BadRequest
// @Failure 	 401    	{object}	responses.Unauthorized
// @Failure      422		{object}	responses.UnprocessableEntity
// @Failure      500		{object}	responses.InternalServerError
// @Router       /phones [get]
func (h *PhoneHandler) Index(c fiber.Ctx) error {
	ctx, span := h.tracer.StartFromFiberCtx(c)
	defer span.End()

	ctxLogger := h.tracer.CtxLogger(h.logger, span)

	var request requests.PhoneIndex
	if err := c.Bind().Query(&request); err != nil {
		msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
		ctxLogger.Warn(stacktrace.Propagate(err, msg))
		return h.responseBadRequest(c, err)
	}

	if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
		msg := fmt.Sprintf("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request)
		ctxLogger.Warn(stacktrace.NewError(msg))
		return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phones")
	}

	phones, err := h.service.Index(ctx, h.userFromContext(c), request.ToIndexParams())
	if err != nil {
		msg := fmt.Sprintf("cannot index phones with params [%+#v]", request)
		ctxLogger.Error(stacktrace.Propagate(err, msg))
		return h.responseInternalServerError(c)
	}

	return h.responseOK(c, fmt.Sprintf("fetched %d %s", len(*phones), h.pluralize("phone", len(*phones))), phones)
}

// Upsert a phone
// @Summary      Upsert Phone
// @Description  Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'
// @Security	 ApiKeyAuth
// @Tags         Phones
// @Accept       json
// @Produce      json
// @Param        payload   	body 		requests.PhoneUpsert  			true 	"Payload of new phone number."
// @Success      200 		{object}	responses.PhoneResponse
// @Failure      400		{object}	responses.BadRequest
// @Failure 	 401    	{object}	responses.Unauthorized
// @Failure      422		{object}	responses.UnprocessableEntity
// @Failure      500		{object}	responses.InternalServerError
// @Router       /phones [put]
func (h *PhoneHandler) Upsert(c fiber.Ctx) error {
	ctx, span := h.tracer.StartFromFiberCtx(c)
	defer span.End()

	ctxLogger := h.tracer.CtxLogger(h.logger, span)

	var request requests.PhoneUpsert
	if err := c.Bind().Body(&request); err != nil {
		msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
		ctxLogger.Warn(stacktrace.Propagate(err, msg))
		return h.responseBadRequest(c, err)
	}

	if errors := h.validator.ValidateUpsert(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 {
		msg := fmt.Sprintf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)
		ctxLogger.Warn(stacktrace.NewError(msg))
		return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
	}

	phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL(), c.Body()))
	if err != nil {
		msg := fmt.Sprintf("cannot update phones with params [%+#v]", request)
		ctxLogger.Error(stacktrace.Propagate(err, msg))
		return h.responseInternalServerError(c)
	}

	return h.responseOK(c, "phone updated successfully", phone)
}

// Delete a phone
// @Summary      Delete Phone
// @Description  Delete a phone that has been sored in the database
// @Security	 ApiKeyAuth
// @Tags         Phones
// @Accept       json
// @Produce      json
// @Param 		 phoneID 	path		string 							true 	"ID of the phone"	default(32343a19-da5e-4b1b-a767-3298a73703ca)
// @Success      204		{object}    responses.NoContent
// @Failure      400		{object}	responses.BadRequest
// @Failure 	 401    	{object}	responses.Unauthorized
// @Failure      422		{object}	responses.UnprocessableEntity
// @Failure      500		{object}	responses.InternalServerError
// @Router       /phones/{phoneID} [delete]
func (h *PhoneHandler) Delete(c fiber.Ctx) error {
	ctx, span := h.tracer.StartFromFiberCtx(c)
	defer span.End()

	ctxLogger := h.tracer.CtxLogger(h.logger, span)

	request := requests.PhoneDelete{PhoneID: c.Params("phoneID")}
	if errors := h.validator.ValidateDelete(ctx, request); len(errors) != 0 {
		msg := fmt.Sprintf("validation errors [%s], while deleting phone [%+#v]", spew.Sdump(errors), request)
		ctxLogger.Warn(stacktrace.NewError(msg))
		return h.responseUnprocessableEntity(c, errors, "validation errors while deleting phone")
	}

	err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c), request.PhoneIDUuid())
	if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
		return h.responseNotFound(c, fmt.Sprintf("cannot find phone with ID [%s]", request.PhoneID))
	}
	if err != nil {
		msg := fmt.Sprintf("cannot delete phones with params [%+#v]", request)
		ctxLogger.Error(stacktrace.Propagate(err, msg))
		return h.responseInternalServerError(c)
	}

	return h.responseOK(c, "phone deleted successfully", nil)
}

// UpsertFCMToken upserts the FCM token of a phone
// @Summary      Upserts the FCM token of a phone
// @Description  Updates the FCM token of a phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'
// @Security	 ApiKeyAuth
// @Tags         Phones
// @Accept       json
// @Produce      json
// @Param        payload   	body 		requests.PhoneFCMToken  			true 	"Payload of new FCM token."
// @Success      200 		{object}	responses.PhoneResponse
// @Failure      400		{object}	responses.BadRequest
// @Failure 	 401    	{object}	responses.Unauthorized
// @Failure      422		{object}	responses.UnprocessableEntity
// @Failure      500		{object}	responses.InternalServerError
// @Router       /phones/fcm-token [put]
func (h *PhoneHandler) UpsertFCMToken(c fiber.Ctx) error {
	ctx, span := h.tracer.StartFromFiberCtx(c)
	defer span.End()

	ctxLogger := h.tracer.CtxLogger(h.logger, span)

	var request requests.PhoneFCMToken
	if err := c.Bind().Body(&request); err != nil {
		msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
		ctxLogger.Warn(stacktrace.Propagate(err, msg))
		return h.responseBadRequest(c, err)
	}

	if errors := h.validator.ValidateFCMToken(ctx, request.Sanitize()); len(errors) != 0 {
		msg := fmt.Sprintf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)
		ctxLogger.Warn(stacktrace.NewError(msg))
		return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones")
	}

	phone, err := h.service.UpsertFCMToken(ctx, request.ToPhoneFCMTokenParams(h.userFromContext(c), c.OriginalURL()))
	if err != nil {
		msg := fmt.Sprintf("cannot delete phones with params [%+#v]", request)
		ctxLogger.Error(stacktrace.Propagate(err, msg))
		return h.responseInternalServerError(c)
	}

	return h.responseOK(c, "FCM token updated successfully", phone)
}
