// Package cloudxns implements a DNS provider for solving the DNS-01 challenge
// using cloudxns DNS.
package cloudxns

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/xenolf/lego/acmev2"
)

const cloudXNSBaseURL = "https://www.cloudxns.net/api2/"

// DNSProvider is an implementation of the acmev2.ChallengeProvider interface
type DNSProvider struct {
	apiKey    string
	secretKey string
}

// NewDNSProvider returns a DNSProvider instance configured for cloudxns.
// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY
// and CLOUDXNS_SECRET_KEY.
func NewDNSProvider() (*DNSProvider, error) {
	apiKey := os.Getenv("CLOUDXNS_API_KEY")
	secretKey := os.Getenv("CLOUDXNS_SECRET_KEY")
	return NewDNSProviderCredentials(apiKey, secretKey)
}

// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for cloudxns.
func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) {
	if apiKey == "" || secretKey == "" {
		return nil, fmt.Errorf("CloudXNS credentials missing")
	}

	return &DNSProvider{
		apiKey:    apiKey,
		secretKey: secretKey,
	}, nil
}

// Present creates a TXT record to fulfil the dns-01 challenge.
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
	fqdn, value, ttl := acmev2.DNS01Record(domain, keyAuth)
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return err
	}

	return c.addTxtRecord(zoneID, fqdn, value, ttl)
}

// CleanUp removes the TXT record matching the specified parameters.
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
	fqdn, _, _ := acmev2.DNS01Record(domain, keyAuth)
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return err
	}

	recordID, err := c.findTxtRecord(zoneID, fqdn)
	if err != nil {
		return err
	}

	return c.delTxtRecord(recordID, zoneID)
}

func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
	type Data struct {
		ID     string `json:"id"`
		Domain string `json:"domain"`
	}

	authZone, err := acmev2.FindZoneByFqdn(fqdn, acmev2.RecursiveNameservers)
	if err != nil {
		return "", err
	}

	result, err := c.makeRequest("GET", "domain", nil)
	if err != nil {
		return "", err
	}

	var domains []Data
	err = json.Unmarshal(result, &domains)
	if err != nil {
		return "", err
	}

	for _, data := range domains {
		if data.Domain == authZone {
			return data.ID, nil
		}
	}

	return "", fmt.Errorf("Zone %s not found in cloudxns for domain %s", authZone, fqdn)
}

func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) {
	result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil)
	if err != nil {
		return "", err
	}

	var records []cloudXNSRecord
	err = json.Unmarshal(result, &records)
	if err != nil {
		return "", err
	}

	for _, record := range records {
		if record.Host == acmev2.UnFqdn(fqdn) && record.Type == "TXT" {
			return record.RecordID, nil
		}
	}

	return "", fmt.Errorf("No existing record found for %s", fqdn)
}

func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error {
	id, err := strconv.Atoi(zoneID)
	if err != nil {
		return err
	}

	payload := cloudXNSRecord{
		ID:     id,
		Host:   acmev2.UnFqdn(fqdn),
		Value:  value,
		Type:   "TXT",
		LineID: 1,
		TTL:    ttl,
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	_, err = c.makeRequest("POST", "record", body)
	if err != nil {
		return err
	}

	return nil
}

func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error {
	_, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil)
	return err
}

func (c *DNSProvider) hmac(url, date, body string) string {
	sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey))
	return hex.EncodeToString(sum[:])
}

func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) {
	type APIResponse struct {
		Code    int             `json:"code"`
		Message string          `json:"message"`
		Data    json.RawMessage `json:"data,omitempty"`
	}

	url := cloudXNSBaseURL + uri
	req, err := http.NewRequest(method, url, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}

	requestDate := time.Now().Format(time.RFC1123Z)

	req.Header.Set("API-KEY", c.apiKey)
	req.Header.Set("API-REQUEST-DATE", requestDate)
	req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
	req.Header.Set("API-FORMAT", "json")

	resp, err := acmev2.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	var r APIResponse
	err = json.NewDecoder(resp.Body).Decode(&r)
	if err != nil {
		return nil, err
	}

	if r.Code != 1 {
		return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message)
	}
	return r.Data, nil
}

type cloudXNSRecord struct {
	ID       int    `json:"domain_id,omitempty"`
	RecordID string `json:"record_id,omitempty"`

	Host   string `json:"host"`
	Value  string `json:"value"`
	Type   string `json:"type"`
	LineID int    `json:"line_id,string"`
	TTL    int    `json:"ttl,string"`
}
