// Package llm provides an OpenRouter client that rewrites the game's rules script
// in response to a natural-language rule description from the winning player.
package llm

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"
)

// GenerateRuleRequest is the input to GenerateRule.
type GenerateRuleRequest struct {
	CurrentScript string // the JS rules currently active in the room
	RulePrompt    string // free-text rule description from the winner
	WinnerName    string // for personalised flavour in the description
}

// GenerateRuleResponse is what the API endpoint returns to the browser.
// When Rejected is true, Script and Description are empty and Reason explains why.
type GenerateRuleResponse struct {
	Rejected    bool   `json:"rejected,omitempty"`    // true when the AI refuses to implement the rule
	Reason      string `json:"reason,omitempty"`      // human-readable rejection reason
	Script      string `json:"script,omitempty"`      // validated JS to be stored in the room
	Description string `json:"description,omitempty"` // human-readable summary to display in the UI
}

const systemPrompt = `You are a rules engine for the Czech card game "Prší" (a variant of Crazy Eights).
You will be given the current JavaScript rules script and a natural-language description of a NEW rule
the winning player wants to add. Your job is to return a JSON object.

If the rule CAN be implemented, return:
  {
    "rejected":    false,
    "description": "<short (≤ 120 chars) Czech or English sentence summarising the new rule>",
    "script":      "<COMPLETE updated JavaScript source>"
  }

If the rule CANNOT be implemented, return:
  {
    "rejected": true,
    "reason":   "<clear, friendly explanation of why, and a suggestion for what the player could try instead>"
  }

The script MUST export these functions (no classes, no imports):

  canPlay(cards, topCard, state) → boolean
    Decides whether playing the given array of cards is legal right now.

    cards    – array of { suit, value } objects the player wants to play this turn.
               IMPORTANT: engine enforces exactly ONE card, so cards.length is always 1.
    topCard  – { suit, value } — the current top of the discard pile
    state    – object with the following fields:
        activeSuit   string   — currently required suit (may differ from topCard.suit after a wildcard)
        penaltyCards number   — accumulated draw penalty; 0 = none active
        skipNext     bool     — true when the next player must skip unless they counter with an Ace
        chosenSuit   string   — suit the player chose when playing an Upper (wildcard)
        hand         Card[]   — the acting player's FULL hand, including cards being played
        playerHands  Card[][] — full hands of all players in seat order
        currentPlayerIndex number — acting player seat index
        playerCount  number   — total number of players
        handCounts   number[] — cards held by each player (seat order)
        turnNumber   number   — increments each time turn advances
        roundNumber  number   — increments when turn wraps to seat 0
        lastActionType string — start|play|draw|choice|choice_pending|win
        lastActorIndex number — seat index of actor in last action
        pendingChoices object[] — pending choice queue (each has targetPlayerIndex,prompt,options)

    card/topCard suit values:  "hearts" | "diamonds" | "spades" | "clubs"
    user-facing suit naming in this app: hearts=červený, diamonds=zelený, spades=kule, clubs=žaludy
    card/topCard value values: "7" | "8" | "9" | "10" | "under" | "upper" | "king" | "ace"

  onPlayed(cards, state) → {
      penaltyCards?: number,   — new accumulated draw penalty
      skipNext?:     bool,     — new skip flag
      activeSuit?:   string,   — new active suit
      instantWin?:   bool,     — current player wins immediately (e.g. "king wins the game")
      extraTurn?:    bool,     — current player takes another turn immediately
      reverseDir?:   bool,     — set turn direction: false=clockwise (default), true=counter-clockwise
    }
    'state' has the same shape as above.
    Only returned fields are applied; omitted fields keep their current value.
    state.direction is 1 (clockwise) or -1 (counter-clockwise) — useful to toggle on reverseDir.

  onChoice(choice, state) → same object shape as onPlayed
    Called when onPlayed requests a UI choice via choicePrompt + choiceOptions.
    If a script does not need choices, onChoice can just return current state unchanged.

  Additional optional fields allowed in onPlayed return:
      choicePrompt?:  string,
      choiceOptions?: string[]
      choiceTargetPlayerIndex?: number
    When both are provided and choiceOptions has at least 1 item, frontend prompts current player and
    backend calls onChoice with selected value before turn advances. If a choice is already pending,
    this new one is queued (chained choices).

  Advanced state mutation fields allowed in onPlayed/onChoice return:
      playerHands?: Card[][]
      discardPile?: Card[]   (or playedPile as alias)
      deck?: Card[]
      currentPlayerIndex?: number
    These fully replace corresponding engine state fields (validated by backend).

  To model "discard one extra card" while keeping one-card input:
      discardIndex?: number
    If provided (0-based index into the current player's hand AFTER the played card was removed),
    engine discards that one extra card and applies onPlayed side effects to it as well.
    Use this for rules like "after King, discard one additional compatible card".

Rules about rules:
- Keep all previously implemented rules intact; only ADD the new behaviour.
- Do NOT implement multi-card moves. The engine rejects plays with cards.length != 1.
- If you need an extra discard, use discardIndex in onPlayed instead of cards.length > 1.
- For advanced custom mechanics you may mutate state directly with playerHands/discardPile/deck/currentPlayerIndex.
- You may use state.hand, state.handCounts, state.playerCount for hand-inspection rules.
- Prefer Czech wording in "description" and use suit names červený/zelený/kule/žaludy in user-facing text.
- Do NOT include markdown fences, comments outside the functions, or any code other than function definitions.
- Return ONLY valid JSON — no prose before or after.`

