package engine

import (
	"fmt"
	"log"
	"strconv"

	"github.com/dop251/goja"
)

// DefaultScript implements the standard Prší rules.
//
// Script contract — every rules script MUST export canPlay/onPlayed.
// onChoice is optional; if missing, default no-op behaviour is used.
//
//	canPlay(cards, topCard, state) → bool
//	onPlayed(cards, state)        → OnPlayedResult
//	onChoice(choice, state)       → OnPlayedResult
//
// cards    – array of { suit, value } objects the player wants to play.
//
//	Engine enforces exactly one card, so cards.length is always 1.
//
// topCard  – { suit, value } — current top of the discard pile
// state    – see GameScriptState below
//
// onPlayed return object (all fields optional — omitted fields keep their current value):
//
//	penaltyCards  number  — new accumulated draw penalty
//	skipNext      bool    — new skip flag
//	activeSuit    string  — new active suit
//	instantWin    bool    — current player wins immediately
//	extraTurn     bool    — current player gets an additional turn
//	reverseDir    bool    — toggle turn-order direction (false = clockwise, true = counter-clockwise)
//	choicePrompt  string  — optional UI prompt shown to current player
//	choiceOptions string[] — allowed answer options for that prompt
//	choiceTargetPlayerIndex number — optional target player for the choice prompt
//	discardIndex  number  — optional index of one extra card to discard from current hand
//	playerHands   Card[][] — optional full replacement of all players' hands
//	discardPile   Card[]  — optional full replacement of played/discard pile
//	playedPile    Card[]  — alias for discardPile
//	deck          Card[]  — optional full replacement of draw deck
//	currentPlayerIndex number — optional current player override
//
// state also includes richer context for advanced rules:
//
//	turnNumber, roundNumber, lastActionType, lastActorIndex, pendingChoices
const DefaultScript = `
// ── Standard Prší rules ───────────────────────────────────────────────

function canPlay(cards, topCard, state) {
    if (cards.length !== 1) return false;

    var card = cards[0];

    if (state.penaltyCards > 0) return card.value === "7";
    if (state.skipNext)         return card.value === "ace";
    if (card.value === "upper") return true;

    return card.suit === state.activeSuit || card.value === topCard.value;
}

function onPlayed(cards, state) {
    var card = cards[0];
    var result = {
        penaltyCards: 0,
        skipNext:     false,
        activeSuit:   card.suit
    };

    if (card.value === "7") {
        result.penaltyCards = state.penaltyCards + 2;
    } else if (card.value === "ace") {
        result.skipNext   = true;
        result.activeSuit = card.suit;
    } else if (card.value === "upper") {
        result.activeSuit = state.chosenSuit || card.suit;
    }

    return result;
}

function onChoice(choice, state) {
    return {
        penaltyCards: state.penaltyCards,
        skipNext: state.skipNext,
        activeSuit: state.activeSuit
    };
}
`

// GameScriptState is the rich context object passed into every JS function call.
type GameScriptState struct {
	ActiveSuit         Suit
	PenaltyCards       int
	SkipNext           bool
	ChosenSuit         Suit
	Hand               []Card // acting player's full hand (including cards being played)
	PlayerHands        [][]Card
	CurrentPlayerIndex int
	PlayerCount        int
	HandCounts         []int // cards held by each player, in seat order
	Direction          int   // 1 = clockwise, -1 = counter-clockwise
	TurnNumber         int
	RoundNumber        int
	LastActionType     string
	LastActorIndex     int
	PendingChoices     []ScriptPendingChoice
}

type ScriptPendingChoice struct {
	TargetPlayerIndex int
	Prompt            string
	Options           []string
}

