package faker

import (
	"fmt"
	"reflect"
	"time"

	"github.com/go-faker/faker/v4/pkg/interfaces"
)

var (
	browserTypes          = []string{"chrome", "firefox", "internetExplorer", "msedge", "opera", "safari"}
	windowsPlatformTokens = []string{"Windows NT 6.2", "Windows NT 6.1", "Windows NT 6.0", "Windows NT 5.2",
		"Windows NT 5.1", "Windows NT 5.01", "Windows NT 5.0", "Windows NT 4.0", "Windows 98; Win 9x 4.90",
		"Windows 98", "Windows 95", "Windows CE",
	}
	linuxProcessor = []string{"i686", "x86_64"}
	macProcessor   = []string{"Intel", "PPC", "U; Intel", "U; PPC"}
	// langCodes contains language codes extracted from https://gist.github.com/JamieMason/3748498.
	langCodes = []string{"af-ZA", "ar-AE", "ar-BH", "ar-DZ", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY",
		"ar-MA", "ar-OM", "ar-QA", "ar-SA", "ar-SY", "ar-TN", "ar-YE", "be-BY", "bg-BG", "ca-ES", "cs-CZ", "cy-GB",
		"da-DK", "de-AT", "de-DE", "de-CH", "de-LI", "de-LU", "dv-MV", "el-GR", "en-AU", "en-BZ", "en-CA", "en-GB",
		"en-IE", "en-JM", "en-NZ", "en-PH", "en-TT", "en-US", "en-ZA", "en-ZW", "es-AR", "es-BO", "es-CL", "es-CO",
		"es-CR", "es-DO", "es-EC", "es-ES", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY",
		"es-SV", "es-UY", "es-VE", "et-EE", "eu-ES", "fa-IR", "fi-FI", "fo-FO", "fr-BE", "fr-CA", "fr-FR", "fr-CH",
		"fr-LU", "fr-MC", "gl-ES", "gu-IN", "he-IL", "hi-IN", "hr-BA", "hr-HR", "hu-HU", "hy-AM", "id-ID", "is-IS",
		"it-CH", "it-IT", "ja-JP", "ka-GE", "kk-KZ", "kn-IN", "ko-KR", "ky-KG", "lt-LT", "lv-LV", "mi-NZ", "mk-MK",
		"mn-MN", "mr-IN", "ms-BN", "ms-MY", "mt-MT", "nb-NO", "nl-BE", "nl-NL", "nn-NO", "ns-ZA", "pa-IN", "pl-PL",
		"pt-BR", "pt-PT", "ro-RO", "ru-RU", "sa-IN", "se-FI", "se-NO", "se-SE", "sk-SK", "sl-SI", "sq-AL", "sv-FI",
		"sv-SE", "sw-KE", "ta-IN", "te-IN", "th-TH", "tn-ZA", "tr-TR", "tt-RU", "uk-UA", "ur-PK", "vi-VN", "xh-ZA",
		"zh-CN", "zh-HK", "zh-MO", "zh-SG", "zh-TW", "zu-ZA",
	}
)

func GetUserAgent() UserAgenter {
	ua := &UserAgent{}
	return ua
}

type UserAgenter interface {
	UserAgent(v reflect.Value) (interface{}, error)
}

// UserAgent implements logic loosely based on https://fakerphp.org/formatters/user-agent/.
type UserAgent struct{}

func (ua UserAgent) UserAgent(reflect.Value) (interface{}, error) {
	browserType := randomElementFromSliceString(browserTypes)
	switch browserType {
	case "chrome":
		return ua.chrome(), nil
	case "firefox":
		return ua.firefox(), nil
	case "internetExplorer":
		return ua.internetExplorer(), nil
	case "msedge":
		return ua.msEdge(), nil
	case "opera":
		return ua.opera(), nil
	case "safari":
		return ua.safari(), nil
	}
	return nil, fmt.Errorf(`unknown browser type: "%s"`, browserType)
}

// chrome generates a user agent string that resembles Google Chrome.
// Example: "Mozilla/5.0 (X11; Linux i686) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/69.0.3460.334 Safari/532.2"
func (ua UserAgent) chrome() string {
	chromeVer := fmt.Sprintf("%d.0.%d.%d",
		ua.intBetween(36, 127), ua.intBetween(0, 6000), ua.intBetween(0, 400),
	)
	platformToken := randomElementFromSliceString([]string{
		ua.linuxPlatformToken(),
		ua.macPlatformToken("_"),
		ua.windowsPlatformToken(),
	})
	safariType := randomElementFromSliceString([]string{"Safari", "Mobile Safari"})
	webKitVer := ua.webKitVersion(false)
	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Chrome/%s %s/%s",
		platformToken, webKitVer, chromeVer, safariType, webKitVer,
	)
}

// firefox generates a user agent string that resembles Firefox.
// Example: "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20130219 Firefox/58.0"
func (ua UserAgent) firefox() string {
	firefoxVer := fmt.Sprintf("%d.0", ua.intBetween(35, 129))
	platformToken := randomElementFromSliceString([]string{
		ua.linuxPlatformToken(),
		ua.macPlatformToken("."),
		ua.windowsPlatformToken(),
	})
	return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s) Gecko/%s Firefox/%s",
		platformToken, firefoxVer, ua.geckoVersion(), firefoxVer,
	)
}

