package notifications

import (
	"strconv"
	"strings"
)

// The consultation handshake's own events.
//
// Two rules the whole flow relies on, both worth restating here because this
// is where they would be easiest to break:
//
//   - No token ever travels in a payload. These carry the consultation id and
//     the channel name; the app then fetches credentials from an
//     authenticated endpoint. FCM payloads are logged in transit and readable
//     by anything holding the device token.
//
//   - Every value in Data is a string. FCM rejects anything else, and the
//     failure is a silently undelivered notification rather than an error.

//////////////////////////////////////////////////////////////
// Customer
//////////////////////////////////////////////////////////////

// ConsultationRejected — the astrologer declined. Nothing was billed.
func (n *Notifier) ConsultationRejected(
	userID uint,
	astrologerID uint,
	medium string,
	consultationID uint,
	reason string,
) {

	if n == nil {
		return
	}

	name := n.repo.astrologerName(astrologerID)

	body := name + " could not take your " + mediumLabel(medium) + " right now. Nothing was charged."

	if reason != "" {
		body = name + " could not take your " + mediumLabel(medium) + " right now: " + reason
	}

	n.dispatch(userID, TypeRequestRejected, Payload{
		Title:          mediumTitle(medium) + " request declined",
		Body:           body,
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":            "CONSULTATION_REJECTED",
			"medium":          medium,
			"astrologer_id":   strconv.FormatUint(uint64(astrologerID), 10),
			"consultation_id": strconv.FormatUint(uint64(consultationID), 10),
			"end_reason":      "REJECTED",
			"reason":          reason,
			"screen":          "astrologer_profile",
		},
	}, AppCustomer)
}

// ConsultationMissed — nobody answered before the request rang out.
func (n *Notifier) ConsultationMissed(
	userID uint,
	astrologerID uint,
	medium string,
	consultationID uint,
) {

	if n == nil {
		return
	}

	name := n.repo.astrologerName(astrologerID)

	n.dispatch(userID, TypeRequestMissed, Payload{
		Title:          mediumTitle(medium) + " request expired",
		Body:           name + " did not respond in time. Nothing was charged.",
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":            "CONSULTATION_MISSED",
			"medium":          medium,
			"astrologer_id":   strconv.FormatUint(uint64(astrologerID), 10),
			"consultation_id": strconv.FormatUint(uint64(consultationID), 10),
			"end_reason":      "NO_ANSWER",
			"screen":          "astrologer_profile",
		},
	}, AppCustomer)
}

// ConsultationNotJoined — the astrologer accepted, but the customer never
// opened the session, so the astrologer was released.
//
// Sent to the customer rather than silently dropping it: from their side the
// request simply stopped, and being told it lapsed is what stops them assuming
// they were charged for a session they never had.
func (n *Notifier) ConsultationNotJoined(
	userID uint,
	astrologerID uint,
	medium string,
	consultationID uint,
) {

	if n == nil {
		return
	}

	name := n.repo.astrologerName(astrologerID)

	n.dispatch(userID, TypeRequestNotJoined, Payload{
		Title:          mediumTitle(medium) + " session expired",
		Body:           name + " accepted, but the session was not opened in time. Nothing was charged.",
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":            "CONSULTATION_NOT_JOINED",
			"medium":          medium,
			"astrologer_id":   strconv.FormatUint(uint64(astrologerID), 10),
			"consultation_id": strconv.FormatUint(uint64(consultationID), 10),
			"end_reason":      "NOT_JOINED",
			"screen":          "astrologer_profile",
		},
	}, AppCustomer)
}

// ConsultationCancelledByAstrologer — the astrologer took a different session
// while this one was still ringing.
//
// Telling the customer immediately is kinder than leaving them on a spinner
// that quietly times out a minute later.
func (n *Notifier) ConsultationCancelledByAstrologer(
	userID uint,
	astrologerID uint,
	medium string,
	consultationID uint,
) {

	if n == nil {
		return
	}

	name := n.repo.astrologerName(astrologerID)

	n.dispatch(userID, TypeRequestCancelled, Payload{
		Title:          mediumTitle(medium) + " request cancelled",
		Body:           name + " took another session. Nothing was charged — please try again shortly.",
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":            "CONSULTATION_CANCELLED",
			"medium":          medium,
			"astrologer_id":   strconv.FormatUint(uint64(astrologerID), 10),
			"consultation_id": strconv.FormatUint(uint64(consultationID), 10),
			"end_reason":      "CANCELLED",
			"screen":          "astrologer_profile",
		},
	}, AppCustomer)
}

// FreeChatEnding — the free minutes are nearly up.
//
// Deliberately not the low-balance wording: the customer has not run out of
// money, they have run out of the platform's gift, and the action offered is
// "carry on at the astrologer's rate" rather than "recharge".
func (n *Notifier) FreeChatEnding(
	userID uint,
	consultationID uint,
	remainingSeconds int,
	ratePerMinute float64,
) {

	if n == nil {
		return
	}

	n.dispatch(userID, TypeFreeChatEnding, Payload{
		Title: "Your free chat is ending",
		Body: "About " + strconv.Itoa(remainingSeconds) +
			" seconds left. Continue at " + money(ratePerMinute) + " per minute to keep chatting.",
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":              "FREE_CHAT_ENDING",
			"consultation_id":   strconv.FormatUint(uint64(consultationID), 10),
			"remaining_seconds": strconv.Itoa(remainingSeconds),
			"rate_per_minute":   strconv.FormatFloat(ratePerMinute, 'f', 2, 64),
			"screen":            "consultation",
		},
	}, AppCustomer)
}

//////////////////////////////////////////////////////////////
// Astrologer
//////////////////////////////////////////////////////////////

