package dynadot

import (
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
)

// API layer for Dynadot

type dynadotProvider struct {
	key string
}

type requestParams map[string]string

type header struct {
	SuccessCode int    `xml:"SuccessCode"`
	Status      string `xml:"Status"`
	Error       string `xml:"Error,omitempty"`
}

type addNsResponse struct {
	XMLName     xml.Name `xml:"AddNsResponse"`
	AddNsHeader header   `xml:"AddNsHeader"`
}

type setNsResponse struct {
	XMLName     xml.Name `xml:"SetNsResponse"`
	SetNsHeader header   `xml:"SetNsHeader"`
}

type getNsResponse struct {
	XMLName     xml.Name  `xml:"GetNsResponse"`
	NsContent   nsContent `xml:"NsContent"`
	GetNsHeader header    `xml:"GetNsHeader"`
}

type nsContent struct {
	Host   []string `xml:"Host"`
	NsName string   `xml:"NsName"`
}

func (c *dynadotProvider) getNameservers(domain string) ([]string, error) {
	bodyString, err := c.get("get_ns", requestParams{"domain": domain})
	if err != nil {
		return []string{}, fmt.Errorf("failed NS list (Dynadot): %w", err)
	}
	var ns getNsResponse
	if err := xml.Unmarshal(bodyString, &ns); err != nil {
		return []string{}, fmt.Errorf("failed to unmarshal NS list (Dynadot): %w", err)
	}

	if ns.GetNsHeader.SuccessCode != 0 {
		return []string{}, fmt.Errorf("failed NS list (Dynadot): %s", ns.GetNsHeader.Error)
	}

	hosts := []string{}
	hosts = append(hosts, ns.NsContent.Host...)
	return hosts, nil
}

func (c *dynadotProvider) updateNameservers(ns []string, domain string) error {
	if len(ns) > 13 {
		return errors.New("failed NS update (Dynadot): only up to 13 nameservers are supported")
	}

	// Nameservers must first be added to the Dynadot account
	for _, host := range ns {
		b, err := c.get("add_ns", requestParams{"host": host})
		if err != nil {
			return fmt.Errorf("failed NS add (Dynadot): %w", err)
		}
		var resp addNsResponse
		err = xml.Unmarshal(b, &resp)
		if err != nil {
			return fmt.Errorf("failed NS add (Dynadot): %w", err)
		}

		if resp.AddNsHeader.SuccessCode != 0 {
			// No apparent way to get all existing entries on an account, so filter
			if strings.Contains(resp.AddNsHeader.Error, "already exists") {
				continue
			}
			return fmt.Errorf("failed NS add (Dynadot): %s", resp.AddNsHeader.Error)
		}
	}

	rec := requestParams{}
	rec["domain"] = domain
	// supported prams: ns0 - ns12
	for i, h := range ns {
		rec[fmt.Sprintf("%s%d", "ns", i)] = h
	}

	b, err := c.get("set_ns", rec)
	if err != nil {
		return fmt.Errorf("failed NS set (Dynadot): %w", err)
	}

	var resp setNsResponse
	err = xml.Unmarshal(b, &resp)
	if err != nil {
		return fmt.Errorf("failed NS add (Dynadot): %w", err)
	}

	if resp.SetNsHeader.SuccessCode != 0 {
		return fmt.Errorf("failed NS add (Dynadot): %s", resp.SetNsHeader.Error)
	}

	return nil
}

func (c *dynadotProvider) get(command string, params requestParams) ([]byte, error) {
	client := &http.Client{}
	req, _ := http.NewRequest(http.MethodGet, "https://api.dynadot.com/api3.xml", nil)
	q := req.URL.Query()

	q.Add("key", c.key)
	q.Add("command", command)

	for pName, pValue := range params {
		q.Add(pName, pValue)
	}

	req.URL.RawQuery = q.Encode()

	resp, err := client.Do(req)
	if err != nil {
		return []byte{}, err
	}

	return io.ReadAll(resp.Body)
}