// GenerateRule calls OpenRouter to produce an updated rules script.
func GenerateRule(req GenerateRuleRequest) (GenerateRuleResponse, error) {
	apiKey := os.Getenv("OPENROUTER_API_KEY")
	if apiKey == "" {
		return GenerateRuleResponse{}, fmt.Errorf("OPENROUTER_API_KEY environment variable is not set")
	}

	userMsg := fmt.Sprintf(
		"Current script:\n```javascript\n%s\n```\n\nNew rule proposed by %s:\n%s",
		req.CurrentScript, req.WinnerName, req.RulePrompt,
	)

	body, _ := json.Marshal(map[string]interface{}{
		"model": modelName(),
		"messages": []map[string]string{
			{"role": "system", "content": systemPrompt},
			{"role": "user", "content": userMsg},
		},
		"max_tokens":      1200,
		"temperature":     0.3,
		"response_format": map[string]string{"type": "json_object"},
	})

	httpClient := &http.Client{Timeout: 30 * time.Second}
	httpReq, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewReader(body))
	if err != nil {
		return GenerateRuleResponse{}, err
	}
	httpReq.Header.Set("Authorization", "Bearer "+apiKey)
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("HTTP-Referer", "https://github.com/prsi-game")
	httpReq.Header.Set("X-Title", "Prší")

	resp, err := httpClient.Do(httpReq)
	if err != nil {
		return GenerateRuleResponse{}, fmt.Errorf("OpenRouter request failed: %w", err)
	}
	defer resp.Body.Close()

	var raw struct {
		Choices []struct {
			Message struct {
				Content string `json:"content"`
			} `json:"message"`
		} `json:"choices"`
		Error *struct {
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
		return GenerateRuleResponse{}, fmt.Errorf("decode response: %w", err)
	}
	if raw.Error != nil {
		return GenerateRuleResponse{}, fmt.Errorf("OpenRouter: %s", raw.Error.Message)
	}
	if len(raw.Choices) == 0 {
		return GenerateRuleResponse{}, fmt.Errorf("empty response from OpenRouter")
	}

	content := raw.Choices[0].Message.Content

	// Parse the JSON object the model returned.
	var result GenerateRuleResponse
	if err := json.Unmarshal([]byte(content), &result); err != nil {
		// Try stripping accidental markdown fences.
		cleaned := stripMarkdownFences(content)
		if err2 := json.Unmarshal([]byte(cleaned), &result); err2 != nil {
			return GenerateRuleResponse{}, fmt.Errorf("parse LLM output: %w (raw: %s)", err, content)
		}
	}

	if result.Rejected {
		if strings.TrimSpace(result.Reason) == "" {
			result.Reason = "The AI was unable to implement this rule."
		}
		return result, nil
	}

	if strings.TrimSpace(result.Script) == "" {
		return GenerateRuleResponse{}, fmt.Errorf("LLM returned empty script")
	}

	return result, nil
}

// modelName returns the OpenRouter model to use, defaultable via OPENROUTER_MODEL env.
func modelName() string {
	if m := os.Getenv("OPENROUTER_MODEL"); m != "" {
		return m
	}
	return "openai/gpt-4o-mini"
}

var reFence = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```")

func stripMarkdownFences(s string) string {
	if m := reFence.FindStringSubmatch(s); len(m) == 2 {
		return m[1]
	}
	return s
}
