package services

import (
	"encoding/json"
	"errors"
	"net/url"
	"time"

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

//////////////////////////////////////////////////////////////
// Kundali Matching Service
//////////////////////////////////////////////////////////////

type MatchingService interface {

	//----------------------------------------------------------
	// LIVE LOOKUPS
	//----------------------------------------------------------

	Ashtakoot(userID uint, req dto.MatchingRequest) (*dto.VedicDataResponse, error)

	Dashakoot(userID uint, req dto.MatchingRequest) (*dto.VedicDataResponse, error)

	Aggregate(userID uint, req dto.MatchingRequest) (*dto.VedicDataResponse, error)

	Papasamaya(userID uint, req dto.MatchingRequest) (*dto.VedicDataResponse, error)

	RajjuVedha(userID uint, req dto.MatchingRequest) (*dto.VedicDataResponse, error)

	Nakshatra(req dto.NakshatraMatchRequest) (*dto.VedicDataResponse, error)

	Western(req dto.WesternMatchRequest) (*dto.VedicDataResponse, error)

	//----------------------------------------------------------
	// SAVED MATCHES
	//----------------------------------------------------------

	Save(userID uint, req dto.MatchingRequest) (*dto.SaveMatchingResponse, error)

	List(userID uint, req dto.ListMatchingRequest) (*dto.ListMatchingResponse, error)

	Show(userID uint, matchingID uint) (*dto.MatchingResponse, error)

	Delete(userID uint, matchingID uint) error
}

type matchingService struct {
	astrologyBase
}

func NewMatchingService() MatchingService {
	return &matchingService{astrologyBase: newAstrologyBase()}
}

//////////////////////////////////////////////////////////////
// Parameter Building
//////////////////////////////////////////////////////////////

// coupleParams builds the boy_*/girl_* query set shared by every couple-based
// matching endpoint.
func coupleParams(req dto.MatchingRequest) (url.Values, error) {

	boyDOB, err := ToVedicDate(req.Boy.BirthDate)

	if err != nil {
		return nil, errors.New("boy: " + err.Error())
	}

	girlDOB, err := ToVedicDate(req.Girl.BirthDate)

	if err != nil {
		return nil, errors.New("girl: " + err.Error())
	}

	params := url.Values{}

	params.Set("boy_dob", boyDOB)
	params.Set("boy_tob", req.Boy.BirthTime)
	params.Set("boy_tz", FormatFloat(DefaultTimezone(req.Boy.Timezone)))
	params.Set("boy_lat", FormatFloat(req.Boy.Latitude))
	params.Set("boy_lon", FormatFloat(req.Boy.Longitude))

	params.Set("girl_dob", girlDOB)
	params.Set("girl_tob", req.Girl.BirthTime)
	params.Set("girl_tz", FormatFloat(DefaultTimezone(req.Girl.Timezone)))
	params.Set("girl_lat", FormatFloat(req.Girl.Latitude))
	params.Set("girl_lon", FormatFloat(req.Girl.Longitude))

	params.Set("lang", req.Lang)

	return params, nil
}

// fetchCouple runs one couple-based endpoint, charging the matching price first
// so an upstream failure cannot leave the user paying for nothing.
func (s *matchingService) fetchCouple(
	userID uint,
	path string,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	params, err := coupleParams(req)

	if err != nil {
		return nil, err
	}

	client, err := s.vedic()

	if err != nil {
		return nil, err
	}

	data, err := client.Get(path, params)

	if err != nil {
		return nil, err
	}

	charged, err := s.chargeForRead(
		userID,
		FlagPriceKundaliMatching,
		TxnKundaliMatching,
	)

	if err != nil {
		return nil, err
	}

	return &dto.VedicDataResponse{Charged: charged, Data: data}, nil
}

//////////////////////////////////////////////////////////////
// Live Lookups
//////////////////////////////////////////////////////////////

func (s *matchingService) Ashtakoot(
	userID uint,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	return s.fetchCouple(userID, "matching/ashtakoot", req)
}

func (s *matchingService) Dashakoot(
	userID uint,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	return s.fetchCouple(userID, "matching/dashakoot", req)
}

func (s *matchingService) Aggregate(
	userID uint,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	return s.fetchCouple(userID, "matching/aggregate-match", req)
}

func (s *matchingService) Papasamaya(
	userID uint,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	return s.fetchCouple(userID, "matching/papasamaya-match", req)
}

func (s *matchingService) RajjuVedha(
	userID uint,
	req dto.MatchingRequest,
) (*dto.VedicDataResponse, error) {

	return s.fetchCouple(userID, "matching/rajju-vedha-details", req)
}

// Nakshatra needs only the two star numbers, so it is always free and takes no
// user context.
func (s *matchingService) Nakshatra(
	req dto.NakshatraMatchRequest,
) (*dto.VedicDataResponse, error) {

	client, err := s.vedic()

	if err != nil {
		return nil, err
	}

	params := url.Values{}
	params.Set("boy_star", itoa(req.BoyStar))
	params.Set("girl_star", itoa(req.GirlStar))
	params.Set("lang", req.Lang)

	data, err := client.Get("matching/nakshatra-match", params)

	if err != nil {
		return nil, err
	}

	return &dto.VedicDataResponse{Data: data}, nil
}

func (s *matchingService) Western(
	req dto.WesternMatchRequest,
) (*dto.VedicDataResponse, error) {

	client, err := s.vedic()

	if err != nil {
		return nil, err
	}

	params := url.Values{}
	params.Set("boy_sign", itoa(req.BoySign))
	params.Set("girl_sign", itoa(req.GirlSign))
	params.Set("lang", req.Lang)

	data, err := client.Get("matching/western-match", params)

	if err != nil {
		return nil, err
	}

	return &dto.VedicDataResponse{Data: data}, nil
}

//////////////////////////////////////////////////////////////
// Save
//////////////////////////////////////////////////////////////

// aggregateSummary is the handful of aggregate-match fields worth pulling out
// into columns for the saved-match list; everything else stays in result_json.
type aggregateSummary struct {
	AshtakootScore string `json:"ashtakoot_score"`
	DashakootScore string `json:"dashkoot_score"`
	Conclusion     struct {
		Report string `json:"report"`
	} `json:"conclusion"`
}

func (s *matchingService) Save(
	userID uint,
	req dto.MatchingRequest,
) (*dto.SaveMatchingResponse, error) {

	params, err := coupleParams(req)

	if err != nil {
		return nil, err
	}

	boyBirthDate, err := time.Parse("2006-01-02", req.Boy.BirthDate)

	if err != nil {
		return nil, errors.New("boy: invalid birth_date, expected YYYY-MM-DD")
	}

	girlBirthDate, err := time.Parse("2006-01-02", req.Girl.BirthDate)

	if err != nil {
		return nil, errors.New("girl: invalid birth_date, expected YYYY-MM-DD")
	}

	client, err := s.vedic()

	if err != nil {
		return nil, err
	}

	// Call upstream before touching the wallet: a failed match must not be
	// billed, and the transaction below should stay short.
	data, err := client.Get("matching/aggregate-match", params)

	if err != nil {
		return nil, err
	}

	price, err := s.repo.GetServicePrice(FlagPriceKundaliMatching)

	if err != nil {
		return nil, err
	}

	var summary aggregateSummary

	_ = json.Unmarshal(data, &summary)

	matchType := req.MatchType

	if matchType == "" {
		matchType = "north"
	}

	lang := req.Lang

	if lang == "" {
		lang = "en"
	}

	resultJSON := string(data)

	matching := &usermodel.KundaliMatching{
		BoyName:        req.Boy.Name,
		BoyBirthDate:   boyBirthDate,
		BoyBirthTime:   req.Boy.BirthTime,
		BoyBirthPlace:  req.Boy.BirthPlace,
		BoyLatitude:    &req.Boy.Latitude,
		BoyLongitude:   &req.Boy.Longitude,
		GirlName:       req.Girl.Name,
		GirlBirthDate:  girlBirthDate,
		GirlBirthTime:  req.Girl.BirthTime,
		GirlBirthPlace: req.Girl.BirthPlace,
		GirlLatitude:   &req.Girl.Latitude,
		GirlLongitude:  &req.Girl.Longitude,
		MatchType:      matchType,
		Lang:           lang,
		ResultJSON:     &resultJSON,
		IsActive:       true,
		CreatedBy:      int(userID),
		ModifiedBy:     int(userID),
	}

	boyTZ := FormatFloat(DefaultTimezone(req.Boy.Timezone))
	girlTZ := FormatFloat(DefaultTimezone(req.Girl.Timezone))
	matching.BoyTimezone = &boyTZ
	matching.GirlTimezone = &girlTZ

	if summary.AshtakootScore != "" {
		matching.AshtakootScore = &summary.AshtakootScore
	}

	if summary.DashakootScore != "" {
		matching.DashakootScore = &summary.DashakootScore
	}

	if summary.Conclusion.Report != "" {
		matching.Conclusion = &summary.Conclusion.Report
	}

	tx := s.db.Begin()

	if tx.Error != nil {
		return nil, tx.Error
	}

	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		}
	}()

	if err := s.chargeWallet(tx, userID, price, TxnKundaliMatching); err != nil {
		tx.Rollback()
		return nil, err
	}

	if err := s.repo.CreateMatching(tx, matching); err != nil {
		tx.Rollback()
		return nil, err
	}

	if err := tx.Commit().Error; err != nil {
		return nil, err
	}

	response := toMatchingResponse(matching, true)

	return &dto.SaveMatchingResponse{
		Charged:  price,
		Matching: response,
	}, nil
}

