package plugin

import (
	"fmt"
	"log/slog"
	"math"
	"strings"
	"time"

	"maunium.net/go/mautrix/id"
)

// ── Pending Interaction Types ──────────────────────────────────────────────

type advPendingHospitalPay struct {
	Cost int64 // after-insurance amount (recomputed at confirm time for TOCTOU safety)
}

// ── Hospital Command ───────────────────────────────────────────────────────

func (p *AdventurePlugin) handleHospitalCmd(ctx MessageContext) error {
	char, err := loadAdvCharacter(ctx.Sender)
	if err != nil {
		return p.SendDM(ctx.Sender, "You're not registered for Adventure yet. Type `!adventure` to begin.")
	}

	// If alive, reject
	if char.Alive {
		text, _ := advPickFlavor(nurseJoyAlive, ctx.Sender, "hospital_alive")
		return p.SendDM(ctx.Sender, text)
	}

	// On-demand revive if death timer already expired
	if char.DeadUntil != nil && time.Now().UTC().After(*char.DeadUntil) {
		char.Alive = true
		char.DeadUntil = nil
		if err := saveAdvCharacter(char); err != nil {
			slog.Error("hospital: on-demand revive failed", "user", char.UserID, "err", err)
			return p.SendDM(ctx.Sender, "Something went wrong at the hospital. Try again in a moment.")
		}
		return p.SendDM(ctx.Sender, nurseJoyAlreadyRevived)
	}

	// Compute costs
	beforeInsurance := int64(char.CombatLevel) * 125_000
	afterInsurance := int64(char.CombatLevel) * 25_000

	// Check balance
	balance := p.euro.GetBalance(ctx.Sender)
	if balance < float64(afterInsurance) {
		// Can't afford — back to the ditch
		text, _ := advPickFlavor(nurseJoyCantAfford, ctx.Sender, "hospital_broke")
		p.SendDM(ctx.Sender, text)

		// Room announcement
		gr := gamesRoom()
		if gr != "" {
			p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName))
		}
		return nil
	}

	// Check if holiday for bill surcharge
	isHol, _ := isHolidayToday()

	// Build the full multi-act DM
	var sb strings.Builder

	// Act 1 — Admission
	if char.HospitalVisits == 0 {
		sb.WriteString(nurseJoyFirstVisit)
	}

	admission, _ := advPickFlavor(nurseJoyAdmission, ctx.Sender, "hospital_admit")
	sb.WriteString(admission)
	sb.WriteString("\n\n")

	// Act 2 — The Bill
	sb.WriteString(generateItemizedBill(beforeInsurance, afterInsurance, char.HospitalVisits, isHol))
	sb.WriteString("\n\n")

	delivery, _ := advPickFlavor(nurseJoyBillDelivery, ctx.Sender, "hospital_delivery")
	sb.WriteString(delivery)
	sb.WriteString("\n\n───\n\n")

	// Act 3 — After Insurance
	afterText, _ := advPickFlavor(nurseJoyAfterInsurance, ctx.Sender, "hospital_after")
	// Some entries have two %d placeholders (she says the number twice)
	count := strings.Count(afterText, "%d")
	switch count {
	case 1:
		afterText = fmt.Sprintf(afterText, afterInsurance)
	case 2:
		afterText = fmt.Sprintf(afterText, afterInsurance, afterInsurance)
	}
	sb.WriteString(afterText)
	sb.WriteString("\n\n")

	// Payment prompt
	sb.WriteString(fmt.Sprintf("**St. Guildmore's Memorial Hospital**\nAmount due: **%s**\n\n", fmtEuro(afterInsurance)))
	sb.WriteString("Pay now? (yes / no)\n\n")
	sb.WriteString("*Note: Declining payment will result in discharge to the natural respawn queue. " +
		"You'll be back tomorrow. The chair in the waiting room is available in the meantime.*")

	// Store pending interaction
	p.pending.Store(string(ctx.Sender), &advPendingInteraction{
		Type:      "hospital_pay",
		Data:      &advPendingHospitalPay{Cost: afterInsurance},
		ExpiresAt: time.Now().Add(advDMResponseWindow),
	})
	p.advMarkMenuSent(ctx.Sender)

	return p.SendDM(ctx.Sender, sb.String())
}

// ── Payment Resolution ─────────────────────────────────────────────────────

