package acme

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/mail"
	"os"
	"path/filepath"
	"strings"
	"time"

	"imuslab.com/zoraxy/mod/info/logger"
	"imuslab.com/zoraxy/mod/utils"
)

/*
	autorenew.go

	This script handle auto renew
*/

type AutoRenewConfig struct {
	Enabled      bool     //Automatic renew is enabled
	Email        string   //Email for acme
	RenewAll     bool     //Renew all or selective renew with the slice below
	FilesToRenew []string //If RenewAll is false, renew these certificate files
	DNSServers   string   // DNS servers
}

type AutoRenewer struct {
	ConfigFilePath    string
	CertFolder        string
	AcmeHandler       *ACMEHandler
	RenewerConfig     *AutoRenewConfig
	RenewTickInterval int64
	EarlyRenewDays    int //How many days before cert expire to renew certificate
	TickerstopChan    chan bool
	Logger            *logger.Logger //System wide logger
}

type ExpiredCerts struct {
	Domains  []string
	Filepath string
}

// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
	if renewCheckInterval == 0 {
		renewCheckInterval = 86400 //1 day
	}

	if earlyRenewDays == 0 {
		earlyRenewDays = 30
	}

	//Load the config file. If not found, create one
	if !utils.FileExists(config) {
		//Create one
		os.MkdirAll(filepath.Dir(config), 0775)
		newConfig := AutoRenewConfig{
			RenewAll:     true,
			FilesToRenew: []string{},
		}
		js, _ := json.MarshalIndent(newConfig, "", " ")
		err := os.WriteFile(config, js, 0775)
		if err != nil {
			return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
		}
	}

	renewerConfig := AutoRenewConfig{}
	content, err := os.ReadFile(config)
	if err != nil {
		return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
	}

	err = json.Unmarshal(content, &renewerConfig)
	if err != nil {
		return nil, errors.New("Malformed acme config file: " + err.Error())
	}

	//Create an Auto renew object
	thisRenewer := AutoRenewer{
		ConfigFilePath:    config,
		CertFolder:        certFolder,
		AcmeHandler:       AcmeHandler,
		RenewerConfig:     &renewerConfig,
		RenewTickInterval: renewCheckInterval,
		EarlyRenewDays:    earlyRenewDays,
		Logger:            logger,
	}

	thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)

	if thisRenewer.RenewerConfig.Enabled {
		//Start the renew ticker
		thisRenewer.StartAutoRenewTicker()

		//Check and renew certificate on startup
		go thisRenewer.CheckAndRenewCertificates()
	}

	return &thisRenewer, nil
}

func (a *AutoRenewer) Logf(message string, err error) {
	a.Logger.PrintAndLog("cert-renew", message, err)
}

func (a *AutoRenewer) StartAutoRenewTicker() {
	//Stop the previous ticker if still running
	if a.TickerstopChan != nil {
		a.TickerstopChan <- true
	}

	time.Sleep(1 * time.Second)

	ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
	done := make(chan bool)

	//Start the ticker to check and renew every x seconds
	go func(a *AutoRenewer) {
		for {
			select {
			case <-done:
				return
			case <-ticker.C:
				a.Logf("Check and renew certificates in progress", nil)
				a.CheckAndRenewCertificates()
			}
		}
	}(a)

	a.TickerstopChan = done
}

func (a *AutoRenewer) StopAutoRenewTicker() {
	if a.TickerstopChan != nil {
		a.TickerstopChan <- true
	}

	a.TickerstopChan = nil
}