// internetExplorer generates a user agent string that resembles Internet Explorer
// Example: "Mozilla/5.0 (compatible; MSIE 9.0; Windows 98; Win 9x 4.90; Trident/5.0)"
func (ua UserAgent) internetExplorer() string {
	return fmt.Sprintf("Mozilla/5.0 (compatible; MSIE %d.0; %s; Trident/%d.0)",
		ua.intBetween(5, 11), ua.windowsPlatformToken(), ua.intBetween(3, 7),
	)
}

// msEdge generates a user agent string that resembles MS Edge
// Example: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/533.0 (KHTML, like Gecko) Chrome/86.0.4685.32 Safari/533.0 Edg/86.0.4685.32"
func (ua UserAgent) msEdge() string {
	webKitVer := ua.webKitVersion(false)
	chromiumVer := fmt.Sprintf("%d.0.%d.%d", ua.intBetween(79, 99), ua.intBetween(4000, 4844), ua.intBetween(10, 99))
	platformToken := randomElementFromSliceString([]string{
		ua.linuxPlatformToken(),
		ua.windowsPlatformToken(),
		ua.macPlatformToken("_"),
	})
	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Chrome/%s Safari/%s Edg/%s",
		platformToken, webKitVer, chromiumVer, webKitVer, chromiumVer,
	)
}

// opera generates a user agent string that resembles Opera.
// Example: "Opera/8.40 (X11; Linux i686; zh-TW) Presto/2.12.181 Version/10.57"
func (ua UserAgent) opera() string {
	platformToken := randomElementFromSliceString([]string{
		ua.linuxPlatformToken(),
		ua.windowsPlatformToken(),
	})
	operaVer := fmt.Sprintf("%d.%d", ua.intBetween(8, 9), ua.intBetween(10, 99))
	prestoVer := fmt.Sprintf("2.%d.%d", ua.intBetween(8, 12), ua.intBetween(160, 355))
	ver := fmt.Sprintf("%d.%d", ua.intBetween(10, 12), ua.intBetween(0, 70))
	return fmt.Sprintf("Opera/%s (%s; %s) Presto/%s Version/%s",
		operaVer, platformToken, randomElementFromSliceString(langCodes), prestoVer, ver,
	)
}

// safari generates a user agent string that resembles Safari.
// Example: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_3; bg-BG) AppleWebKit/548.43.16 (KHTML, like Gecko) Version/7.5 Safari/548.43.16"
func (ua UserAgent) safari() string {
	webKitVer := ua.webKitVersion(true)
	safariVer := fmt.Sprintf("%d.%d", ua.intBetween(4, 17), ua.intBetween(0, 5))
	if ua.intBetween(0, 100) < 50 {
		safariVer = fmt.Sprintf("%s.%d", safariVer, ua.intBetween(1, 20))
	}
	platform := randomElementFromSliceString([]string{
		fmt.Sprintf("Windows; U; %s; %s", ua.windowsPlatformToken(), randomElementFromSliceString(langCodes)),
		fmt.Sprintf("%s; %s", ua.macPlatformToken("_"), randomElementFromSliceString(langCodes)),
	})

	return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/%s (KHTML, like Gecko) Version/%s Safari/%s",
		platform, webKitVer, safariVer, webKitVer,
	)
}

func (ua UserAgent) linuxPlatformToken() string {
	return fmt.Sprintf("X11; Linux %s", randomElementFromSliceString(linuxProcessor))
}

func (ua UserAgent) macPlatformToken(sep string) string {
	return fmt.Sprintf("Macintosh; %s Mac OS X %d%s%d%s%d",
		randomElementFromSliceString(macProcessor), ua.intBetween(10, 17), sep, ua.intBetween(5, 8), sep, ua.intBetween(0, 9),
	)
}

func (ua UserAgent) windowsPlatformToken() string {
	return randomElementFromSliceString(windowsPlatformTokens)
}

// webKitVersion generates a random string that resembles an AppleWebKit version string.
func (ua UserAgent) webKitVersion(withPatchVer bool) string {
	majorVer := ua.intBetween(531, 618)
	if withPatchVer {
		return fmt.Sprintf("%d.%d.%d", majorVer, ua.intBetween(1, 50), ua.intBetween(1, 20))
	}
	return fmt.Sprintf("%d.%d", majorVer, ua.intBetween(0, 10))
}

// geckoVersion generates a random string that resembles a Gecko version string.
func (ua UserAgent) geckoVersion() string {
	const YYYYMMDD = "20060102"
	minDate := time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC)
	maxDate := time.Now()
	randDuration := time.Duration(rand.Int63n(maxDate.Unix()-minDate.Unix())) * time.Second
	return minDate.Add(randDuration).Format(YYYYMMDD)
}

// intBetween generates a random integer in the range [min, max].
//
//nolint:revive
func (ua UserAgent) intBetween(min, max int) int {
	boundary := interfaces.RandomIntegerBoundary{Start: min, End: max + 1}
	return randomIntegerWithBoundary(boundary)
}
