package utils

import (
	"encoding/json"
	"math/rand"
	"regexp"
	"net/http"
	"encoding/base64"
	"os"
	"strconv"
	"strings"
	"io/ioutil"
	"fmt"
	"sync"
	"time"
	"path/filepath"
	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/mem"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/net"
	"golang.org/x/net/publicsuffix"
)

var ConfigLock sync.Mutex

var BaseMainConfig Config
var MainConfig Config
var IsHTTPS = false
var NewVersionAvailable = false

var NeedsRestart = false

var UpdateAvailable = map[string]bool{}

var RestartHTTPServer func()
var ReBootstrapContainer func(string) error

var LetsEncryptErrors = []string{}

var CONFIGFOLDER = "/var/lib/cosmos/"

var DefaultConfig = Config{
	LoggingLevel: "INFO",
	NewInstall:   true,
	AutoUpdate:	  true,
	BlockedCountries: []string{
	},
	HTTPConfig: HTTPConfig{
		HTTPSCertificateMode:    "DISABLED",
		GenerateMissingAuthCert: true,
		HTTPPort:                "80",
		HTTPSPort:               "443",
		Hostname:                "0.0.0.0",
		ProxyConfig: ProxyConfig{
			Routes: []ProxyRouteConfig{},
		},
	},
	DockerConfig: DockerConfig{
		DefaultDataPath: "/usr",
	},
  MarketConfig: MarketConfig{
    Sources: []MarketSource{
		},
	},
  ConstellationConfig: ConstellationConfig{
    Enabled: false,
		DNSDisabled: false,
		DNSFallback: "8.8.8.8:53",
		DNSAdditionalBlocklists: []string{
			"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
      "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
      "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
      "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts",
		},
	},
}

func FileExists(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	Error("Reading file error: ", err)
	return false
}

func GetRootAppId() string {
	return "COSMOS"
}

func GetPrivateAuthKey() string {
	return MainConfig.HTTPConfig.AuthPrivateKey
}

func GetPublicAuthKey() string {
	return MainConfig.HTTPConfig.AuthPublicKey
}

var AlphaNumRunes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func GenerateRandomString(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = AlphaNumRunes[rand.Intn(len(AlphaNumRunes))]
	}
	return string(b)
}

type HTTPErrorResult struct {
	Status  string `json:"status"`
	Message string `json:"message"`
	Code    string `json:"code"`
}

func HTTPError(w http.ResponseWriter, message string, code int, userCode string) {
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(HTTPErrorResult{
		Status:  "error",
		Message: message,
		Code:    userCode,
	})
	Error("HTTP Request returned Error "+strconv.Itoa(code)+" : "+message, nil)
}

func SetBaseMainConfig(config Config) {
	SaveConfigTofile(config)
	LoadBaseMainConfig(config)
}

func ReadConfigFromFile() Config {
	configFile := GetConfigFileName()
	Log("Using config file: " + configFile)
	if CreateDefaultConfigFileIfNecessary() {
		LoadBaseMainConfig(DefaultConfig)
		return DefaultConfig
	}

	file, err := os.Open(configFile)
	if err != nil {
		Fatal("Opening Config File: ", err)
	}
	defer file.Close()

	decoder := json.NewDecoder(file)
	config := Config{}
	err = decoder.Decode(&config)

	// check file is not empty
	if err != nil {
		// check error is not empty 
		if err.Error() == "EOF" {
			Fatal("Reading Config File: File is empty.", err)
		}

		// get error string 
		errString := err.Error()

		// replace string in error
		m1 := regexp.MustCompile(`json: cannot unmarshal ([A-Za-z\.]+) into Go struct field ([A-Za-z\.]+) of type ([A-Za-z\.]+)`)
		errString = m1.ReplaceAllString(errString, "Invalid JSON in config file.\n > Field $2 is wrong.\n > Type is $1 Should be $3")
		Fatal("Reading Config File: " + errString, err)
	}

	return config
}