// Handle update auto renew domains
// Set opr for different mode of operations
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
// opr = setAuto -> Set to use auto detect certificates and renew
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
	opr, err := utils.PostPara(r, "opr")
	if err != nil {
		utils.SendErrorResponse(w, "Operation not set")
		return
	}

	if opr == "setSelected" {
		files, err := utils.PostPara(r, "domains")
		if err != nil {
			utils.SendErrorResponse(w, "Domains is not defined")
			return
		}

		//Parse it int array of string
		matchingRuleFiles := []string{}
		err = json.Unmarshal([]byte(files), &matchingRuleFiles)
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}

		//Update the configs
		a.RenewerConfig.RenewAll = false
		a.RenewerConfig.FilesToRenew = matchingRuleFiles
		a.saveRenewConfigToFile()
		utils.SendOK(w)
	} else if opr == "setAuto" {
		a.RenewerConfig.RenewAll = true
		a.saveRenewConfigToFile()
		utils.SendOK(w)
	} else {
		utils.SendErrorResponse(w, "invalid operation given")
	}

}

// if auto renew all is true (aka auto scan), it will return []string{"*"}
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
	results := []string{}
	if a.RenewerConfig.RenewAll {
		//Auto pick which cert to renew.
		results = append(results, "*")
	} else {
		//Manually set the files to renew
		results = a.RenewerConfig.FilesToRenew
	}

	js, _ := json.Marshal(results)
	utils.SendJSONResponse(w, string(js))
}

func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
	//Load the current value
	js, _ := json.Marshal(a.RenewerConfig.RenewAll)
	utils.SendJSONResponse(w, string(js))
}

func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
	renewedDomains, err := a.CheckAndRenewCertificates()
	if err != nil {
		utils.SendErrorResponse(w, err.Error())
		return
	}

	message := "Domains renewed"
	if len(renewedDomains) == 0 {
		message = ("All certificates are up-to-date!")
	} else {
		message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
	}

	js, _ := json.Marshal(message)
	utils.SendJSONResponse(w, string(js))
}

// HandleAutoRenewEnable get and set the auto renew enable state
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		js, _ := json.Marshal(a.RenewerConfig.Enabled)
		utils.SendJSONResponse(w, string(js))
	} else if r.Method == http.MethodPost {
		val, err := utils.PostBool(r, "enable")
		if err != nil {
			utils.SendErrorResponse(w, "invalid or empty enable state")
		}
		if val {
			//Check if the email is not empty
			if a.RenewerConfig.Email == "" {
				utils.SendErrorResponse(w, "Email is not set")
				return
			}
			a.RenewerConfig.Enabled = true
			a.saveRenewConfigToFile()
			a.Logf("ACME auto renew enabled", nil)
			a.StartAutoRenewTicker()
		} else {
			a.RenewerConfig.Enabled = false
			a.saveRenewConfigToFile()
			a.Logf("ACME auto renew disabled", nil)
			a.StopAutoRenewTicker()
		}
	} else {
		http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
	}

}

func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		//Return the current email to user
		js, _ := json.Marshal(a.RenewerConfig.Email)
		utils.SendJSONResponse(w, string(js))
	} else if r.Method == http.MethodPost {
		email, err := utils.PostPara(r, "set")
		if err != nil {
			utils.SendErrorResponse(w, "invalid or empty email given")
			return
		}

		//Check if the email is valid
		_, err = mail.ParseAddress(email)
		if err != nil {
			utils.SendErrorResponse(w, err.Error())
			return
		}

		//Set the new config
		a.RenewerConfig.Email = email
		a.saveRenewConfigToFile()

		utils.SendOK(w)
	} else {
		http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
	}
}

