package hostingde

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/StackExchange/dnscontrol/v4/pkg/diff"
	"golang.org/x/net/idna"
)

const endpoint = "%s/api/%s/v1/json/%s"

type hostingdeProvider struct {
	authToken       string
	ownerAccountID  string
	filterAccountID string
	baseURL         string
	nameservers     []string
	defaultSoa      soaValues
}

func (hp *hostingdeProvider) getDomainConfig(domain string) (*domainConfig, error) {
	params := request{
		Filter: &filter{
			Field: "domainName",
			Value: domain,
		},
	}

	resp, err := hp.get("domain", "domainsFind", params)
	if err != nil {
		return nil, fmt.Errorf("error getting domain info: %w", err)
	}

	domainConf := []*domainConfig{}
	if err := json.Unmarshal(resp.Data, &domainConf); err != nil {
		return nil, fmt.Errorf("error parsing response: %w", err)
	}

	if len(domainConf) == 0 {
		return nil, fmt.Errorf("could not get domain config: %s", domain)
	}

	return domainConf[0], nil
}

func (hp *hostingdeProvider) createZone(domain string) error {
	t, err := idna.ToASCII(domain)
	if err != nil {
		return err
	}

	records := []*record{}
	for _, ns := range hp.nameservers {
		records = append(records, &record{
			Name:    domain,
			Type:    "NS",
			Content: ns,
			TTL:     86400,
		})
	}

	params := request{
		ZoneConfig: &zoneConfig{
			Name: t,
			Type: "NATIVE",
		},
		Records: records,
	}

	_, err = hp.get("dns", "zoneCreate", params)
	if err != nil {
		return fmt.Errorf("error creating zone: %w", err)
	}
	return nil
}

func (hp *hostingdeProvider) getNameservers(domain string) ([]string, error) {
	t, err := idna.ToASCII(domain)
	if err != nil {
		return nil, err
	}

	domainConf, err := hp.getDomainConfig(t)
	if err != nil {
		return nil, fmt.Errorf("error getting domain config: %w", err)
	}

	nss := []string{}
	for _, ns := range domainConf.Nameservers {
		// Currently does not support glued IP addresses
		if len(ns.IPs) > 0 {
			return nil, fmt.Errorf("domain %s has glued IP addresses which are not supported", domain)
		}

		nss = append(nss, ns.Name)
	}

	return nss, nil
}

func (hp *hostingdeProvider) updateNameservers(nss []string, domain string) func() error {
	return func() error {
		domainConf, err := hp.getDomainConfig(domain)
		if err != nil {
			return err
		}

		nameservers := []nameserver{}
		for _, ns := range nss {
			nameservers = append(nameservers, nameserver{Name: ns})
		}

		domainConf.Nameservers = nameservers

		params := request{
			Domain: domainConf,
		}

		if _, err := hp.get("domain", "domainUpdate", params); err != nil {
			return err
		}
		return nil
	}
}

func (hp *hostingdeProvider) updateZone(zc *zoneConfig, options *dnsSecOptions, create, del, mod diff.Changeset) error {
	toAdd := []*record{}
	for _, c := range create {
		r := recordToNative(c.Desired)
		toAdd = append(toAdd, r)
	}

	toDelete := []*record{}
	for _, d := range del {
		r := recordToNative(d.Existing)
		r.ID = d.Existing.Original.(record).ID
		toDelete = append(toDelete, r)
	}

	toModify := []*record{}
	for _, m := range mod {
		r := recordToNative(m.Desired)
		r.ID = m.Existing.Original.(record).ID
		toModify = append(toModify, r)
	}

	params := request{
		ZoneConfig:      zc,
		RecordsToAdd:    toAdd,
		RecordsToDelete: toDelete,
		RecordsToModify: toModify,
		DNSSECOptions:   options,
	}

	_, err := hp.get("dns", "zoneUpdate", params)
	if err != nil {
		return err
	}
	return nil
}

