package services

import (
	"encoding/json"
	"fmt"
	"net/url"
	"strings"
	"sync"

	dto "astrology-api/dto/tarot"
)

//////////////////////////////////////////////////////////////
// Tarot
//////////////////////////////////////////////////////////////

// The web flow this serves: pick a reading, enter a name, get a spread of
// face-down cards from Shuffle, choose one (or three), then ask for the
// interpretation. The catalogue pages — the deck, the major arcana, the minor
// arcana, a single card — are answered locally, so only the interpretations
// cost an upstream call.
//
// Readings are free and public, matching the rest of the tarot experience. If a
// price is wanted later it follows the pattern in astrology_base.go: a
// systemflag price and a chargeForRead call after the upstream succeeds.

type TarotService interface {
	Catalogue(request dto.TarotCardListRequest) *dto.TarotCardListResponse

	Card(id string, lang string, withReading bool) (*dto.TarotCardDetailResponse, error)

	Shuffle(request dto.TarotShuffleRequest) (*dto.TarotShuffleResponse, error)

	OneCard(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error)

	ThreeCard(request dto.TarotThreeCardRequest) (*dto.TarotReadingResponse, error)

	Love(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error)

	LoveTriangle(request dto.TarotLoveTriangleRequest) (*dto.TarotReadingResponse, error)

	YesNo(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error)

	Career(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error)

	Breakup(request dto.TarotBreakupRequest) (*dto.TarotReadingResponse, error)

	FortuneCookie(lang string) (*dto.TarotReadingResponse, error)
}

type tarotService struct {
	astrologyBase
}

func NewTarotService() TarotService {
	return &tarotService{astrologyBase: newAstrologyBase()}
}

//////////////////////////////////////////////////////////////
// Love Variants
//////////////////////////////////////////////////////////////

// The four love spreads differ only by endpoint, so they share one handler.
var tarotLoveStyles = map[string]string{
	"in_depth":            "tarot/in-depth-love",
	"erotic":              "tarot/erotic-love",
	"made_for_each_other": "tarot/made-for-each-other-or-not",
	"flirt":               "tarot/flirt-reading",
}

var tarotLoveStyleOrder = []string{
	"in_depth",
	"erotic",
	"made_for_each_other",
	"flirt",
}

var tarotBreakupKinds = map[string]string{
	"romantic": "tarot/romantic-breakup",
	"business": "tarot/business-breakup",
}

//////////////////////////////////////////////////////////////
// Helpers
//////////////////////////////////////////////////////////////

// resolveTarotCard turns a client-supplied card name into a deck card, or says
// so. Validating here means a typo never reaches the vendor, who answers an
// unknown card with a bare 400 and no message.
func resolveTarotCard(name string, field string) (*dto.TarotCard, error) {

	card, found := TarotCardByID(name)

	if !found {

		return nil, fmt.Errorf(
			"unknown tarot card %q in %s - use an id from /tarot/cards, such as the_fool or ace_of_cups",
			name,
			field,
		)
	}

	return card, nil
}

// tarotParams starts the query every tarot endpoint takes.
func tarotParams(lang string) url.Values {

	params := url.Values{}

	params.Set("lang", tarotLang(lang))

	return params
}

// fetch calls one tarot endpoint and returns the unwrapped payload.
func (s *tarotService) fetch(path string, params url.Values) (json.RawMessage, error) {

	client, err := s.vedic()

	if err != nil {
		return nil, err
	}

	return client.Get(path, params)
}

//////////////////////////////////////////////////////////////
// Reading A Single Card
//////////////////////////////////////////////////////////////

// The vendor's coverage is uneven. tarot/daily returns the four life-area texts
// for the major arcana and, for the minor arcana, nothing but the card's
// identity — no words at all. tarot/yes-no returns a meaning and a description
// for every card in the deck, in both directions.
//
// So a one-card reading asks daily first and falls back to yes-no when daily
// comes back wordless. Which endpoint answered is reported, because the two
// bodies do not have the same fields.
const (
	tarotSourceDaily = "daily"

	tarotSourceYesNo = "yes_no"
)

// vendorReading is the union of the fields the two endpoints fill in.
type vendorReading struct {
	Health string `json:"health"`

	Relationship string `json:"relationship"`

	Career string `json:"career"`

	Finance string `json:"finance"`

	Meaning string `json:"meaning"`

	Description string `json:"description"`
}

