package middleware

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"strings"
	"time"

	dto "astrology-api/dto/astrology"
	usermodel "astrology-api/models/usermodel"
	"astrology-api/services"

	"github.com/gin-gonic/gin"
)

///////////////////////////////////////////////////////////
// Astrology Service Usage
///////////////////////////////////////////////////////////
//
// TrackAstrologyService wraps one astrology route with the admin panel's
// catalogue (astrology_services):
//
//   before  refuse the call when the admin has switched the service off for
//           this audience - HTTP 403, "This service is currently unavailable."
//   after   write one astrology_service_usage row: who, from which platform,
//           charged or free, succeeded or why not, and how long it took
//
// It reads the outcome from the response the handler already writes, so the
// handlers need no changes. Mount it per route, after the auth middleware:
//
//   track := middleware.TrackAstrologyService(middleware.CustomerSide)
//   auth.POST("/matching/ashtakoot", track("matching.ashtakoot"), matchingController.Ashtakoot)

// Side is which stack a route belongs to; it decides the actor and which of
// the admin's audience switches applies.
type Side int

const (
	CustomerSide Side = iota
	AstrologerSide
)

// Only the start of a response is inspected; every envelope carries status,
// message and data.charged near the front, and a kundali can run to megabytes.
const usageCaptureLimit = 256 << 10

// A request body is read for the optional platform / device_type / app_version
// fields. Larger bodies are left alone.
const usagePeekLimit = 64 << 10

func TrackAstrologyService(side Side) func(code string) gin.HandlerFunc {

	allowed := []string{services.HoroscopePlatformCustomerApp, services.HoroscopePlatformWeb}

	if side == AstrologerSide {
		allowed = []string{services.HoroscopePlatformAstrologerApp, services.HoroscopePlatformAstrologerWeb}
	}

	return func(code string) gin.HandlerFunc {

		return func(ctx *gin.Context) {

			started := time.Now()

			event := services.UsageEvent{Code: code}

			identifyActor(ctx, side, &event)

			event.Platform, event.DeviceType, event.AppVersion =
				services.ResolveClient(ctx.GetHeader, peekClientInfo(ctx), allowed)

			row, found, allowedNow := services.AllowAstrologyService(code, event.ActorType)

			event.Service = row
			event.Found = found

			if !allowedNow {

				ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{
					"status":  false,
					"message": services.ServiceUnavailableMessage,
				})

				event.Succeeded = false
				event.FailureReason = "Service disabled by admin"
				event.Duration = time.Since(started)

				services.RecordAstrologyUsage(event)

				return
			}

			//------------------------------------------------
			// Price (services/astrology_pricing.go)
			//------------------------------------------------

			price := float64(0)

			if found && event.ActorType == services.UsageActorCustomer {
				price = services.QuoteAstrologyService(row, event.UserID)
			}

			if price > 0 {

				balance, err := services.CustomerWalletBalance(event.UserID)

				if err != nil || balance < price {

					// The provider is not called for a call the wallet cannot pay.
					writeRecharge(ctx.Writer, row, price, balance)
					ctx.Abort()

					event.Succeeded = false
					event.FailureReason = "Insufficient wallet balance"
					event.Duration = time.Since(started)

					services.RecordAstrologyUsage(event)

					return
				}

				ctx.Set(services.AstroPriceContextKey, price)
			}

			// A priced call holds its response until the wallet is debited, so a
			// customer never receives a result that was not paid for.
			underlying := ctx.Writer
			capture := &usageCaptureWriter{ResponseWriter: underlying, hold: price > 0}
			ctx.Writer = capture

			ctx.Next()

			ctx.Writer = underlying

			event.Duration = time.Since(started)

			readOutcome(capture, &event)

			if capture.hold {
				settleCharge(capture, row, price, &event)
			}

			services.RecordAstrologyUsage(event)
		}
	}
}

// identifyActor fills the actor from the context the auth middleware left.
// A customer route that is public (kundali/generate, tarot) still credits a
// signed-in customer when the app sends its token; otherwise it is a guest.
func identifyActor(ctx *gin.Context, side Side, event *services.UsageEvent) {

	userID := GetUserID(ctx)

	if side == AstrologerSide {

		event.ActorType = services.UsageActorAstrologer
		event.AstrologerID = services.AstrologerIDForUser(userID)

		return
	}

	if userID == 0 {
		userID = optionalCustomerID(ctx)
	}

	if userID == 0 {
		event.ActorType = services.UsageActorGuest
		return
	}

	event.ActorType = services.UsageActorCustomer
	event.UserID = userID
}

