package services_astrologer

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
)

// Mobile number validation, driven by the country code the request carries.
//
// The rule used to be hardcoded to India — ten digits starting 6-9 — which is
// right for +91 and wrong for every other country the app can register from: a
// UAE number is nine digits starting 5, a Singapore number eight. The number is
// now checked against the rule for its own country, so a valid foreign number
// is accepted and an invalid one is still refused.
//
// users.countryCode holds a dial code ("+91", "91"), which is what the app
// sends, so that is the primary form. An ISO alpha-2 code ("IN") is accepted
// for the countries listed below as a convenience.

// mobileRule is the national mobile number format for one country: how many
// digits after the dial code, and which digits it may start with.
type mobileRule struct {
	Country string

	MinDigits int
	MaxDigits int

	// LeadingDigits lists the permitted first digits. Empty means any.
	LeadingDigits string
}

// mobileRules is keyed by dial code without the "+". Add a country here when
// its numbers need a tighter rule than defaultMobileRule; anything not listed
// still has to look like a phone number, it is just not length-checked to the
// digit.
var mobileRules = map[string]mobileRule{
	"91":  {Country: "India", MinDigits: 10, MaxDigits: 10, LeadingDigits: "6789"},
	"1":   {Country: "USA/Canada", MinDigits: 10, MaxDigits: 10, LeadingDigits: "23456789"},
	"44":  {Country: "United Kingdom", MinDigits: 10, MaxDigits: 10, LeadingDigits: "7"},
	"971": {Country: "United Arab Emirates", MinDigits: 9, MaxDigits: 9, LeadingDigits: "5"},
	"966": {Country: "Saudi Arabia", MinDigits: 9, MaxDigits: 9, LeadingDigits: "5"},
	"974": {Country: "Qatar", MinDigits: 8, MaxDigits: 8, LeadingDigits: "3567"},
	"965": {Country: "Kuwait", MinDigits: 8, MaxDigits: 8, LeadingDigits: "569"},
	"968": {Country: "Oman", MinDigits: 8, MaxDigits: 8, LeadingDigits: "79"},
	"973": {Country: "Bahrain", MinDigits: 8, MaxDigits: 8, LeadingDigits: "3"},
	"65":  {Country: "Singapore", MinDigits: 8, MaxDigits: 8, LeadingDigits: "89"},
	"60":  {Country: "Malaysia", MinDigits: 9, MaxDigits: 10, LeadingDigits: "1"},
	"61":  {Country: "Australia", MinDigits: 9, MaxDigits: 9, LeadingDigits: "4"},
	"64":  {Country: "New Zealand", MinDigits: 8, MaxDigits: 10, LeadingDigits: "2"},
	"977": {Country: "Nepal", MinDigits: 10, MaxDigits: 10, LeadingDigits: "9"},
	"94":  {Country: "Sri Lanka", MinDigits: 9, MaxDigits: 9, LeadingDigits: "7"},
	"880": {Country: "Bangladesh", MinDigits: 10, MaxDigits: 10, LeadingDigits: "1"},
	"92":  {Country: "Pakistan", MinDigits: 10, MaxDigits: 10, LeadingDigits: "3"},
	"27":  {Country: "South Africa", MinDigits: 9, MaxDigits: 9, LeadingDigits: "6789"},
	"234": {Country: "Nigeria", MinDigits: 10, MaxDigits: 10, LeadingDigits: "789"},
	"254": {Country: "Kenya", MinDigits: 9, MaxDigits: 9, LeadingDigits: "17"},
	"49":  {Country: "Germany", MinDigits: 10, MaxDigits: 11, LeadingDigits: "1"},
	"33":  {Country: "France", MinDigits: 9, MaxDigits: 9, LeadingDigits: "67"},
	"39":  {Country: "Italy", MinDigits: 9, MaxDigits: 10, LeadingDigits: "3"},
	"7":   {Country: "Russia/Kazakhstan", MinDigits: 10, MaxDigits: 10, LeadingDigits: "9"},
	"86":  {Country: "China", MinDigits: 11, MaxDigits: 11, LeadingDigits: "1"},
	"81":  {Country: "Japan", MinDigits: 10, MaxDigits: 10, LeadingDigits: "789"},
	"62":  {Country: "Indonesia", MinDigits: 9, MaxDigits: 12, LeadingDigits: "8"},
	"63":  {Country: "Philippines", MinDigits: 10, MaxDigits: 10, LeadingDigits: "9"},
	"66":  {Country: "Thailand", MinDigits: 9, MaxDigits: 9, LeadingDigits: "689"},
	"84":  {Country: "Vietnam", MinDigits: 9, MaxDigits: 9, LeadingDigits: "3579"},
}

// defaultMobileRule applies to a dial code with no entry above. E.164 caps a
// full number at 15 digits including the dial code, so this is deliberately
// wide: it rejects junk without pretending to know the country's format.
var defaultMobileRule = mobileRule{MinDigits: 6, MaxDigits: 14}

