package cloudflare

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

	"github.com/StackExchange/dnscontrol/models"
)

const (
	baseURL         = "https://api.cloudflare.com/client/v4/"
	zonesURL        = baseURL + "zones/"
	recordsURL      = zonesURL + "%s/dns_records/"
	singleRecordURL = recordsURL + "%s"
)

// get list of domains for account. Cache so the ids can be looked up from domain name
func (c *CloudflareApi) fetchDomainList() error {
	c.domainIndex = map[string]string{}
	c.nameservers = map[string][]string{}
	page := 1
	for {
		zr := &zoneResponse{}
		url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page)
		if err := c.get(url, zr); err != nil {
			return fmt.Errorf("Error fetching domain list from cloudflare: %s", err)
		}
		if !zr.Success {
			return fmt.Errorf("Error fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors))
		}
		for _, zone := range zr.Result {
			c.domainIndex[zone.Name] = zone.ID
			for _, ns := range zone.Nameservers {
				c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns)
			}
		}
		ri := zr.ResultInfo
		if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
			break
		}
		page++
	}
	return nil
}

// get all records for a domain
func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) {
	url := fmt.Sprintf(recordsURL, id)
	page := 1
	records := []*models.RecordConfig{}
	for {
		reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
		var data recordsResponse
		if err := c.get(reqURL, &data); err != nil {
			return nil, fmt.Errorf("Error fetching record list from cloudflare: %s", err)
		}
		if !data.Success {
			return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors))
		}
		for _, rec := range data.Result {
			records = append(records, rec.toRecord(domain))
		}
		ri := data.ResultInfo
		if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
			break
		}
		page++
	}
	return records, nil
}

// create a correction to delete a record
func (c *CloudflareApi) deleteRec(rec *cfRecord, domainID string) *models.Correction {
	return &models.Correction{
		Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
		F: func() error {
			endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID)
			req, err := http.NewRequest("DELETE", endpoint, nil)
			if err != nil {
				return err
			}
			c.setHeaders(req)
			_, err = handleActionResponse(http.DefaultClient.Do(req))
			return err
		},
	}
}

func (c *CloudflareApi) createZone(domainName string) (string, error) {
	type createZone struct {
		Name string `json:"name"`
	}
	var id string
	cz := &createZone{
		Name: domainName}
	buf := &bytes.Buffer{}
	encoder := json.NewEncoder(buf)
	if err := encoder.Encode(cz); err != nil {
		return "", err
	}
	req, err := http.NewRequest("POST", zonesURL, buf)
	if err != nil {
		return "", err
	}
	c.setHeaders(req)
	id, err = handleActionResponse(http.DefaultClient.Do(req))
	return id, err
}

func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction {
	type createRecord struct {
		Name     string `json:"name"`
		Type     string `json:"type"`
		Content  string `json:"content"`
		TTL      uint32 `json:"ttl"`
		Priority uint16 `json:"priority"`
	}
	var id string
	content := rec.Target
	if rec.Metadata[metaOriginalIP] != "" {
		content = rec.Metadata[metaOriginalIP]
	}
	prio := ""
	if rec.Type == "MX" {
		prio = fmt.Sprintf(" %d ", rec.Priority)
	}
	arr := []*models.Correction{{
		Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.Name, rec.Type, rec.TTL, prio, content),
		F: func() error {

			cf := &createRecord{
				Name:     rec.Name,
				Type:     rec.Type,
				TTL:      rec.TTL,
				Content:  content,
				Priority: rec.Priority,
			}
			endpoint := fmt.Sprintf(recordsURL, domainID)
			buf := &bytes.Buffer{}
			encoder := json.NewEncoder(buf)
			if err := encoder.Encode(cf); err != nil {
				return err
			}
			req, err := http.NewRequest("POST", endpoint, buf)
			if err != nil {
				return err
			}
			c.setHeaders(req)
			id, err = handleActionResponse(http.DefaultClient.Do(req))
			return err
		},
	}}
	if rec.Metadata[metaProxy] != "off" {
		arr = append(arr, &models.Correction{
			Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.Name, rec.Type, rec.TTL, rec.Target),
			F:   func() error { return c.modifyRecord(domainID, id, true, rec) },
		})
	}
	return arr
}

func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error {
	if domainID == "" || recID == "" {
		return fmt.Errorf("Cannot modify record if domain or record id are empty.")
	}
	type record struct {
		ID       string `json:"id"`
		Proxied  bool   `json:"proxied"`
		Name     string `json:"name"`
		Type     string `json:"type"`
		Content  string `json:"content"`
		Priority uint16 `json:"priority"`
		TTL      uint32 `json:"ttl"`
	}
	r := record{recID, proxied, rec.Name, rec.Type, rec.Target, rec.Priority, rec.TTL}
	endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
	buf := &bytes.Buffer{}
	encoder := json.NewEncoder(buf)
	if err := encoder.Encode(r); err != nil {
		return err
	}
	req, err := http.NewRequest("PUT", endpoint, buf)
	if err != nil {
		return err
	}
	c.setHeaders(req)
	_, err = handleActionResponse(http.DefaultClient.Do(req))
	return err
}

// common error handling for all action responses
func handleActionResponse(resp *http.Response, err error) (id string, e error) {
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	result := &basicResponse{}
	decoder := json.NewDecoder(resp.Body)
	if err = decoder.Decode(result); err != nil {
		return "", fmt.Errorf("Unknown error. Status code: %d", resp.StatusCode)
	}
	if resp.StatusCode != 200 {
		return "", fmt.Errorf(stringifyErrors(result.Errors))
	}
	return result.Result.ID, nil
}

func (c *CloudflareApi) setHeaders(req *http.Request) {
	req.Header.Set("X-Auth-Key", c.ApiKey)
	req.Header.Set("X-Auth-Email", c.ApiUser)
}

// generic get handler. makes request and unmarshalls response to given interface
func (c *CloudflareApi) get(endpoint string, target interface{}) error {
	req, err := http.NewRequest("GET", endpoint, nil)
	if err != nil {
		return err
	}
	c.setHeaders(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("Bad status code from cloudflare: %d not 200.", resp.StatusCode)
	}
	decoder := json.NewDecoder(resp.Body)
	return decoder.Decode(target)
}

func stringifyErrors(errors []interface{}) string {
	dat, err := json.Marshal(errors)
	if err != nil {
		return "???"
	}
	return string(dat)
}

type recordsResponse struct {
	basicResponse
	Result     []*cfRecord `json:"result"`
	ResultInfo pagingInfo  `json:"result_info"`
}
type basicResponse struct {
	Success  bool          `json:"success"`
	Errors   []interface{} `json:"errors"`
	Messages []interface{} `json:"messages"`
	Result   struct {
		ID string `json:"id"`
	} `json:"result"`
}

type zoneResponse struct {
	basicResponse
	Result []struct {
		ID          string   `json:"id"`
		Name        string   `json:"name"`
		Nameservers []string `json:"name_servers"`
	} `json:"result"`
	ResultInfo pagingInfo `json:"result_info"`
}

type pagingInfo struct {
	Page       int `json:"page"`
	PerPage    int `json:"per_page"`
	Count      int `json:"count"`
	TotalCount int `json:"total_count"`
}