// parseTarotInterpretation maps either body onto the fixed shape. It returns nil
// when the body carries no prose, which is how the fallback is triggered.
func parseTarotInterpretation(raw json.RawMessage) *dto.TarotInterpretation {

	if len(raw) == 0 {
		return nil
	}

	var body vendorReading

	if err := json.Unmarshal(raw, &body); err != nil {
		return nil
	}

	interpretation := &dto.TarotInterpretation{
		Meaning:      strings.TrimSpace(body.Meaning),
		Description:  strings.TrimSpace(body.Description),
		Health:       strings.TrimSpace(body.Health),
		Relationship: strings.TrimSpace(body.Relationship),
		Career:       strings.TrimSpace(body.Career),
		Finance:      strings.TrimSpace(body.Finance),
	}

	// The identity fields are always present, so "did we get a reading" has to
	// be decided on the prose alone.
	prose := interpretation.Description +
		interpretation.Health +
		interpretation.Relationship +
		interpretation.Career +
		interpretation.Finance

	if strings.TrimSpace(prose) == "" {
		return nil
	}

	return interpretation
}

// readCard fetches one card's general reading, falling back when the first
// endpoint has nothing to say about it.
func (s *tarotService) readCard(
	card *dto.TarotCard,
	direction string,
	lang string,
) (json.RawMessage, *dto.TarotInterpretation, string, error) {

	params := tarotParams(lang)

	params.Set("card_name", card.ID)

	raw, err := s.fetch("tarot/daily", params)

	if err != nil {
		return nil, nil, "", err
	}

	if interpretation := parseTarotInterpretation(raw); interpretation != nil {
		return raw, interpretation, tarotSourceDaily, nil
	}

	// Wordless: the minor arcana. Ask the endpoint that covers the whole deck.
	fallbackParams := tarotParams(lang)

	fallbackParams.Set("card_name", card.ID)
	fallbackParams.Set("direction", direction)

	fallback, fallbackErr := s.fetch("tarot/yes-no", fallbackParams)

	if fallbackErr != nil {

		// Keep the first body rather than failing the reading; the card and its
		// artwork are still worth showing.
		return raw, nil, tarotSourceDaily, nil
	}

	return fallback, parseTarotInterpretation(fallback), tarotSourceYesNo, nil
}

//////////////////////////////////////////////////////////////
// Catalogue
//////////////////////////////////////////////////////////////

func (s *tarotService) Catalogue(request dto.TarotCardListRequest) *dto.TarotCardListResponse {

	return TarotCatalogue(request)
}

// Card is a card's detail page. The identity is local and always present; the
// interpretation is fetched when asked for, and a failure there leaves the page
// standing rather than emptying it.
func (s *tarotService) Card(id string, lang string, withReading bool) (*dto.TarotCardDetailResponse, error) {

	card, err := resolveTarotCard(id, "card id")

	if err != nil {
		return nil, err
	}

	response := &dto.TarotCardDetailResponse{Card: *card}

	if !withReading {
		return response, nil
	}

	reading, interpretation, source, err := s.readCard(card, TarotUpright, lang)

	if err != nil {

		response.Errors = map[string]string{"reading": err.Error()}

		return response, nil
	}

	response.Reading = reading
	response.Interpretation = interpretation
	response.ReadingSource = source

	return response, nil
}

//////////////////////////////////////////////////////////////
// Shuffle
//////////////////////////////////////////////////////////////

// shuffledCard is the part of a shuffle entry worth keeping. The vendor also
// returns image URLs, which are dropped in favour of the catalogue's: they are
// the same files, and taking them from one place keeps a spread consistent with
// the card pages.
type shuffledCard struct {
	ID string `json:"id"`

	Index int `json:"index"`

	Direction string `json:"direction"`
}

// Shuffle deals the face-down spread the querent picks from.
func (s *tarotService) Shuffle(request dto.TarotShuffleRequest) (*dto.TarotShuffleResponse, error) {

	params := tarotParams(request.Lang)

	shuffleType := strings.ToLower(strings.TrimSpace(request.ShuffleType))

	if shuffleType == "" {
		shuffleType = "both"
	}

	params.Set("shuffle_type", shuffleType)

	raw, err := s.fetch("tarot/shuffle", params)

	if err != nil {
		return nil, err
	}

	var dealt []shuffledCard

	if err := json.Unmarshal(raw, &dealt); err != nil {
		return nil, fmt.Errorf("unexpected shuffle response: %w", err)
	}

	response := &dto.TarotShuffleResponse{
		Lang:  tarotLang(request.Lang),
		Cards: make([]dto.TarotDrawnCard, 0, len(dealt)),
	}

	for _, entry := range dealt {

		card, found := TarotCardByID(entry.ID)

		if !found {

			// An id the catalogue does not know is a vendor addition; pass it
			// through with what was given rather than dropping a card from the
			// spread.
			response.Cards = append(response.Cards, dto.TarotDrawnCard{
				ID:        entry.ID,
				Name:      titleCase(entry.ID),
				Direction: normaliseTarotDirection(entry.Direction),
			})

			continue
		}

		response.Cards = append(
			response.Cards,
			drawnTarotCard(card, normaliseTarotDirection(entry.Direction), ""),
		)
	}

	response.Count = len(response.Cards)

	return response, nil
}

