package main

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

/*
	Usage

	git clone {repo_link_for_lego}
	go run extract.go
	//go run extract.go -- "win7"

*/

var legoProvidersSourceFolder string = "./lego/providers/dns/"
var outputDir string = "./acmedns"
var defTemplate string = `package acmedns
/*
	THIS MODULE IS GENERATED AUTOMATICALLY
	DO NOT EDIT THIS FILE
*/
import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/go-acme/lego/v4/challenge"
{{imports}}
)

//name is the DNS provider name, e.g. cloudflare or gandi
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64)(challenge.Provider, error){
	pgDuration := time.Duration(propagationTimeout) * time.Second
	plInterval := time.Duration(pollingInterval) * time.Second
	switch name {
	{{magic}}
	default:
		return nil, fmt.Errorf("unrecognized DNS provider: %s", name)
	}
}
`

type Field struct {
	Title    string
	Datatype string
}
type ProviderInfo struct {
	Name             string   //Name of this provider
	ConfigableFields []*Field //Field that shd be expose to user for filling in
	HiddenFields     []*Field //Fields that is usable but shd be hidden from user
}

func fileExists(filePath string) bool {
	_, err := os.Stat(filePath)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	// For other errors, you may handle them differently
	return false
}

// This function define the DNS not supported by zoraxy
func getExcludedDNSProviders() []string {
	return []string{
		"edgedns",      //Too complex data structure
		"exec",         //Not a DNS provider
		"httpreq",      //Not a DNS provider
		"hurricane",    //Multi-credentials arch
		"oraclecloud",  //Evil company
		"acmedns",      //Not a DNS provider
		"selectelv2",   //Not sure why not working with our code generator
		"designate",    //OpenStack, if you are using this you shd not be using zoraxy
		"mythicbeasts", //Module require url.URL, which cannot be automatically parsed

		//The following are incomaptible with Zoraxy due to dependencies issue,
		//might be resolved in future
		"corenetworks",
		"timewebcloud",
		"volcengine",
		"exoscale",
	}
}

// Exclude list for Windows build, due to limitations for lego versions
func getExcludedDNSProvidersNT61() []string {
	fmt.Println("Windows 7 support is deprecated, please consider upgrading to a newer version of Windows.")
	return append(getExcludedDNSProviders(), []string{"cpanel",
		"mailinabox",
		"shellrent",
	}...)
}

func isInSlice(str string, slice []string) bool {
	for _, s := range slice {
		if s == str {
			return true
		}
	}
	return false
}

func isExcludedDNSProvider(providerName string) bool {
	return isInSlice(providerName, getExcludedDNSProviders())
}

func isExcludedDNSProviderNT61(providerName string) bool {
	return isInSlice(providerName, getExcludedDNSProvidersNT61())
}

// extractConfigStruct extracts the name of the config struct and its content as plain text from a given source code.
func extractConfigStruct(sourceCode string) (string, string) {
	// Regular expression to match the struct declaration.
	structRegex := regexp.MustCompile(`type\s+([A-Za-z0-9_]+)\s+struct\s*{([^{}]*)}`)

	// Find the first match of the struct declaration.
	match := structRegex.FindStringSubmatch(sourceCode)
	if len(match) < 3 {
		return "", "" // No match found
	}

	// Extract the struct name and its content.
	structName := match[1]
	structContent := match[2]
	if structName != "Config" {
		allStructs := structRegex.FindAllStringSubmatch(sourceCode, 10)
		for _, thisStruct := range allStructs {
			//fmt.Println("Name => ", test[1])
			//fmt.Println("Content => ", test[2])

			if thisStruct[1] == "Config" {
				structName = "Config"
				structContent = thisStruct[2]
				break
			}
		}

		if structName != "Config" {
			panic("Unable to find Config for this provider")
		}
	}

	return structName, structContent
}

