package cloudns

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"time"
)

// Api layer for ClouDNS
type cloudnsProvider struct {
	domainIndex      map[string]string
	nameserversNames []string
	creds            struct {
		id       string
		password string
		subid    string
	}
}

type requestParams map[string]string

type errorResponse struct {
	Status      string `json:"status"`
	Description string `json:"statusDescription"`
}

type nameserverRecord struct {
	Type string `json:"type"`
	Name string `json:"name"`
}

type nameserverResponse []nameserverRecord

type zoneRecord struct {
	Name   string `json:"name"`
	Type   string `json:"type"`
	Status string `json:"status"`
	Zone   string `json:"zone"`
}

type zoneResponse []zoneRecord

type domainRecord struct {
	ID               string `json:"id"`
	Type             string `json:"type"`
	Host             string `json:"host"`
	Target           string `json:"record"`
	Priority         string `json:"priority"`
	Weight           string `json:"weight"`
	Port             string `json:"port"`
	Service          string `json:"service"`
	Protocol         string `json:"protocol"`
	TTL              string `json:"ttl"`
	Status           int8   `json:"status"`
	CaaFlag          string `json:"caa_flag,omitempty"`
	CaaTag           string `json:"caa_type,omitempty"`
	CaaValue         string `json:"caa_value,omitempty"`
	TlsaUsage        string `json:"tlsa_usage,omitempty"`
	TlsaSelector     string `json:"tlsa_selector,omitempty"`
	TlsaMatchingType string `json:"tlsa_matching_type,omitempty"`
	SshfpAlgorithm   string `json:"algorithm,omitempty"`
	SshfpFingerprint string `json:"fp_type,omitempty"`
	DsKeyTag         string `json:"key_tag,omitempty"`
	DsAlgorithm      string `json:"dsalgorithm,omitempty"`
	DsDigestType     string `json:"digest_type,omitempty"`
	DsDigest         string `json:"dsdigest,omitempty"`
}

type recordResponse map[string]domainRecord

var allowedTTLValues = []uint32{}

func (c *cloudnsProvider) fetchAvailableNameservers() error {
	c.nameserversNames = nil

	var bodyString, err = c.get("/dns/available-name-servers.json", requestParams{})
	if err != nil {
		return fmt.Errorf("failed fetching available nameservers list from ClouDNS: %s", err)
	}

	var nr nameserverResponse
	json.Unmarshal(bodyString, &nr)

	for _, nameserver := range nr {
		if nameserver.Type == "premium" {
			c.nameserversNames = append(c.nameserversNames, nameserver.Name)
		}

	}
	return nil
}

func (c *cloudnsProvider) fetchAvailableTTLValues(domain string) error {
	allowedTTLValues = nil
	params := requestParams{
		"domain-name": domain,
	}

	var bodyString, err = c.get("/dns/get-available-ttl.json", params)
	if err != nil {
		return fmt.Errorf("failed fetching available TTL values list from ClouDNS: %s", err)
	}

	json.Unmarshal(bodyString, &allowedTTLValues)
	return nil
}

func (c *cloudnsProvider) fetchDomainList() error {
	// FIXME(tlim): This should return nil if c.domainIndex != nil.  Then all
	// the callers won't have to check if c.domainIndex == nil before calling.

	c.domainIndex = map[string]string{}
	rowsPerPage := 100
	page := 1
	for {
		var dr zoneResponse
		params := requestParams{
			"page":          strconv.Itoa(page),
			"rows-per-page": strconv.Itoa(rowsPerPage),
		}
		endpoint := "/dns/list-zones.json"
		var bodyString, err = c.get(endpoint, params)
		if err != nil {
			return fmt.Errorf("failed fetching domain list from ClouDNS: %s", err)
		}
		json.Unmarshal(bodyString, &dr)

		for _, domain := range dr {
			c.domainIndex[domain.Name] = domain.Name
		}
		if len(dr) < rowsPerPage {
			break
		}
		page++
	}
	return nil
}

func (c *cloudnsProvider) createDomain(domain string) error {
	params := requestParams{
		"domain-name": domain,
		"zone-type":   "master",
	}
	if _, err := c.get("/dns/register.json", params); err != nil {
		return fmt.Errorf("failed create domain (ClouDNS): %s", err)
	}
	return nil
}

func (c *cloudnsProvider) createRecord(domainID string, rec requestParams) error {
	rec["domain-name"] = domainID
	if _, err := c.get("/dns/add-record.json", rec); err != nil { // here we add record
		return fmt.Errorf("failed create record (ClouDNS): %s", err)
	}
	return nil
}

func (c *cloudnsProvider) deleteRecord(domainID string, recordID string) error {
	params := requestParams{
		"domain-name": domainID,
		"record-id":   recordID,
	}
	if _, err := c.get("/dns/delete-record.json", params); err != nil {
		return fmt.Errorf("failed delete record (ClouDNS): %s", err)
	}
	return nil
}

func (c *cloudnsProvider) modifyRecord(domainID string, recordID string, rec requestParams) error {
	rec["domain-name"] = domainID
	rec["record-id"] = recordID
	if _, err := c.get("/dns/mod-record.json", rec); err != nil {
		return fmt.Errorf("failed update (ClouDNS): %s", err)
	}
	return nil
}

func (c *cloudnsProvider) getRecords(id string) ([]domainRecord, error) {
	params := requestParams{"domain-name": id}

	var bodyString, err = c.get("/dns/records.json", params)
	if err != nil {
		return nil, fmt.Errorf("failed fetching record list from ClouDNS: %s", err)
	}

	var dr recordResponse
	json.Unmarshal(bodyString, &dr)

	var records []domainRecord
	for _, rec := range dr {
		records = append(records, rec)
	}
	return records, nil
}

func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, error) {
	client := &http.Client{}
	req, _ := http.NewRequest("GET", "https://api.cloudns.net"+endpoint, nil)
	q := req.URL.Query()

	//TODO: Support  sub-auth-user https://asia.cloudns.net/wiki/article/42/
	// Add auth params
	q.Add("auth-id", c.creds.id)
	q.Add("auth-password", c.creds.password)
	q.Add("sub-auth-id", c.creds.subid)

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

	req.URL.RawQuery = q.Encode()

	// ClouDNS has a rate limit (not documented) of 10 request/second
	// so we do a very primitive rate-limiting here - delay every request for 100ms - so max. 10 requests/second ...
	time.Sleep(100 * time.Millisecond)
	resp, err := client.Do(req)
	if err != nil {
		return []byte{}, err
	}

	bodyString, _ := io.ReadAll(resp.Body)

	// Got error from API ?
	var errResp errorResponse
	err = json.Unmarshal(bodyString, &errResp)
	if err == nil {
		if errResp.Status == "Failed" {
			return bodyString, fmt.Errorf("ClouDNS API error: %s URL:%s%s ", errResp.Description, req.Host, req.URL.RequestURI())
		}
	}

	return bodyString, nil
}

func fixTTL(ttl uint32) uint32 {
	// if the TTL is larger than the largest allowed value, return the largest allowed value
	if ttl > allowedTTLValues[len(allowedTTLValues)-1] {
		return allowedTTLValues[len(allowedTTLValues)-1]
	}

	for _, v := range allowedTTLValues {
		if v >= ttl {
			return v
		}
	}

	return allowedTTLValues[0]
}