// Check and renew certificates. This check all the certificates in the
// certificate folder and return a list of certs that is renewed in this call
// Return string array with length 0 when no cert is expired
func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
	certFolder := a.CertFolder
	files, err := os.ReadDir(certFolder)
	if err != nil {
		a.Logf("Read certificate store failed", err)
		return []string{}, err
	}

	expiredCertList := []*ExpiredCerts{}
	if a.RenewerConfig.RenewAll {
		//Scan and renew all
		for _, file := range files {
			if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
				//This is a public key file
				certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
				if err != nil {
					continue
				}
				if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
					//This cert is expired
					DNSName, err := ExtractDomains(certBytes)
					if err != nil {
						//Maybe self signed. Ignore this
						a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
						continue
					}

					expiredCertList = append(expiredCertList, &ExpiredCerts{
						Filepath: filepath.Join(certFolder, file.Name()),
						Domains:  DNSName,
					})
				}
			}
		}
	} else {
		//Only renew those in the list
		for _, file := range files {
			fileName := file.Name()
			certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
			if contains(a.RenewerConfig.FilesToRenew, certName) {
				//This is the one to auto renew
				certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
				if err != nil {
					continue
				}
				if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
					//This cert is expired
					DNSName, err := ExtractDomains(certBytes)
					if err != nil {
						//Maybe self signed. Ignore this
						a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
						continue
					}

					expiredCertList = append(expiredCertList, &ExpiredCerts{
						Filepath: filepath.Join(certFolder, file.Name()),
						Domains:  DNSName,
					})
				}
			}
		}
	}

	return a.renewExpiredDomains(expiredCertList)
}

// Close the auto renewer
func (a *AutoRenewer) Close() {
	if a.TickerstopChan != nil {
		a.TickerstopChan <- true
	}
}

// Renew the certificate by filename extract all DNS name from the
// certificate and renew them one by one by calling to the acmeHandler
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
	renewedCertFiles := []string{}
	for _, expiredCert := range certs {
		a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
		fileName := filepath.Base(expiredCert.Filepath)
		certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]

		// Load certificate info for ACME detail
		certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
		certInfo, err := LoadCertInfoJSON(certInfoFilename)
		if err != nil {
			a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)

			if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
				a.Logf("Extract issuer name for cert error, using default ca", err)
				certInfo = &CertificateInfoJSON{}
			} else {
				certInfo = &CertificateInfoJSON{AcmeName: CAName}
			}
		}

		//For upgrading config from older version of Zoraxy which don't have timeout
		if certInfo.PropTimeout == 0 {
			//Set default timeout
			certInfo.PropTimeout = 300
		}

		// Extract DNS servers from the certificate info if available
		var dnsServers string
		if len(certInfo.DNSServers) > 0 {
			dnsServers = strings.Join(certInfo.DNSServers, ",")
		}

		_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
		if err != nil {
			a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
		} else {
			a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
			renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
		}
	}

	return renewedCertFiles, nil
}

// Write the current renewer config to file
func (a *AutoRenewer) saveRenewConfigToFile() error {
	js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
	return os.WriteFile(a.ConfigFilePath, js, 0775)
}

// Handle update auto renew EAD configuration
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
	kid, err := utils.GetPara(r, "kid")
	if err != nil {
		utils.SendErrorResponse(w, "kid not set")
		return
	}

	hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
	if err != nil {
		utils.SendErrorResponse(w, "hmacEncoded not set")
		return
	}

	acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
	if err != nil {
		utils.SendErrorResponse(w, "acmeDirectoryURL not set")
		return
	}

	if !a.AcmeHandler.Database.TableExists("acme") {
		a.AcmeHandler.Database.NewTable("acme")
	}

	a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
	a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)

	utils.SendOK(w)

}

// Handle update auto renew DNS configuration
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
	dnsProvider, err := utils.PostPara(r, "dnsProvider")
	if err != nil {
		utils.SendErrorResponse(w, "dnsProvider not set")
		return
	}

	dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
	if err != nil {
		utils.SendErrorResponse(w, "dnsCredentials not set")
		return
	}

	filename, err := utils.PostPara(r, "filename")
	if err != nil {
		utils.SendErrorResponse(w, "filename not set")
		return
	}

	dnsServers, err := utils.PostPara(r, "dnsServers")
	if err != nil {
		dnsServers = ""
	}

	if !a.AcmeHandler.Database.TableExists("acme") {
		a.AcmeHandler.Database.NewTable("acme")
	}

	a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
	a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
	a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)

	utils.SendOK(w)

}