//////////////////////////////////////////////////////////////
// Single Card Readings
//////////////////////////////////////////////////////////////

// singleCardReading is the shape shared by one-card, yes/no, career and the
// love spreads: one card in, one interpretation out.
func (s *tarotService) singleCardReading(
	readingType string,
	style string,
	path string,
	request dto.TarotReadingRequest,
	withDirection bool,
) (*dto.TarotReadingResponse, error) {

	card, err := resolveTarotCard(request.Card, "card")

	if err != nil {
		return nil, err
	}

	direction := normaliseTarotDirection(request.Direction)

	params := tarotParams(request.Lang)

	params.Set("card_name", card.ID)

	// Only some endpoints take a direction, and the ones that do not reject it
	// outright rather than ignoring it.
	if withDirection {
		params.Set("direction", direction)
	}

	reading, err := s.fetch(path, params)

	if err != nil {
		return nil, err
	}

	drawn := drawnTarotCard(card, direction, "")

	drawn.Interpretation = parseTarotInterpretation(reading)

	return &dto.TarotReadingResponse{
		Type:    readingType,
		Style:   style,
		Name:    strings.TrimSpace(request.Name),
		Lang:    tarotLang(request.Lang),
		Cards:   []dto.TarotDrawnCard{drawn},
		Reading: reading,
	}, nil
}

// OneCard is the daily one-card reading.
func (s *tarotService) OneCard(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error) {

	card, err := resolveTarotCard(request.Card, "card")

	if err != nil {
		return nil, err
	}

	direction := normaliseTarotDirection(request.Direction)

	reading, interpretation, source, err := s.readCard(card, direction, request.Lang)

	if err != nil {
		return nil, err
	}

	drawn := drawnTarotCard(card, direction, "")

	drawn.Reading = reading
	drawn.Interpretation = interpretation
	drawn.ReadingSource = source

	return &dto.TarotReadingResponse{
		Type:    "one_card",
		Name:    strings.TrimSpace(request.Name),
		Lang:    tarotLang(request.Lang),
		Cards:   []dto.TarotDrawnCard{drawn},
		Reading: reading,
	}, nil
}

// YesNo answers the question the querent had in mind.
func (s *tarotService) YesNo(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error) {

	return s.singleCardReading("yes_no", "", "tarot/yes-no", request, true)
}

// Career reads the card for work.
func (s *tarotService) Career(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error) {

	return s.singleCardReading("career", "", "tarot/career-select", request, true)
}

// Love reads one of the four love spreads.
func (s *tarotService) Love(request dto.TarotReadingRequest) (*dto.TarotReadingResponse, error) {

	style := strings.ToLower(strings.TrimSpace(request.Style))

	if style == "" {
		style = "in_depth"
	}

	path, found := tarotLoveStyles[style]

	if !found {

		return nil, fmt.Errorf(
			"unknown love reading style %q - use one of: %s",
			style,
			strings.Join(tarotLoveStyleOrder, ", "),
		)
	}

	return s.singleCardReading("love", style, path, request, false)
}

//////////////////////////////////////////////////////////////
// Three Card Spread
//////////////////////////////////////////////////////////////

// The vendor has no three-card endpoint, so the past / present / future spread
// is three one-card readings, run together and labelled by position. They are
// fetched concurrently: sequentially the querent waits for three round trips
// where one will do.
var tarotThreeCardPositions = []string{
	"past",
	"present",
	"future",
}

