package services

import (
	"crypto/rand"
	"math/big"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"astrology-api/repositories"
)

// SMS gateway configuration, resolved from the systemflag table.
//
// The credentials used to be read straight off os.Getenv at construction time,
// which has two problems on a live server: .env is outside version control, so a
// deployment whose file predates a change has no way to supply the new value
// short of a redeploy, and rotating an auth key means editing a file and
// restarting the process. The flags below hold the same values in the database,
// so the admin panel can change a sender id or an auth key on a running server.
//
// Resolution order for every setting is systemflag -> environment -> built-in
// default. The environment is kept as a fallback so an existing deployment whose
// rows have not been inserted yet keeps working exactly as it did.

const (
	FlagMsg91AuthKey          = "Msg91AuthKey"
	FlagMsg91ApiURL           = "Msg91ApiUrl"
	FlagMsg91SenderID         = "Msg91SenderId"
	FlagMsg91OtpTemplateID    = "Msg91OtpTemplateId"
	FlagMsg91HeaderID         = "Msg91HeaderId"
	FlagMsg91PeID             = "Msg91PeId"
	FlagMsg91Route            = "Msg91Route"
	FlagMsg91Country          = "Msg91Country"
	FlagMsg91OtpExpiryMinutes = "Msg91OtpExpiryMinutes"
	FlagMsg91OtpLength        = "Msg91OtpLength"

	// FlagSmsGatewayEnabled is the kill switch. "0" stops every real SMS even
	// in production, which is what a carrier outage or an exhausted balance
	// needs: OTPs keep being issued and stored, nothing is dispatched.
	FlagSmsGatewayEnabled = "SmsGatewayEnabled"

	// FlagAppEnvironment mirrors APP_ENV, and is only consulted when APP_ENV is
	// unset, so the .env file stays authoritative for the environment.
	FlagAppEnvironment = "AppEnvironment"

	// FlagOtpStaticCode is the fixed OTP every non-production environment
	// issues, so QA can be pointed at a different code without a rebuild.
	FlagOtpStaticCode = "OtpStaticCode"

	// OTPLESS delivers the email OTP. These rows already existed in the
	// systemflag table - spelled in the lower camel case the older rows use -
	// while the code read OTPLESS_CLIENT_ID and OTPLESS_CLIENT_SECRET from the
	// environment, and neither is in the committed .env. Every production email
	// OTP therefore went out with empty credentials and came back 401, which
	// nothing noticed because the status was ignored.
	FlagOtplessClientID     = "otplessClientId"
	FlagOtplessClientSecret = "otplessSecretKey"
)

// Built-in defaults, matching what the committed .env template carries. They
// exist so the API still sends on a database whose rows were never inserted.
const (
	defaultMsg91ApiURL        = "http://sms.togwe.com/api/sendhttp.php"
	defaultMsg91Sender        = "ASTEYE"
	defaultMsg91OtpTemplateID = "1277178939743229517"
	defaultMsg91Route         = "4"
	defaultMsg91Country       = "0"
	defaultOtpLength          = 6
	defaultOtpExpiryMinutes   = 10

	// StaticOTP is the OTP every non-production environment issues, so the apps
	// can be exercised end to end without an SMS round trip.
	StaticOTP = "123456"

	// MaxOTPAttempts bounds both wrong guesses and resends within one OTP
	// window. It is what the user-side rows are written with, so a row created
	// by the login path no longer carries a zero limit that blocks the first
	// resend outright.
	MaxOTPAttempts = 5
)

// MSG91Config is one resolved snapshot of the gateway settings.
type MSG91Config struct {
	AuthKey          string
	APIURL           string
	Sender           string
	DLTTemplateID    string
	HeaderID         string
	PeID             string
	Route            string
	Country          string
	OTPLength        int
	OTPExpiryMinutes int
	Enabled          bool
	StaticCode       string
}

// The resolved configuration is cached briefly. Without it every OTP would cost
// a dozen single-row queries; with a short window an admin panel change still
// takes effect on its own, without a restart.
const smsConfigTTL = 60 * time.Second

var (
	smsConfigMutex  sync.RWMutex
	smsConfigCached *MSG91Config
	smsConfigExpiry time.Time
)

// ResolveMSG91Config returns the current gateway settings.
func ResolveMSG91Config() MSG91Config {

	smsConfigMutex.RLock()
	cached := smsConfigCached
	fresh := time.Now().Before(smsConfigExpiry)
	smsConfigMutex.RUnlock()

	if fresh && cached != nil {
		return *cached
	}

	config := MSG91Config{
		AuthKey:       configValue(FlagMsg91AuthKey, "MSG91_AUTH_KEY", ""),
		APIURL:        configValue(FlagMsg91ApiURL, "MSG91_API_URL", defaultMsg91ApiURL),
		Sender:        configValue(FlagMsg91SenderID, "MSG91_SENDER_ID", defaultMsg91Sender),
		DLTTemplateID: configValue(FlagMsg91OtpTemplateID, "MSG91_OTP_TEMPLATE_ID", defaultMsg91OtpTemplateID),
		HeaderID:      configValue(FlagMsg91HeaderID, "MSG91_HEADER_ID", ""),
		PeID:          configValue(FlagMsg91PeID, "MSG91_PE_ID", ""),
		Route:         configValue(FlagMsg91Route, "MSG91_ROUTE", defaultMsg91Route),
		Country:       configValue(FlagMsg91Country, "MSG91_COUNTRY", defaultMsg91Country),
	}

	config.OTPLength = configInt(
		FlagMsg91OtpLength,
		"MSG91_OTP_LENGTH",
		defaultOtpLength,
	)

	// The OTP pattern the verify endpoints enforce is exactly six digits, so a
	// flag outside that range would issue codes no client could submit.
	if config.OTPLength < 4 || config.OTPLength > 8 {
		config.OTPLength = defaultOtpLength
	}

	config.OTPExpiryMinutes = configInt(
		FlagMsg91OtpExpiryMinutes,
		"MSG91_OTP_EXPIRY_MINUTES",
		defaultOtpExpiryMinutes,
	)

	if config.OTPExpiryMinutes <= 0 {
		config.OTPExpiryMinutes = defaultOtpExpiryMinutes
	}

	// Absent means enabled: a deployment that never inserted the row keeps
	// sending, and only an explicit "0" switches the gateway off.
	config.Enabled = configValue(FlagSmsGatewayEnabled, "SMS_GATEWAY_ENABLED", "1") != "0"

	config.StaticCode = configValue(FlagOtpStaticCode, "OTP_STATIC_CODE", StaticOTP)

	smsConfigMutex.Lock()
	smsConfigCached = &config
	smsConfigExpiry = time.Now().Add(smsConfigTTL)
	smsConfigMutex.Unlock()

	return config
}