func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *advPendingInteraction) error {
	reply := strings.TrimSpace(strings.ToLower(ctx.Body))

	if reply == "yes" || reply == "y" {
		// Acquire lock
		mu := p.advUserLock(ctx.Sender)
		mu.Lock()
		defer mu.Unlock()

		// Reload fresh (TOCTOU protection)
		char, err := loadAdvCharacter(ctx.Sender)
		if err != nil {
			return p.SendDM(ctx.Sender, "Something went wrong loading your character.")
		}

		// Already alive (or timer expired while they were deciding)?
		if char.Alive {
			return p.SendDM(ctx.Sender, nurseJoyAlreadyRevived)
		}
		if char.DeadUntil != nil && time.Now().UTC().After(*char.DeadUntil) {
			char.Alive = true
			char.DeadUntil = nil
			_ = saveAdvCharacter(char)
			return p.SendDM(ctx.Sender, nurseJoyAlreadyRevived)
		}

		// Recompute cost from current combat level (don't trust cached value)
		cost := int64(char.CombatLevel) * 25_000

		// Check balance
		balance := p.euro.GetBalance(ctx.Sender)
		if balance < float64(cost) {
			text, _ := advPickFlavor(nurseJoyCantAfford, ctx.Sender, "hospital_broke")
			p.SendDM(ctx.Sender, text)

			gr := gamesRoom()
			if gr != "" {
				p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName))
			}
			return nil
		}

		// Debit
		if !p.euro.Debit(ctx.Sender, float64(cost), "hospital_revival") {
			text, _ := advPickFlavor(nurseJoyCantAfford, ctx.Sender, "hospital_broke")
			return p.SendDM(ctx.Sender, text)
		}

		// Community health fund: 10% of revival cost.
		if potCut := int(math.Round(float64(cost) * 0.1)); potCut > 0 {
			communityPotAdd(potCut)
			trackTaxPaid(ctx.Sender, potCut)
		}

		// Revive
		char.Alive = true
		char.DeadUntil = nil
		char.HospitalVisits++
		if err := saveAdvCharacter(char); err != nil {
			slog.Error("hospital: failed to save character after revival", "user", char.UserID, "err", err)
			// Refund on save failure
			p.euro.Credit(ctx.Sender, float64(cost), "hospital_revival_refund")
			return p.SendDM(ctx.Sender, "Something went wrong during recovery. Your payment has been refunded.")
		}

		// Discharge DM
		p.SendDM(ctx.Sender, fmt.Sprintf(
			"✅ **Discharged — you're alive!**\n\n"+
				"%s deducted. Nurse Joy wishes you the best. "+
				"She means it. She always means it.\n\n"+
				"Go get 'em. Type `!adventure` when you're ready.",
			fmtEuro(cost)))

		// Room announcement
		gr := gamesRoom()
		if gr != "" {
			p.SendMessage(gr, fmt.Sprintf(hospitalDischargeAnnounce, char.DisplayName))
		}

		return nil
	}

	// Declined or anything else — back to the ditch
	p.SendDM(ctx.Sender, "Understood. Nurse Joy nods cheerfully and signals the orderlies. "+
		"You're wheeled back to where they found you.\n\n"+
		"Natural respawn will occur in due time. Rest up.")

	char, err := loadAdvCharacter(ctx.Sender)
	if err == nil {
		gr := gamesRoom()
		if gr != "" {
			p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, char.DisplayName))
		}
	}

	return nil
}

// ── Hospital Ad (sent after death) ─────────────────────────────────────────

func (p *AdventurePlugin) sendHospitalAd(userID id.UserID, char *AdventureCharacter) {
	beforeInsurance := int64(char.CombatLevel) * 125_000
	afterInsurance := int64(char.CombatLevel) * 25_000
	respawnTime := "unknown"
	if char.DeadUntil != nil {
		respawnTime = char.DeadUntil.Format("15:04")
	}

	text := fmt.Sprintf(
		"🏥 **St. Guildmore's Memorial Hospital**\n\n"+
			"Nurse Joy has been notified. A bed is being prepared.\n\n"+
			"Type `!hospital` to check in for same-day revival.\n"+
			"Estimated bill: %s (Guild insurance covers %s → you pay %s)\n\n"+
			"Or rest up — natural respawn at %s UTC.",
		fmtEuro(beforeInsurance), fmtEuro(beforeInsurance-afterInsurance), fmtEuro(afterInsurance), respawnTime)

	time.AfterFunc(10*time.Second, func() {
		if err := p.SendDM(userID, text); err != nil {
			slog.Error("hospital: failed to send hospital ad", "user", userID, "err", err)
			return
		}
		// Schedule nudge for 2 hours from now — checked by hospitalNudgeTicker
		p.hospitalNudges.Store(string(userID), time.Now().UTC().Add(2*time.Hour))
	})
}

// ── Hospital Nudge Ticker ──────────────────────────────────────────────────
// Runs every minute, checks for nudges that are due. No long-lived goroutines.

func (p *AdventurePlugin) hospitalNudgeTicker() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()

	for range ticker.C {
		now := time.Now().UTC()
		p.hospitalNudges.Range(func(key, val any) bool {
			uid := key.(string)
			nudgeAt := val.(time.Time)
			if now.Before(nudgeAt) {
				return true
			}

			// Due — remove regardless of outcome
			p.hospitalNudges.Delete(uid)

			userID := id.UserID(uid)
			char, err := loadAdvCharacter(userID)
			if err != nil || char.Alive {
				return true
			}

			// Don't nudge if already in hospital flow
			if v, ok := p.pending.Load(uid); ok {
				if pi, ok := v.(*advPendingInteraction); ok && pi.Type == "hospital_pay" {
					return true
				}
			}

			text, _ := advPickFlavor(nurseJoyNudge, userID, "hospital_nudge")
			if err := p.SendDM(userID, text); err != nil {
				slog.Error("hospital: failed to send nudge", "user", userID, "err", err)
			}
			return true
		})
	}
}
