package hostingde

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

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

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

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

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) getRecords(domain string) ([]*record, error) {
	zc, err := hp.getZoneConfig(domain)
	if err != nil {
		return nil, err
	}

	records := []*record{}
	page := uint(1)
	for {
		params := request{
			Filter: filter{
				Field: "ZoneConfigId",
				Value: zc.ID,
			},
			Limit: 1000,
			Page:  page,
		}

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

		newRecords := []*record{}
		if err := json.Unmarshal(resp.Data, &newRecords); err != nil {
			return nil, err
		}

		records = append(records, newRecords...)

		if page >= resp.TotalPages {
			break
		}
		page++
	}
	return records, nil
}

func (hp *hostingdeProvider) updateRecords(domain string, create, del, mod diff.Changeset) error {
	zc, err := hp.getZoneConfig(domain)
	if err != nil {
		return err
	}

	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,
	}

	_, err = hp.get("dns", "zoneUpdate", params)
	if err != nil {
		return err
	}
	return 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) 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 := ioutil.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
}
