package services

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

//////////////////////////////////////////////////////////////
// VedicAstroAPI Client
//////////////////////////////////////////////////////////////

const VedicBaseURL = "https://api.vedicastroapi.com/v3-json"

// vedicEnvelope is the wrapper every v3-json response uses. The transport can
// return HTTP 200 while Status carries a failure (402 out of api calls, 400
// invalid parameter), so the body must always be inspected.
type vedicEnvelope struct {
	Status            int             `json:"status"`
	Response          json.RawMessage `json:"response"`
	RemainingAPICalls *int            `json:"remaining_api_calls"`

	// Message carries the detail on the failures that arrive without a
	// response body at all — "Not Found" for an endpoint that is not on the
	// plan, "Internal Server Error" for one that is broken upstream.
	Message string `json:"message"`
}

type VedicClient struct {
	apiKey string
	client *http.Client
}

func NewVedicClient(apiKey string) *VedicClient {

	return &VedicClient{
		apiKey: apiKey,
		client: &http.Client{Timeout: 30 * time.Second},
	}
}

//////////////////////////////////////////////////////////////
// Get
//////////////////////////////////////////////////////////////

// Get calls a v3-json endpoint and returns the unwrapped "response" payload.
// api_key and lang are filled in when the caller left them out.
func (c *VedicClient) Get(
	path string,
	params url.Values,
) (json.RawMessage, error) {

	if c.apiKey == "" {
		return nil, errors.New("vedic api key is not configured")
	}

	if params == nil {
		params = url.Values{}
	}

	params.Set("api_key", c.apiKey)

	if params.Get("lang") == "" {
		params.Set("lang", "en")
	}

	fullURL := fmt.Sprintf(
		"%s/%s?%s",
		VedicBaseURL,
		strings.TrimPrefix(path, "/"),
		params.Encode(),
	)

	resp, err := c.client.Get(fullURL)

	if err != nil {
		return nil, fmt.Errorf("astrology service unreachable: %w", err)
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)

	if err != nil {
		return nil, err
	}

	var envelope vedicEnvelope

	if err := json.Unmarshal(body, &envelope); err != nil {
		return nil, fmt.Errorf("unexpected astrology service response: %w", err)
	}

	if envelope.Status != http.StatusOK {
		return nil, vedicError(envelope)
	}

	return envelope.Response, nil
}

//////////////////////////////////////////////////////////////
// Error Mapping
//////////////////////////////////////////////////////////////

// vedicError turns an in-body failure into a message worth showing. Response is
// a bare JSON string on failure, so it is unquoted where possible.
func vedicError(envelope vedicEnvelope) error {

	message := strings.TrimSpace(string(envelope.Response))

	var text string

	if err := json.Unmarshal(envelope.Response, &text); err == nil && text != "" {
		message = text
	}

	if message == "" {
		message = strings.TrimSpace(envelope.Message)
	}

	if message == "" {
		message = "no detail returned"
	}

	// 402 means the subscription is exhausted. Surfacing it verbatim to an app
	// user is noise, so name it for whoever reads the logs instead.
	if envelope.Status == http.StatusPaymentRequired {
		return fmt.Errorf("astrology service quota exhausted (%s)", message)
	}

	return fmt.Errorf("astrology service error (status %d): %s", envelope.Status, message)
}

//////////////////////////////////////////////////////////////
// Parameter Helpers
//////////////////////////////////////////////////////////////

// ToVedicDate converts an API-facing YYYY-MM-DD date into the DD/MM/YYYY the
// Vedic API expects. A value already in DD/MM/YYYY is passed through.
func ToVedicDate(value string) (string, error) {

	value = strings.TrimSpace(value)

	if value == "" {
		return "", errors.New("date is required")
	}

	if parsed, err := time.Parse("2006-01-02", value); err == nil {
		return parsed.Format("02/01/2006"), nil
	}

	if _, err := time.Parse("02/01/2006", value); err == nil {
		return value, nil
	}

	return "", fmt.Errorf("invalid date %q, expected YYYY-MM-DD", value)
}

// DefaultTimezone falls back to IST when the client omits the offset, which the
// app does for users who never set a birth place.
func DefaultTimezone(timezone float64) float64 {

	if timezone == 0 {
		return 5.5
	}

	return timezone
}

// FormatFloat renders coordinates and offsets without scientific notation.
func FormatFloat(value float64) string {
	return fmt.Sprintf("%g", value)
}