func (s *tarotService) ThreeCard(request dto.TarotThreeCardRequest) (*dto.TarotReadingResponse, error) {

	if len(request.Cards) != len(tarotThreeCardPositions) {

		return nil, fmt.Errorf(
			"a three card reading needs exactly 3 cards, in the order %s",
			strings.Join(tarotThreeCardPositions, ", "),
		)
	}

	drawn := make([]dto.TarotDrawnCard, len(request.Cards))

	for index, pick := range request.Cards {

		card, err := resolveTarotCard(pick.Card, tarotThreeCardPositions[index]+" card")

		if err != nil {
			return nil, err
		}

		drawn[index] = drawnTarotCard(
			card,
			normaliseTarotDirection(pick.Direction),
			tarotThreeCardPositions[index],
		)
	}

	failures := map[string]string{}

	var mutex sync.Mutex

	var group sync.WaitGroup

	for index := range drawn {

		group.Add(1)

		go func(position int) {

			defer group.Done()

			card, _ := TarotCardByID(drawn[position].ID)

			reading, interpretation, source, err := s.readCard(
				card,
				drawn[position].Direction,
				request.Lang,
			)

			mutex.Lock()

			if err != nil {
				failures[drawn[position].Position] = err.Error()
			} else {
				drawn[position].Reading = reading
				drawn[position].Interpretation = interpretation
				drawn[position].ReadingSource = source
			}

			mutex.Unlock()

		}(index)
	}

	group.Wait()

	// All three failing is a failed reading; one or two is a partial spread,
	// which still reads.
	if len(failures) == len(drawn) {

		return nil, fmt.Errorf(
			"could not read the cards: %s",
			failures[tarotThreeCardPositions[0]],
		)
	}

	if len(failures) == 0 {
		failures = nil
	}

	return &dto.TarotReadingResponse{
		Type:   "three_card",
		Name:   strings.TrimSpace(request.Name),
		Lang:   tarotLang(request.Lang),
		Cards:  drawn,
		Errors: failures,
	}, nil
}

//////////////////////////////////////////////////////////////
// Multi Card Readings
//////////////////////////////////////////////////////////////

// LoveTriangle reads the three people involved as one spread — upstream
// interprets them together, so there is no per-card reading here.
func (s *tarotService) LoveTriangle(request dto.TarotLoveTriangleRequest) (*dto.TarotReadingResponse, error) {

	self, err := resolveTarotCard(request.CardSelf, "card_self")

	if err != nil {
		return nil, err
	}

	lover1, err := resolveTarotCard(request.CardLover1, "card_lover1")

	if err != nil {
		return nil, err
	}

	lover2, err := resolveTarotCard(request.CardLover2, "card_lover2")

	if err != nil {
		return nil, err
	}

	params := tarotParams(request.Lang)

	params.Set("card_self", self.ID)
	params.Set("card_lover1", lover1.ID)
	params.Set("card_lover2", lover2.ID)

	reading, err := s.fetch("tarot/love-triangle", params)

	if err != nil {
		return nil, err
	}

	return &dto.TarotReadingResponse{
		Type: "love_triangle",
		Name: strings.TrimSpace(request.Name),
		Lang: tarotLang(request.Lang),
		Cards: []dto.TarotDrawnCard{
			drawnTarotCard(self, TarotUpright, "self"),
			drawnTarotCard(lover1, TarotUpright, "lover1"),
			drawnTarotCard(lover2, TarotUpright, "lover2"),
		},
		Reading: reading,
	}, nil
}

// Breakup reads a cause card and an advice card, romantic or business.
func (s *tarotService) Breakup(request dto.TarotBreakupRequest) (*dto.TarotReadingResponse, error) {

	kind := strings.ToLower(strings.TrimSpace(request.Kind))

	if kind == "" {
		kind = "romantic"
	}

	path, found := tarotBreakupKinds[kind]

	if !found {
		return nil, fmt.Errorf("unknown breakup reading %q - use romantic or business", kind)
	}

	cause, err := resolveTarotCard(request.CardCause, "card_cause")

	if err != nil {
		return nil, err
	}

	advise, err := resolveTarotCard(request.CardAdvise, "card_advise")

	if err != nil {
		return nil, err
	}

	params := tarotParams(request.Lang)

	params.Set("card_cause", cause.ID)
	params.Set("card_advise", advise.ID)

	reading, err := s.fetch(path, params)

	if err != nil {
		return nil, err
	}

	return &dto.TarotReadingResponse{
		Type:  "breakup",
		Style: kind,
		Name:  strings.TrimSpace(request.Name),
		Lang:  tarotLang(request.Lang),
		Cards: []dto.TarotDrawnCard{
			drawnTarotCard(cause, TarotUpright, "cause"),
			drawnTarotCard(advise, TarotUpright, "advice"),
		},
		Reading: reading,
	}, nil
}

// FortuneCookie needs no card at all.
func (s *tarotService) FortuneCookie(lang string) (*dto.TarotReadingResponse, error) {

	reading, err := s.fetch("tarot/fortune-cookie", tarotParams(lang))

	if err != nil {
		return nil, err
	}

	return &dto.TarotReadingResponse{
		Type:    "fortune_cookie",
		Lang:    tarotLang(lang),
		Reading: reading,
	}, nil
}

// NewTarotServiceForAstrologer is the same service for the astrologer routes,
// which never charge.
func NewTarotServiceForAstrologer() TarotService {
	return &tarotService{astrologyBase: freeAstrologyBase()}
}