func LoadBaseMainConfig(config Config) {
	BaseMainConfig = config
	MainConfig = config

	// use ENV to overwrite configs

	if os.Getenv("COSMOS_HTTP_PORT") != "" {
		MainConfig.HTTPConfig.HTTPPort = os.Getenv("COSMOS_HTTP_PORT")
	}
	if os.Getenv("COSMOS_HTTPS_PORT") != "" {
		MainConfig.HTTPConfig.HTTPSPort = os.Getenv("COSMOS_HTTPS_PORT")
	}
	if os.Getenv("COSMOS_HOSTNAME") != "" {
		MainConfig.HTTPConfig.Hostname = os.Getenv("COSMOS_HOSTNAME")
	}
	if os.Getenv("COSMOS_HTTPS_MODE") != "" {
		MainConfig.HTTPConfig.HTTPSCertificateMode = os.Getenv("COSMOS_HTTPS_MODE")
	}
	if os.Getenv("COSMOS_GENERATE_MISSING_AUTH_CERT") != "" {
		MainConfig.HTTPConfig.GenerateMissingAuthCert = os.Getenv("COSMOS_GENERATE_MISSING_AUTH_CERT") == "true"
	}
	if os.Getenv("COSMOS_TLS_CERT") != "" {
		MainConfig.HTTPConfig.TLSCert = os.Getenv("COSMOS_TLS_CERT")
	}
	if os.Getenv("COSMOS_TLS_KEY") != "" {
		MainConfig.HTTPConfig.TLSKey = os.Getenv("COSMOS_TLS_KEY")
	}
	if os.Getenv("COSMOS_AUTH_PRIV_KEY") != "" {
		MainConfig.HTTPConfig.AuthPrivateKey = os.Getenv("COSMOS_AUTH_PRIVATE_KEY")
	}
	if os.Getenv("COSMOS_AUTH_PUBLIC_KEY") != "" {
		MainConfig.HTTPConfig.AuthPublicKey = os.Getenv("COSMOS_AUTH_PUBLIC_KEY")
	}
	if os.Getenv("COSMOS_LOG_LEVEL") != "" {
		MainConfig.LoggingLevel = (LoggingLevel)(os.Getenv("COSMOS_LOG_LEVEL"))
	}
	if os.Getenv("COSMOS_MONGODB") != "" {
		MainConfig.MongoDB = os.Getenv("COSMOS_MONGODB")
	}
	if os.Getenv("COSMOS_SERVER_COUNTRY") != "" {
		MainConfig.ServerCountry = os.Getenv("COSMOS_SERVER_COUNTRY")
	}
	if os.Getenv("COSMOS_CONFIG_FOLDER") != "" {
		Log("Overwriting config folder with " + os.Getenv("COSMOS_CONFIG_FOLDER"))
		CONFIGFOLDER = os.Getenv("COSMOS_CONFIG_FOLDER")
	}
	
	if MainConfig.DockerConfig.DefaultDataPath == "" {
		MainConfig.DockerConfig.DefaultDataPath = "/usr"
	}
	
	if MainConfig.ConstellationConfig.ConstellationHostname == "" {
		// if hostname is a domain add vpn. suffix otherwise use hostname
		if IsDomain(MainConfig.HTTPConfig.Hostname) {
			MainConfig.ConstellationConfig.ConstellationHostname = "vpn." + MainConfig.HTTPConfig.Hostname
		} else {
			MainConfig.ConstellationConfig.ConstellationHostname = MainConfig.HTTPConfig.Hostname
		}
	}
}

func GetMainConfig() Config {
	return MainConfig
}

func GetBaseMainConfig() Config {
	return BaseMainConfig
}

func Sanitize(s string) string {
	return strings.ToLower(strings.TrimSpace(s))
}

func SanitizeSafe(s string) string {
	return strings.TrimSpace(s)
}

func GetConfigFileName() string {
	if os.Getenv("COSMOS_CONFIG_FOLDER") != "" {
		CONFIGFOLDER = os.Getenv("COSMOS_CONFIG_FOLDER")
	} else if os.Getenv("HOSTNAME") != "" {
		CONFIGFOLDER = "/config/"
	}
	
	configFile := os.Getenv("CONFIG_FILE")

	if configFile == "" {
		configFile = CONFIGFOLDER + "cosmos.config.json"
	}

	return configFile
}