//////////////////////////////////////////////////////////////
// Saved Match Reads
//////////////////////////////////////////////////////////////

func (s *matchingService) List(
	userID uint,
	req dto.ListMatchingRequest,
) (*dto.ListMatchingResponse, error) {

	page, limit := req.Normalized()

	total, err := s.repo.CountUserMatchings(userID, req.Search)

	if err != nil {
		return nil, err
	}

	records, err := s.repo.GetUserMatchings(userID, req.Search, page, limit)

	if err != nil {
		return nil, err
	}

	list := make([]dto.MatchingResponse, 0, len(records))

	for index := range records {
		// The stored payload is large; the list view only needs the summary.
		list = append(list, toMatchingResponse(&records[index], false))
	}

	return &dto.ListMatchingResponse{
		Total:    total,
		Page:     page,
		Limit:    limit,
		Matching: list,
	}, nil
}

// Show replays the stored result, so re-opening a saved match costs neither a
// wallet deduction nor a Vedic call.
func (s *matchingService) Show(
	userID uint,
	matchingID uint,
) (*dto.MatchingResponse, error) {

	matching, err := s.repo.FindUserMatching(userID, matchingID)

	if err != nil {
		return nil, errors.New("matching not found")
	}

	response := toMatchingResponse(matching, true)

	return &response, nil
}