// OnPlayedResult carries every mutation onPlayed() can request.
// All fields are optional; the engine only applies what the script explicitly returns.
type OnPlayedResult struct {
	PenaltyCards          int
	SkipNext              bool
	ActiveSuit            Suit
	InstantWin            bool // current player wins immediately
	ExtraTurn             bool // current player takes another turn
	SetReverseDir         bool // whether to apply the ReverseDir value
	ReverseDir            bool // new direction: false=CW, true=CCW (only used when SetReverseDir=true)
	ChoicePrompt          string
	ChoiceOptions         []string
	SetDiscardIdx         bool
	DiscardIdx            int
	SetPlayerHands        bool
	PlayerHands           [][]Card
	SetDiscardPile        bool
	DiscardPile           []Card
	SetDeck               bool
	Deck                  []Card
	SetCurrentPlayer      bool
	CurrentPlayer         int
	SetChoiceTargetPlayer bool
	ChoiceTargetPlayer    int
}

// ScriptRuntime holds a compiled goja VM. NOT goroutine-safe.
type ScriptRuntime struct {
	vm     *goja.Runtime
	source string
}

func NewScriptRuntime(source string) (*ScriptRuntime, error) {
	vm := goja.New()
	if _, err := vm.RunString(source); err != nil {
		return nil, fmt.Errorf("script compile error: %w", err)
	}
	for _, fn := range []string{"canPlay", "onPlayed"} {
		if _, ok := goja.AssertFunction(vm.Get(fn)); !ok {
			return nil, fmt.Errorf("script is missing required function %q", fn)
		}
	}
	return &ScriptRuntime{vm: vm, source: source}, nil
}

func (sr *ScriptRuntime) Source() string { return sr.source }

// CanPlay calls JS canPlay(cards, topCard, state) → bool.
func (sr *ScriptRuntime) CanPlay(cards []Card, topCard Card, state GameScriptState) (bool, error) {
	fn, ok := goja.AssertFunction(sr.vm.Get("canPlay"))
	if !ok {
		return false, fmt.Errorf("canPlay not found")
	}
	result, err := fn(goja.Undefined(), sr.cardsVal(cards), sr.cardVal(topCard), sr.stateVal(state))
	if err != nil {
		return false, err
	}
	return result.ToBoolean(), nil
}

// OnPlayed calls JS onPlayed(cards, state) and returns structured mutations.
func (sr *ScriptRuntime) OnPlayed(cards []Card, state GameScriptState) (OnPlayedResult, error) {
	defaults := OnPlayedResult{
		PenaltyCards: state.PenaltyCards,
		SkipNext:     state.SkipNext,
		ActiveSuit:   state.ActiveSuit,
	}
	fn, ok := goja.AssertFunction(sr.vm.Get("onPlayed"))
	if !ok {
		return defaults, fmt.Errorf("onPlayed not found")
	}
	res, err := fn(goja.Undefined(), sr.cardsVal(cards), sr.stateVal(state))
	if err != nil {
		return defaults, err
	}
	return sr.parseResult(res, defaults), nil
}

// OnChoice calls JS onChoice(choice, state) and returns structured mutations.
func (sr *ScriptRuntime) OnChoice(choice string, state GameScriptState) (OnPlayedResult, error) {
	defaults := OnPlayedResult{
		PenaltyCards: state.PenaltyCards,
		SkipNext:     state.SkipNext,
		ActiveSuit:   state.ActiveSuit,
	}
	fn, ok := goja.AssertFunction(sr.vm.Get("onChoice"))
	if !ok {
		return defaults, nil
	}
	res, err := fn(goja.Undefined(), sr.vm.ToValue(choice), sr.stateVal(state))
	if err != nil {
		return defaults, err
	}
	return sr.parseResult(res, defaults), nil
}

// ── Value builders ────────────────────────────────────────────────────────────

func (sr *ScriptRuntime) cardVal(c Card) goja.Value {
	return sr.vm.ToValue(map[string]string{"suit": string(c.Suit), "value": string(c.Value)})
}

func (sr *ScriptRuntime) cardsVal(cards []Card) goja.Value {
	objs := make([]interface{}, len(cards))
	for i, c := range cards {
		objs[i] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
	}
	return sr.vm.ToValue(objs)
}