func main() {
	// A map of provider name to information on what can be filled
	extractedProviderList := map[string]*ProviderInfo{}
	buildForWindowsSeven := (len(os.Args) > 2 && os.Args[2] == "win7")

	//Search all providers
	providers, err := filepath.Glob(filepath.Join(legoProvidersSourceFolder, "/*"))
	if err != nil {
		panic(err)
	}

	//Create output folder if not exists
	err = os.MkdirAll(outputDir, 0775)
	if err != nil {
		panic(err)
	}

	generatedConvertcode := ""
	importList := ""
	for _, provider := range providers {
		providerName := filepath.Base(provider)

		if buildForWindowsSeven {
			if isExcludedDNSProviderNT61(providerName) {
				//Ignore this provider
				continue
			}
		} else {
			if isExcludedDNSProvider(providerName) {
				//Ignore this provider
				continue
			}
		}

		//Check if {provider_name}.go exists
		providerDef := filepath.Join(provider, providerName+".go")
		if !fileExists(providerDef) {
			continue
		}

		fmt.Println("Extracting config structure for: " + providerDef)
		providerSrc, err := os.ReadFile(providerDef)
		if err != nil {
			fmt.Println(err.Error())
			return
		}
		_, strctContent := extractConfigStruct(string(providerSrc))

		//Filter and write the content to json
		/*
			Example of stctContent (Note the tab prefix)

				Token              string
				PropagationTimeout time.Duration
				PollingInterval    time.Duration
				SequenceInterval   time.Duration
				HTTPClient         *http.Client
		*/
		strctContentLines := strings.Split(strctContent, "\n")
		configKeys := []*Field{}
		hiddenKeys := []*Field{}
		for _, lineDef := range strctContentLines {
			fields := strings.Fields(lineDef)
			if len(fields) < 2 || strings.HasPrefix(fields[0], "//") {
				//Ignore this line
				continue
			}

			//Filter out the fields that is not user-filled
			switch fields[1] {
			case "*url.URL":
				fallthrough
			case "string":
				//Add exception rule for gandi baseURL
				if (providerName == "gandi" || providerName == "gandiv5") && fields[0] == "BaseURL" {
					//Not useful stuff. Ignore this field
					continue
				}
				configKeys = append(configKeys, &Field{
					Title:    fields[0],
					Datatype: fields[1],
				})
			case "int":
				if fields[0] != "TTL" {
					configKeys = append(configKeys, &Field{
						Title:    fields[0],
						Datatype: fields[1],
					})
				} else if fields[0] == "TTL" {
					//haveTTLField = true
				} else {
					hiddenKeys = append(hiddenKeys, &Field{
						Title:    fields[0],
						Datatype: fields[1],
					})
				}
			case "bool":
				if fields[0] == "InsecureSkipVerify" || fields[0] == "SSLVerify" || fields[0] == "Debug" {
					configKeys = append(configKeys, &Field{
						Title:    fields[0],
						Datatype: fields[1],
					})
				} else {
					hiddenKeys = append(hiddenKeys, &Field{
						Title:    fields[0],
						Datatype: fields[1],
					})
				}
			case "time.Duration":
				if fields[0] == "PropagationTimeout" || fields[0] == "PollingInterval" {
					configKeys = append(configKeys, &Field{
						Title:    fields[0],
						Datatype: fields[1],
					})
				}

			default:
				//Not used fields
				hiddenKeys = append(hiddenKeys, &Field{
					Title:    fields[0],
					Datatype: fields[1],
				})
			}
		}
		fmt.Println(strctContent)

		extractedProviderList[providerName] = &ProviderInfo{
			Name:             providerName,
			ConfigableFields: configKeys,
			HiddenFields:     hiddenKeys,
		}

		//Generate the code for converting incoming json into target config
		codeSegment := `
	case "` + providerName + `":
		cfg := ` + providerName + `.NewDefaultConfig()
		err := json.Unmarshal([]byte(js), &cfg)
		if err != nil {
			return nil, err
		}
		cfg.PropagationTimeout = pgDuration
		cfg.PollingInterval = plInterval
		return ` + providerName + `.NewDNSProviderConfig(cfg)`
		generatedConvertcode += codeSegment
		importList += `	"github.com/go-acme/lego/v4/providers/dns/` + providerName + "\"\n"
	}

	js, err := json.MarshalIndent(extractedProviderList, "", " ")
	if err != nil {
		panic(err)
	}

	fullCodeSnippet := strings.ReplaceAll(defTemplate, "{{magic}}", generatedConvertcode)
	fullCodeSnippet = strings.ReplaceAll(fullCodeSnippet, "{{imports}}", importList)

	outJsonFilename := "providers.json"
	outGoFilename := "acmedns.go"
	if buildForWindowsSeven {
		outJsonFilename = "providers_nt61.json"
		outGoFilename = "acmedns_nt61.go"
	}
	os.WriteFile(filepath.Join(outputDir, outJsonFilename), js, 0775)
	os.WriteFile(filepath.Join(outputDir, outGoFilename), []byte(fullCodeSnippet), 0775)
	fmt.Println("Output written to file")
}