// optionalCustomerID reads a customer token on a public route. Only the
// signature is checked: this attributes a report row, it grants nothing.
func optionalCustomerID(ctx *gin.Context) uint {

	header := strings.TrimSpace(ctx.GetHeader("Authorization"))

	if !strings.HasPrefix(strings.ToLower(header), "bearer ") {
		return 0
	}

	claims, err := services.ValidateJWT(strings.TrimSpace(header[7:]))

	if err != nil {
		return 0
	}

	id, ok := claims["id"].(float64)

	if !ok || id <= 0 {
		return 0
	}

	return uint(id)
}

// peekClientInfo reads platform / device_type / app_version from a JSON body
// and puts the body back for the handler.
func peekClientInfo(ctx *gin.Context) dto.ClientInfo {

	var info dto.ClientInfo

	if ctx.Request.Body == nil ||
		!strings.Contains(strings.ToLower(ctx.GetHeader("Content-Type")), "json") {
		return info
	}

	head, err := io.ReadAll(io.LimitReader(ctx.Request.Body, usagePeekLimit))

	// Stitch the read part back in front of whatever is left.
	ctx.Request.Body = io.NopCloser(io.MultiReader(bytes.NewReader(head), ctx.Request.Body))

	if err != nil || len(head) == 0 || len(head) >= usagePeekLimit {
		return info
	}

	_ = json.Unmarshal(head, &info)

	return info
}

///////////////////////////////////////////////////////////
// Outcome
///////////////////////////////////////////////////////////

// usageCaptureWriter keeps a copy of the start of the response body. With hold
// set it keeps all of it and sends nothing, until settleCharge decides what
// the customer gets.
type usageCaptureWriter struct {
	gin.ResponseWriter
	body      bytes.Buffer
	truncated bool

	hold       bool
	heldStatus int
}

func (w *usageCaptureWriter) Write(data []byte) (int, error) {

	if w.hold {
		return w.body.Write(data)
	}

	w.keep(data)

	return w.ResponseWriter.Write(data)
}

func (w *usageCaptureWriter) WriteString(data string) (int, error) {
	return w.Write([]byte(data))
}

func (w *usageCaptureWriter) WriteHeader(code int) {

	if w.hold {
		w.heldStatus = code
		return
	}

	w.ResponseWriter.WriteHeader(code)
}

func (w *usageCaptureWriter) WriteHeaderNow() {

	if !w.hold {
		w.ResponseWriter.WriteHeaderNow()
	}
}

func (w *usageCaptureWriter) Status() int {

	if w.hold {

		if w.heldStatus == 0 {
			return http.StatusOK
		}

		return w.heldStatus
	}

	return w.ResponseWriter.Status()
}

func (w *usageCaptureWriter) Written() bool {

	if w.hold {
		return w.heldStatus != 0 || w.body.Len() > 0
	}

	return w.ResponseWriter.Written()
}

func (w *usageCaptureWriter) Size() int {

	if w.hold {
		return w.body.Len()
	}

	return w.ResponseWriter.Size()
}

// release sends the held response as the handler wrote it, or with body
// replaced.
func (w *usageCaptureWriter) release(body []byte) {

	w.ResponseWriter.WriteHeader(w.Status())
	_, _ = w.ResponseWriter.Write(body)
}

func (w *usageCaptureWriter) keep(data []byte) {

	room := usageCaptureLimit - w.body.Len()

	if room <= 0 {
		w.truncated = true
		return
	}

	if len(data) > room {
		data = data[:room]
		w.truncated = true
	}

	w.body.Write(data)
}

// usageEnvelope covers the three response conventions in this API:
// helpers.* ({status: bool}), utils.* ({status, statusCode}) and the
// middlewares ({success: bool}).
type usageEnvelope struct {
	Status  interface{}     `json:"status"`
	Success *bool           `json:"success"`
	Message string          `json:"message"`
	Error   string          `json:"error"`
	Data    json.RawMessage `json:"data"`
}