func (sr *ScriptRuntime) stateVal(s GameScriptState) goja.Value {
	hand := make([]interface{}, len(s.Hand))
	for i, c := range s.Hand {
		hand[i] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
	}
	playerHands := make([]interface{}, len(s.PlayerHands))
	for i, h := range s.PlayerHands {
		cards := make([]interface{}, len(h))
		for j, c := range h {
			cards[j] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
		}
		playerHands[i] = cards
	}
	counts := make([]interface{}, len(s.HandCounts))
	for i, n := range s.HandCounts {
		counts[i] = n
	}
	pending := make([]interface{}, len(s.PendingChoices))
	for i, ch := range s.PendingChoices {
		opts := make([]interface{}, len(ch.Options))
		for j, o := range ch.Options {
			opts[j] = o
		}
		pending[i] = map[string]interface{}{
			"targetPlayerIndex": ch.TargetPlayerIndex,
			"prompt":            ch.Prompt,
			"options":           opts,
		}
	}
	return sr.vm.ToValue(map[string]interface{}{
		"activeSuit":         string(s.ActiveSuit),
		"penaltyCards":       s.PenaltyCards,
		"skipNext":           s.SkipNext,
		"chosenSuit":         string(s.ChosenSuit),
		"hand":               hand,
		"playerHands":        playerHands,
		"currentPlayerIndex": s.CurrentPlayerIndex,
		"playerCount":        s.PlayerCount,
		"handCounts":         counts,
		"direction":          s.Direction,
		"turnNumber":         s.TurnNumber,
		"roundNumber":        s.RoundNumber,
		"lastActionType":     s.LastActionType,
		"lastActorIndex":     s.LastActorIndex,
		"pendingChoices":     pending,
	})
}

// ── Result parser — uses goja's own object API, not Export(). ─────────────────
// This is robust regardless of how the LLM wrote the return statement.

func (sr *ScriptRuntime) parseResult(v goja.Value, defaults OnPlayedResult) OnPlayedResult {
	if goja.IsNull(v) || goja.IsUndefined(v) {
		return defaults
	}
	obj := v.ToObject(sr.vm)
	if obj == nil {
		return defaults
	}
	out := defaults

	if p := objGet(sr.vm, obj, "penaltyCards"); p != nil {
		out.PenaltyCards = int(p.ToInteger())
	}
	if s := objGet(sr.vm, obj, "skipNext"); s != nil {
		out.SkipNext = s.ToBoolean()
	}
	if a := objGet(sr.vm, obj, "activeSuit"); a != nil {
		if str := a.String(); str != "" && str != "undefined" {
			out.ActiveSuit = Suit(str)
		}
	}
	if w := objGet(sr.vm, obj, "instantWin"); w != nil {
		out.InstantWin = w.ToBoolean()
	}
	if e := objGet(sr.vm, obj, "extraTurn"); e != nil {
		out.ExtraTurn = e.ToBoolean()
	}
	if rd := objGet(sr.vm, obj, "reverseDir"); rd != nil {
		out.ReverseDir = rd.ToBoolean()
		out.SetReverseDir = true
	}
	if cp := objGet(sr.vm, obj, "choicePrompt"); cp != nil {
		out.ChoicePrompt = cp.String()
	}
	if co := objGet(sr.vm, obj, "choiceOptions"); co != nil {
		if arr := co.ToObject(sr.vm); arr != nil {
			var opts []string
			for i := 0; ; i++ {
				v := arr.Get(fmt.Sprintf("%d", i))
				if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
					break
				}
				opts = append(opts, v.String())
			}
			out.ChoiceOptions = opts
		}
	}
	if di := objGet(sr.vm, obj, "discardIndex"); di != nil {
		out.DiscardIdx = int(di.ToInteger())
		out.SetDiscardIdx = true
	}
	if ph := objGet(sr.vm, obj, "playerHands"); ph != nil {
		if hands, ok := sr.parseHands(ph); ok {
			out.PlayerHands = hands
			out.SetPlayerHands = true
		}
	}
	if dp := objGet(sr.vm, obj, "discardPile"); dp != nil {
		if pile, ok := sr.parseCards(dp); ok {
			out.DiscardPile = pile
			out.SetDiscardPile = true
		}
	}
	if pp := objGet(sr.vm, obj, "playedPile"); pp != nil {
		if pile, ok := sr.parseCards(pp); ok {
			out.DiscardPile = pile
			out.SetDiscardPile = true
		}
	}
	if dk := objGet(sr.vm, obj, "deck"); dk != nil {
		if deck, ok := sr.parseCards(dk); ok {
			out.Deck = deck
			out.SetDeck = true
		}
	}
	if cp := objGet(sr.vm, obj, "currentPlayerIndex"); cp != nil {
		out.CurrentPlayer = int(cp.ToInteger())
		out.SetCurrentPlayer = true
	}
	if tp := objGet(sr.vm, obj, "choiceTargetPlayerIndex"); tp != nil {
		out.ChoiceTargetPlayer = int(tp.ToInteger())
		out.SetChoiceTargetPlayer = true
	}

	return out
}