func CreateDefaultConfigFileIfNecessary() bool {
	configFile := GetConfigFileName()

	// get folder path
	folderPath := strings.Split(configFile, "/")
	folderPath = folderPath[:len(folderPath)-1]
	folderPathString := strings.Join(folderPath, "/")
	os.MkdirAll(folderPathString, os.ModePerm)

	if _, err := os.Stat(configFile); os.IsNotExist(err) {
		Log("Config file does not exist. Creating default config file.")
		file, err := os.Create(configFile)
		if err != nil {
			Fatal("Creating Default Config File", err)
		}
		defer file.Close()

		encoder := json.NewEncoder(file)
		encoder.SetIndent("", "  ")
		err = encoder.Encode(DefaultConfig)
		if err != nil {
			Fatal("Writing Default Config File", err)
		}

		return true
	}
	return false
}

func SaveConfigTofile(config Config) {
	configFile := GetConfigFileName()
	CreateDefaultConfigFileIfNecessary()

	file, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
	if err != nil {
		Fatal("Opening Config File", err)
	}
	defer file.Close()

	encoder := json.NewEncoder(file)
	encoder.SetIndent("", "  ")
	err = encoder.Encode(config)
	if err != nil {
		Fatal("Writing Config File", err)
	}

	Log("Config file saved.")
}

func RestartServer() {
	Log("Restarting server...")
	os.Exit(0)
}

func LetsEncryptValidOnly(hostnames []string, acceptWildcard bool) []string {
	wrongPattern := `^(localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|.*\.local)$`
	re, _ := regexp.Compile(wrongPattern)

	var validDomains []string
	for _, domain := range hostnames {
		if !re.MatchString(domain) && (acceptWildcard || !strings.Contains(domain, "*")) && !strings.Contains(domain, " ") && !strings.Contains(domain, ",") {
			validDomains = append(validDomains, domain)
		} else {
			Error("Invalid domain found in URLs: " + domain + " it was removed from the certificate to not break Let's Encrypt", nil)
		}
	}

	return validDomains
}

func GetAllHostnames(applyWildCard bool, removePorts bool) []string {
	mainHostname := GetMainConfig().HTTPConfig.Hostname
	OverrideWildcardDomains := GetMainConfig().HTTPConfig.OverrideWildcardDomains

	if removePorts {
		mainHostname = strings.Split(mainHostname, ":")[0]
	}

	hostnames := []string{
		mainHostname,
	}

	proxies := GetMainConfig().HTTPConfig.ProxyConfig.Routes
	for _, proxy := range proxies {
		if proxy.UseHost && proxy.Host != "" && !strings.Contains(proxy.Host, ",") && !strings.Contains(proxy.Host, " ") {
			if removePorts {
				hostnames = append(hostnames, strings.Split(proxy.Host, ":")[0])
			} else {
				hostnames = append(hostnames, proxy.Host)
			}
		}
	}

	// remove doubles
	seen := make(map[string]bool)
	uniqueHostnames := []string{}
	for _, hostname := range hostnames {
		if _, ok := seen[hostname]; !ok {
			seen[hostname] = true
			uniqueHostnames = append(uniqueHostnames, hostname)
		}
	}

	if applyWildCard && MainConfig.HTTPConfig.UseWildcardCertificate {
		bareMainHostname, _ := publicsuffix.EffectiveTLDPlusOne(mainHostname)

		Debug("bareMainHostname: " + bareMainHostname)

		filteredHostnames := []string{
			bareMainHostname,
			"*." + bareMainHostname,
		}

		if(OverrideWildcardDomains != "") {
			filteredHostnames = strings.Split(OverrideWildcardDomains, ",")
		}

		for _, hostname := range uniqueHostnames {
			if hostname != bareMainHostname && !strings.HasSuffix(hostname, "." + bareMainHostname) {
				filteredHostnames = append(filteredHostnames, hostname)
			}
		}

		uniqueHostnames = filteredHostnames
	}

	return uniqueHostnames
}

func GetAvailableRAM() uint64 {
	vmStat, err := mem.VirtualMemory()
	if err != nil {
		panic(err)
	}

	// Use total available memory as an approximation
	return vmStat.Available
}

func StringArrayEquals(a []string, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	for _, value := range a {
		if !StringArrayContains(b, value) {
			return false
		}
	}

	for _, value := range b {
		if !StringArrayContains(a, value) {
			return false
		}
	}

	return true
}