func readOutcome(capture *usageCaptureWriter, event *services.UsageEvent) {

	status := capture.Status()

	event.Succeeded = status < http.StatusBadRequest

	// A binary response (the chart image) or a body too large to keep whole:
	// the HTTP status is all there is to go on.
	if capture.truncated || capture.body.Len() == 0 ||
		!strings.Contains(strings.ToLower(capture.Header().Get("Content-Type")), "json") {

		if !event.Succeeded {
			event.FailureReason = http.StatusText(status)
		}

		return
	}

	var envelope usageEnvelope

	if err := json.Unmarshal(capture.body.Bytes(), &envelope); err != nil {

		if !event.Succeeded {
			event.FailureReason = http.StatusText(status)
		}

		return
	}

	if value, ok := envelope.Status.(bool); ok && !value {
		event.Succeeded = false
	}

	if envelope.Success != nil && !*envelope.Success {
		event.Succeeded = false
	}

	if !event.Succeeded {

		event.FailureReason = firstText(envelope.Message, envelope.Error, http.StatusText(status))

		return
	}

	// dto.VedicDataResponse: {"charged": 10, "data": {...}}
	var charged struct {
		Charged float64 `json:"charged"`
	}

	if len(envelope.Data) > 0 && json.Unmarshal(envelope.Data, &charged) == nil {
		event.ChargedAmount = charged.Charged
	}
}

func firstText(values ...string) string {

	for _, value := range values {

		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}

	return ""
}

///////////////////////////////////////////////////////////
// Charging
///////////////////////////////////////////////////////////

// settleCharge finishes a priced call: a failed call is released as it is and
// costs nothing; a successful one is released only once the wallet has been
// debited, with `charged` set to what was taken.
func settleCharge(
	capture *usageCaptureWriter,
	row usermodel.AstrologyService,
	price float64,
	event *services.UsageEvent,
) {

	if !event.Succeeded {
		capture.release(capture.body.Bytes())
		return
	}

	_, err := services.ChargeAstrologyService(event.UserID, price, row)

	if err != nil {

		event.Succeeded = false

		var short *services.InsufficientBalanceError

		if errors.As(err, &short) {

			// The balance was spent between the quote and the debit.
			event.FailureReason = "Insufficient wallet balance"
			writeRecharge(capture.ResponseWriter, row, price, short.Balance)

			return
		}

		event.FailureReason = "Wallet debit failed: " + err.Error()

		writeJSON(capture.ResponseWriter, http.StatusInternalServerError, gin.H{
			"status":  false,
			"message": "We could not complete the payment for this service. Nothing was charged; please try again.",
		})

		return
	}

	event.ChargedAmount = price

	capture.release(withCharged(capture.body.Bytes(), price))
}

// writeRecharge is the answer to a customer whose wallet cannot pay for a
// service: HTTP 402 with what it costs, what they have, and the shortfall, so
// the app can open the recharge screen with the right amount.
func writeRecharge(w gin.ResponseWriter, row usermodel.AstrologyService, price float64, balance float64) {

	name := strings.TrimSpace(row.Name)

	if name == "" {
		name = "this service"
	}

	shortfall := price - balance

	if shortfall < 0 {
		shortfall = 0
	}

	writeJSON(w, http.StatusPaymentRequired, gin.H{
		"status": false,
		"message": fmt.Sprintf(
			"Insufficient wallet balance. %s costs ₹%.2f and your wallet has ₹%.2f. Please recharge your wallet to continue.",
			name, price, balance,
		),
		"data": gin.H{
			"error_code":        "INSUFFICIENT_BALANCE",
			"recharge_required": true,
			"service_code":      row.Code,
			"service_name":      name,
			"required_amount":   round2(price),
			"wallet_balance":    round2(balance),
			"shortfall":         round2(shortfall),
		},
	})
}

func writeJSON(w gin.ResponseWriter, status int, body gin.H) {

	encoded, _ := json.Marshal(body)

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_, _ = w.Write(encoded)
}

// withCharged sets `charged` in the response to the amount debited: in
// data.charged where the envelope has one (dto.VedicDataResponse), otherwise
// at the top level. A body that is not a JSON object is left untouched.
func withCharged(body []byte, price float64) []byte {

	var top map[string]json.RawMessage

	if json.Unmarshal(body, &top) != nil {
		return body
	}

	amount, _ := json.Marshal(round2(price))

	var data map[string]json.RawMessage

	if raw, ok := top["data"]; ok && json.Unmarshal(raw, &data) == nil && data != nil {

		if _, has := data["charged"]; has {

			data["charged"] = amount

			if encoded, err := json.Marshal(data); err == nil {
				top["data"] = encoded
			}

			return marshalOr(top, body)
		}
	}

	top["charged"] = amount

	return marshalOr(top, body)
}

func marshalOr(value interface{}, fallback []byte) []byte {

	encoded, err := json.Marshal(value)

	if err != nil {
		return fallback
	}

	return encoded
}

func round2(value float64) float64 {
	return math.Round(value*100) / 100
}