func (hp *hostingdeProvider) getZone(domain string) (*zone, error) {
	t, err := idna.ToASCII(domain)
	if err != nil {
		return nil, err
	}

	params := request{
		Filter: &filter{
			Field: "ZoneName",
			Value: t,
		},
	}

	resp, err := hp.get("dns", "zonesFind", params)
	if err != nil {
		return nil, fmt.Errorf("could not get zone config: %w", err)
	}

	zones := []*zone{}
	if err := json.Unmarshal(resp.Data, &zones); err != nil {
		return nil, fmt.Errorf("could not parse response: %w", err)
	}

	if len(zones) == 0 {
		return nil, errZoneNotFound
	}

	return zones[0], nil
}

func (hp *hostingdeProvider) getZoneConfig(domain string) (*zoneConfig, error) {
	t, err := idna.ToASCII(domain)
	if err != nil {
		return nil, err
	}

	params := request{
		Filter: &filter{
			Field: "ZoneName",
			Value: t,
		},
	}

	resp, err := hp.get("dns", "zoneConfigsFind", params)
	if err != nil {
		return nil, fmt.Errorf("could not get zone config: %w", err)
	}

	zc := []*zoneConfig{}
	if err := json.Unmarshal(resp.Data, &zc); err != nil {
		return nil, fmt.Errorf("could not parse response: %w", err)
	}

	if len(zc) == 0 {
		return nil, errZoneNotFound
	}

	return zc[0], nil
}

func (hp *hostingdeProvider) getDNSSECOptions(zoneConfigID string) (*dnsSecOptions, error) {
	params := request{
		Filter: &filter{
			Field: "zoneConfigId",
			Value: zoneConfigID,
		},
	}

	resp, err := hp.get("dns", "dnsSecOptionsFind", params)
	if err != nil {
		return nil, fmt.Errorf("could not get dnssec options: %w", err)
	}

	dnsSecOptions := []*dnsSecOptions{}
	if err := json.Unmarshal(resp.Data, &dnsSecOptions); err != nil {
		return nil, fmt.Errorf("could not parse response: %w", err)
	}

	if len(dnsSecOptions) == 0 {
		return nil, nil
	}

	return dnsSecOptions[0], nil
}

func (hp *hostingdeProvider) dnsSecKeyModify(domain string, add []dnsSecEntry, remove []dnsSecEntry) error {
	params := request{
		DomainName: domain,
		Add:        add,
		Remove:     remove,
	}

	_, err := hp.get("domain", "dnsSecKeyModify", params)
	if err != nil {
		return err
	}
	return nil
}

func (hp *hostingdeProvider) get(service, method string, params request) (*responseData, error) {
	params.AuthToken = hp.authToken
	params.OwnerAccountID = hp.ownerAccountID
	reqBody, err := json.Marshal(params)
	if err != nil {
		return nil, fmt.Errorf("could not marshal request body: %w", err)
	}

	url := fmt.Sprintf(endpoint, hp.baseURL, service, method)
	resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
	if err != nil {
		return nil, fmt.Errorf("could not carry out request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("error occurred: %s", resp.Status)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("could not read response body: %w", err)
	}

	respData := &response{}
	if err := json.Unmarshal(bodyBytes, &respData); err != nil {
		return nil, fmt.Errorf("could not unmarshal response body: %w", err)
	}
	if len(respData.Errors) > 0 && respData.Status == "error" {
		return nil, fmt.Errorf("%+v", respData.Errors)
	}

	return respData.Response, nil
}

func (hp *hostingdeProvider) getAllZoneConfigs() ([]*zoneConfig, error) {
	params := request{
		Limit: 10000,
	}
	if hp.filterAccountID != "" {
		params.Filter = &filter{
			Field: "accountId",
			Value: hp.filterAccountID,
		}
	}

	resp, err := hp.get("dns", "zoneConfigsFind", params)
	if err != nil {
		return nil, fmt.Errorf("could not get zones: %w", err)
	}

	zc := []*zoneConfig{}
	if err := json.Unmarshal(resp.Data, &zc); err != nil {
		return nil, fmt.Errorf("could not parse response: %w", err)
	}

	return zc, nil
}