// isoToDialCode covers the countries in mobileRules, so a client that sends
// "IN" instead of "+91" is understood.
var isoToDialCode = map[string]string{
	"IN": "91", "US": "1", "CA": "1", "GB": "44", "UK": "44",
	"AE": "971", "SA": "966", "QA": "974", "KW": "965", "OM": "968",
	"BH": "973", "SG": "65", "MY": "60", "AU": "61", "NZ": "64",
	"NP": "977", "LK": "94", "BD": "880", "PK": "92", "ZA": "27",
	"NG": "234", "KE": "254", "DE": "49", "FR": "33", "IT": "39",
	"RU": "7", "KZ": "7", "CN": "86", "JP": "81", "ID": "62",
	"PH": "63", "TH": "66", "VN": "84",
}

// defaultDialCode is used when the request carries no country_code. The app
// has always sent Indian numbers without one, and users.countryCode holds
// "+91" or "91" for every existing account.
const defaultDialCode = "91"

var (
	dialCodePattern = regexp.MustCompile(`^\+?[0-9]{1,4}$`)
	isoCodePattern  = regexp.MustCompile(`^[A-Za-z]{2}$`)
	digitsOnly      = regexp.MustCompile(`^[0-9]+$`)

	mobileSeparators = strings.NewReplacer(
		" ", "", "-", "", "(", "", ")", "", ".", "",
	)
)

// normaliseDialCode turns whatever the client sent for country_code into a
// bare dial code ("91"). An empty value falls back to defaultDialCode.
func normaliseDialCode(countryCode string) (string, error) {

	trimmed := strings.TrimSpace(countryCode)

	if trimmed == "" {
		return defaultDialCode, nil
	}

	if isoCodePattern.MatchString(trimmed) {

		dial, ok := isoToDialCode[strings.ToUpper(trimmed)]

		if !ok {
			return "", fmt.Errorf(
				"country code %q is not recognised, please send a dial code such as +91",
				trimmed,
			)
		}

		return dial, nil
	}

	if !dialCodePattern.MatchString(trimmed) {
		return "", fmt.Errorf(
			"country code %q is not valid, please send a dial code such as +91",
			trimmed,
		)
	}

	return strings.TrimPrefix(trimmed, "+"), nil
}

// nationalNumber strips the separators, the country's own dial code and a
// trunk zero, leaving the digits the rule applies to.
func nationalNumber(dialCode string, mobile string) string {

	digits := mobileSeparators.Replace(strings.TrimSpace(mobile))
	digits = strings.TrimPrefix(digits, "+")

	// "+919829012345" and "919829012345" both carry the dial code; only strip
	// it when what remains could still be a whole number, so a national number
	// that happens to begin with its own dial code is left alone.
	if trimmed := strings.TrimPrefix(digits, dialCode); trimmed != digits {

		if len(trimmed) >= defaultMobileRule.MinDigits {
			digits = trimmed
		}
	}

	return strings.TrimPrefix(digits, "0")
}

// validateMobileForCountry checks the number against the rule for its country
// code and returns the number trimmed.
//
// The value comes back as the caller sent it (whitespace aside) rather than
// normalised: the OTP row is looked up by an exact match on the stored
// contactNo, so rewriting the number here would fail to find a row that
// registration wrote in the client's own format.
func validateMobileForCountry(countryCode string, mobile string) (string, error) {

	trimmed := strings.TrimSpace(mobile)

	if trimmed == "" {
		return "", errors.New("mobile number is required")
	}

	dialCode, err := normaliseDialCode(countryCode)

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

	national := nationalNumber(dialCode, trimmed)

	if !digitsOnly.MatchString(national) {
		return "", errors.New("mobile number must contain digits only")
	}

	rule, known := mobileRules[dialCode]

	if !known {
		rule = defaultMobileRule
	}

	if len(national) < rule.MinDigits || len(national) > rule.MaxDigits {
		return "", errors.New(mobileLengthMessage(dialCode, rule))
	}

	if rule.LeadingDigits != "" &&
		!strings.ContainsAny(national[0:1], rule.LeadingDigits) {

		return "", fmt.Errorf(
			"%s mobile number must start with %s",
			ruleLabel(dialCode, rule),
			humanDigitList(rule.LeadingDigits),
		)
	}

	return trimmed, nil
}

// ruleLabel names the country in an error, falling back to the dial code for
// one that has no rule of its own.
func ruleLabel(dialCode string, rule mobileRule) string {

	if rule.Country != "" {
		return rule.Country
	}

	return "+" + dialCode
}

func mobileLengthMessage(dialCode string, rule mobileRule) string {

	if rule.MinDigits == rule.MaxDigits {

		return fmt.Sprintf(
			"%s mobile number must be %d digits",
			ruleLabel(dialCode, rule),
			rule.MinDigits,
		)
	}

	return fmt.Sprintf(
		"%s mobile number must be between %d and %d digits",
		ruleLabel(dialCode, rule),
		rule.MinDigits,
		rule.MaxDigits,
	)
}

// humanDigitList renders "6789" as "6, 7, 8 or 9".
func humanDigitList(digits string) string {

	parts := strings.Split(digits, "")

	if len(parts) == 1 {
		return parts[0]
	}

	return strings.Join(parts[:len(parts)-1], ", ") + " or " + parts[len(parts)-1]
}