func (s *matchingService) Delete(userID uint, matchingID uint) error {
	return s.repo.DeleteMatching(userID, matchingID)
}

//////////////////////////////////////////////////////////////
// Mapping
//////////////////////////////////////////////////////////////

func toMatchingResponse(
	matching *usermodel.KundaliMatching,
	includeResult bool,
) dto.MatchingResponse {

	response := dto.MatchingResponse{
		ID:             matching.ID,
		BoyName:        matching.BoyName,
		BoyBirthDate:   matching.BoyBirthDate.Format("2006-01-02"),
		BoyBirthTime:   matching.BoyBirthTime,
		BoyBirthPlace:  matching.BoyBirthPlace,
		GirlName:       matching.GirlName,
		GirlBirthDate:  matching.GirlBirthDate.Format("2006-01-02"),
		GirlBirthTime:  matching.GirlBirthTime,
		GirlBirthPlace: matching.GirlBirthPlace,
		MatchType:      matching.MatchType,
		Lang:           matching.Lang,
		AshtakootScore: matching.AshtakootScore,
		DashakootScore: matching.DashakootScore,
		Conclusion:     matching.Conclusion,
		PDFLink:        matching.PDFLink,
		CreatedAt:      matching.CreatedAt,
	}

	if includeResult && matching.ResultJSON != nil {
		response.Result = json.RawMessage(*matching.ResultJSON)
	}

	return response
}

// NewMatchingServiceForAstrologer is the same service for the astrologer routes,
// which never charge.
func NewMatchingServiceForAstrologer() MatchingService {
	return &matchingService{astrologyBase: freeAstrologyBase()}
}