// ConsultationCancelledByCustomer — the customer gave up while it was still
// ringing. The astrologer app must handle this while the incoming-request
// sheet is on screen and dismiss it.
func (n *Notifier) ConsultationCancelledByCustomer(
	astrologerID uint,
	customerUserID uint,
	medium string,
	consultationID uint,
) {

	if n == nil {
		return
	}

	userID := n.repo.astrologerUserID(astrologerID)

	name := n.repo.userName(customerUserID)

	n.dispatch(userID, TypeRequestCancelled, Payload{
		Title:          mediumTitle(medium) + " request cancelled",
		Body:           name + " cancelled the " + mediumLabel(medium) + " request.",
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":            "CONSULTATION_CANCELLED",
			"medium":          medium,
			"user_id":         strconv.FormatUint(uint64(customerUserID), 10),
			"consultation_id": strconv.FormatUint(uint64(consultationID), 10),
			"end_reason":      "CANCELLED",
			"screen":          "dashboard",
		},
	}, AppAstrologer)
}

// SessionEndingSoonAstrologer warns the other side of the session.
//
// The astrologer app does not tick, so without this it would have no way of
// knowing the session is about to be cut — it would simply stop mid-sentence.
func (n *Notifier) SessionEndingSoonAstrologer(
	astrologerID uint,
	medium string,
	consultationID uint,
	remainingSeconds int,
	isFree bool,
) {

	if n == nil {
		return
	}

	userID := n.repo.astrologerUserID(astrologerID)

	body := "About " + strconv.Itoa(remainingSeconds) +
		" seconds left before the customer's balance runs out."

	if isFree {
		body = "About " + strconv.Itoa(remainingSeconds) +
			" seconds left of the customer's free minutes."
	}

	n.dispatch(userID, TypeSessionEndingSoon, Payload{
		Title:          mediumTitle(medium) + " ending soon",
		Body:           body,
		AndroidChannel: channelCalls,
		Data: map[string]string{
			"type":              "SESSION_ENDING_SOON",
			"medium":            medium,
			"consultation_id":   strconv.FormatUint(uint64(consultationID), 10),
			"remaining_seconds": strconv.Itoa(remainingSeconds),
			"is_free":           strconv.FormatBool(isFree),
			"screen":            "consultation",
		},
	}, AppAstrologer)
}

//////////////////////////////////////////////////////////////
// Both sides
//////////////////////////////////////////////////////////////

// ConsultationEnded — the server closed the session, rather than either app.
//
// Sent when the cap is reached, the free minutes run out, or the sweeper finds
// an abandoned session: in all three the apps need telling, because neither of
// them decided it. An app that receives this must tear down the channel and
// show the receipt — it must NOT call /end, which has already happened.
func (n *Notifier) ConsultationEnded(
	customerUserID uint,
	astrologerID uint,
	medium string,
	consultationID uint,
	consultationNo string,
	endReason string,
	durationSeconds int,
	deducted float64,
	earning float64,
) {

	if n == nil {
		return
	}

	shared := map[string]string{
		"type":             "CONSULTATION_ENDED",
		"medium":           medium,
		"consultation_id":  strconv.FormatUint(uint64(consultationID), 10),
		"consultation_no":  consultationNo,
		"end_reason":       endReason,
		"duration_seconds": strconv.Itoa(durationSeconds),
	}

	//------------------------------------------------
	// Customer
	//------------------------------------------------

	customerData := cloneData(shared)
	customerData["amount"] = strconv.FormatFloat(deducted, 'f', 2, 64)
	customerData["screen"] = "consultation_summary"

	customerBody := "Your " + mediumLabel(medium) + " lasted " +
		formatClock(durationSeconds) + ". " + money(deducted) + " was deducted."

	if deducted == 0 {
		customerBody = "Your free " + mediumLabel(medium) + " lasted " +
			formatClock(durationSeconds) + ". Nothing was charged."
	}

	n.dispatch(customerUserID, TypeConsultationEnded, Payload{
		Title:          mediumTitle(medium) + " ended",
		Body:           customerBody,
		AndroidChannel: channelActivity,
		Data:           customerData,
	}, AppCustomer)

	//------------------------------------------------
	// Astrologer
	//------------------------------------------------

	astrologerUserID := n.repo.astrologerUserID(astrologerID)

	if astrologerUserID == 0 {
		return
	}

	astrologerData := cloneData(shared)
	astrologerData["earning"] = strconv.FormatFloat(earning, 'f', 2, 64)
	astrologerData["settlement_status"] = "PENDING"
	astrologerData["screen"] = "chat_detail"

	n.dispatch(astrologerUserID, TypeConsultationEnded, Payload{
		Title: mediumTitle(medium) + " ended",
		Body: "Your " + mediumLabel(medium) + " lasted " + formatClock(durationSeconds) +
			". " + money(earning) + " added to your pending earnings.",
		AndroidChannel: channelActivity,
		Data:           astrologerData,
	}, AppAstrologer)
}

//////////////////////////////////////////////////////////////
// Wording helpers
//////////////////////////////////////////////////////////////

func cloneData(source map[string]string) map[string]string {

	copied := make(map[string]string, len(source)+3)

	for key, value := range source {
		copied[key] = value
	}

	return copied
}

// mediumTitle is mediumLabel with a capital, for the start of a title.
func mediumTitle(medium string) string {

	label := mediumLabel(medium)

	if label == "" {
		return "Session"
	}

	return strings.ToUpper(label[:1]) + label[1:]
}

func formatClock(seconds int) string {

	if seconds <= 0 {
		return "00:00"
	}

	return pad(seconds/60) + ":" + pad(seconds%60)
}

func pad(value int) string {

	if value < 10 {
		return "0" + strconv.Itoa(value)
	}

	return strconv.Itoa(value)
}