// objGet returns the named property of obj, or nil if it is absent/undefined/null.
func objGet(vm *goja.Runtime, obj *goja.Object, key string) goja.Value {
	v := obj.Get(key)
	if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
		return nil
	}
	_ = vm // kept for potential future use
	_ = log.Writer
	return v
}

func (sr *ScriptRuntime) parseHands(v goja.Value) ([][]Card, bool) {
	obj := v.ToObject(sr.vm)
	if obj == nil {
		return nil, false
	}
	length := obj.Get("length")
	if length == nil || goja.IsUndefined(length) || goja.IsNull(length) {
		return nil, false
	}
	n := int(length.ToInteger())
	if n < 0 {
		return nil, false
	}
	hands := make([][]Card, n)
	for i := 0; i < n; i++ {
		entry := obj.Get(strconv.Itoa(i))
		cards, ok := sr.parseCards(entry)
		if !ok {
			return nil, false
		}
		hands[i] = cards
	}
	return hands, true
}

func (sr *ScriptRuntime) parseCards(v goja.Value) ([]Card, bool) {
	obj := v.ToObject(sr.vm)
	if obj == nil {
		return nil, false
	}
	length := obj.Get("length")
	if length == nil || goja.IsUndefined(length) || goja.IsNull(length) {
		return nil, false
	}
	n := int(length.ToInteger())
	if n < 0 {
		return nil, false
	}
	cards := make([]Card, n)
	for i := 0; i < n; i++ {
		entry := obj.Get(strconv.Itoa(i))
		card, ok := sr.parseCard(entry)
		if !ok {
			return nil, false
		}
		cards[i] = card
	}
	return cards, true
}

func (sr *ScriptRuntime) parseCard(v goja.Value) (Card, bool) {
	obj := v.ToObject(sr.vm)
	if obj == nil {
		return Card{}, false
	}
	sv := obj.Get("suit")
	vv := obj.Get("value")
	if sv == nil || vv == nil || goja.IsUndefined(sv) || goja.IsNull(sv) || goja.IsUndefined(vv) || goja.IsNull(vv) {
		return Card{}, false
	}
	card := Card{Suit: Suit(sv.String()), Value: Value(vv.String())}
	if !isValidSuit(card.Suit) || !isValidValue(card.Value) {
		return Card{}, false
	}
	return card, true
}

func isValidSuit(s Suit) bool {
	switch s {
	case Hearts, Diamonds, Spades, Clubs:
		return true
	default:
		return false
	}
}

func isValidValue(v Value) bool {
	switch v {
	case Seven, Eight, Nine, Ten, Under, Upper, King, Ace:
		return true
	default:
		return false
	}
}