// ResetSMSConfigCache drops the cached snapshot, so the next read goes back to
// the database. Used by the notify/SMS test command.
func ResetSMSConfigCache() {

	smsConfigMutex.Lock()
	smsConfigCached = nil
	smsConfigExpiry = time.Time{}
	smsConfigMutex.Unlock()
}

// configValue reads a setting from systemflag, then the environment, then the
// supplied default.
func configValue(flag string, envName string, fallback string) string {

	if value, err := repositories.GetSystemFlagValue(flag); err == nil {

		if cleaned := cleanConfigValue(value); cleaned != "" {
			return cleaned
		}
	}

	if cleaned := cleanConfigValue(os.Getenv(envName)); cleaned != "" {
		return cleaned
	}

	return fallback
}

func configInt(flag string, envName string, fallback int) int {

	value := configValue(flag, envName, "")

	if value == "" {
		return fallback
	}

	parsed, err := strconv.Atoi(value)

	if err != nil {
		return fallback
	}

	return parsed
}

//////////////////////////////////////////////////////////////
// Environment and OTP values
//////////////////////////////////////////////////////////////

// IsProduction reports whether this deployment sends real OTPs.
//
// It is a function rather than the package-level variable it replaced. That
// variable was initialised at import time, which runs before main calls
// LoadEnv, so it read an empty APP_ENV on every start and the production branch
// was unreachable no matter what the .env file said.
//
// Matching is case-insensitive and trims surrounding whitespace, because the
// committed .env carries "APP_ENV =Production " — a value an exact comparison
// against "Production" also missed.
func IsProduction() bool {

	environment := cleanConfigValue(os.Getenv("APP_ENV"))

	if environment == "" {

		if value, err := repositories.GetSystemFlagValue(FlagAppEnvironment); err == nil {
			environment = cleanConfigValue(value)
		}
	}

	switch strings.ToLower(environment) {
	case "production", "prod", "live":
		return true
	}

	return false
}

// StaticOTPCode is the OTP issued outside production.
//
// Read off the cached snapshot rather than queried per call: registering issues
// two OTPs and delivers two, and each of those used to cost its own single-row
// systemflag query.
func StaticOTPCode() string {

	if code := ResolveMSG91Config().StaticCode; code != "" {
		return code
	}

	return StaticOTP
}

// NewOTP is the single place a user-side OTP comes from.
//
// Production gets a fresh random value delivered over MSG91; every other
// environment gets the static code, so the apps can be tested without SMS.
func NewOTP() (string, error) {

	if !IsProduction() {
		return StaticOTPCode(), nil
	}

	return generateSecureOTP(ResolveMSG91Config().OTPLength)
}

// MustNewOTP is NewOTP for the call sites that have no error path of their own.
// A failing entropy source falls back to the static code rather than issuing an
// empty OTP, which would otherwise be stored and then matched by an empty
// submission.
func MustNewOTP() string {

	otp, err := NewOTP()

	if err != nil || strings.TrimSpace(otp) == "" {
		return StaticOTPCode()
	}

	return otp
}

// OTPExpiry is how long an issued OTP stays usable.
func OTPExpiry() time.Duration {

	return time.Duration(ResolveMSG91Config().OTPExpiryMinutes) * time.Minute
}

// generateSecureOTP draws from crypto/rand, unlike the math/rand helper it
// replaced: a seeded-by-clock PRNG makes an OTP guessable from the send time.
func generateSecureOTP(length int) (string, error) {

	if length < 4 || length > 8 {
		length = defaultOtpLength
	}

	// The first digit is drawn from 1-9 so the code always has `length` digits.
	first, err := rand.Int(rand.Reader, big.NewInt(9))

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

	otp := strconv.FormatInt(first.Int64()+1, 10)

	for i := 1; i < length; i++ {

		digit, err := rand.Int(rand.Reader, big.NewInt(10))

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

		otp += strconv.FormatInt(digit.Int64(), 10)
	}

	return otp, nil
}

// SystemFlagRawValue returns a flag's value, or "" when the row is absent. It
// exists for the diagnostics in cmd/smstest, which has to say whether a setting
// came from the database or from the environment.
func SystemFlagRawValue(flag string) string {

	value, err := repositories.GetSystemFlagValue(flag)

	if err != nil {
		return ""
	}

	return cleanConfigValue(value)
}