func HasAnyNewItem(after []string, before []string) bool {
	for _, value := range after {
		if !StringArrayContains(before, value) {
			return true
		}
	}
	return false
}

func StringArrayContains(a []string, b string) bool {
	for _, value := range a {
		if value == b {
			return true
		}
	}
	return false
}

func GetServerURL() string {
	ServerURL := ""

	if IsHTTPS {
		ServerURL += "https://"
	} else {
		ServerURL += "http://"
	}

	ServerURL += MainConfig.HTTPConfig.Hostname

	if IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443" {
		ServerURL += ":" + MainConfig.HTTPConfig.HTTPSPort
	}
	if !IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80" {
		ServerURL += ":" + MainConfig.HTTPConfig.HTTPPort
	}

	return ServerURL + "/"
}

func ImageToBase64(path string) (string, error) {
	imageFile, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer imageFile.Close()

	imageData, err := ioutil.ReadAll(imageFile)
	if err != nil {
		return "", err
	}

	encodedData := base64.StdEncoding.EncodeToString(imageData)

	fileExt := strings.ToLower(filepath.Ext(path))
	var mimeType string
	switch fileExt {
	case ".jpg", ".jpeg":
		mimeType = "image/jpeg"
	case ".png":
		mimeType = "image/png"
	case ".gif":
		mimeType = "image/gif"
	case ".bmp":
		mimeType = "image/bmp"
	default:
		return "", fmt.Errorf("unsupported file format: %s", fileExt)
	}

	dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encodedData)
	return dataURI, nil
}

func Max(x, y int) int {
	if x < y {
		return y
	}
	return x
}

func GetCPUUsage() ([]float64) {
	percentages, _ := cpu.Percent(time.Second, false)
	return percentages
}

func GetRAMUsage() (uint64) {
	v, _ := mem.VirtualMemory()
	return v.Used
}

type DiskStatus struct {
	Path       string
	TotalBytes uint64
	UsedBytes  uint64
}

func GetDiskUsage() []DiskStatus {
	partitions, err := disk.Partitions(false)
	if err != nil {
		Error("Error getting disk partitions", err)
		return nil
	}

	var diskStatuses []DiskStatus

	for _, partition := range partitions {
		usageStat, err := disk.Usage(partition.Mountpoint)
		if err != nil {
			Error("Error getting disk usage", err)
			return nil
		}
		diskStatus := DiskStatus{
			Path:       partition.Mountpoint,
			TotalBytes: usageStat.Total,
			UsedBytes:  usageStat.Used,
		}
		diskStatuses = append(diskStatuses, diskStatus)
	}

	return diskStatuses
}

type NetworkStatus struct {
	BytesSent uint64
	BytesRecv  uint64
}

func GetNetworkUsage() NetworkStatus {
	initialStat, err := net.IOCounters(true)
	if err != nil {
		Error("Error getting network usage", err)
		return NetworkStatus{}
	}

	time.Sleep(1 * time.Second)

	finalStat, err := net.IOCounters(true)
	if err != nil {
		Error("Error getting network usage", err)
		return NetworkStatus{}
	}

	res := NetworkStatus{
		BytesSent: 0,
		BytesRecv: 0,
	}
	
	for i := range initialStat {
		res.BytesSent += finalStat[i].BytesSent - initialStat[i].BytesSent
		res.BytesRecv += finalStat[i].BytesRecv - initialStat[i].BytesRecv
	}

	return NetworkStatus{}
}

func DownloadFile(url string) (string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	
	return string(body), nil
}

func GetClientIP(req *http.Request) string {
	/*ip := req.Header.Get("X-Forwarded-For")
	if ip == "" {
		ip = req.RemoteAddr
	}*/
	return req.RemoteAddr
}

func IsDomain(domain string) bool {
	// contains . and at least a letter and no special characters invalid in a domain
	if strings.Contains(domain, ".") && strings.ContainsAny(domain, "abcdefghijklmnopqrstuvwxyz") && !strings.ContainsAny(domain, " !@#$%^&*()+=[]{}\\|;:'\",/<>?") {
		return true
	}
	return false
}