package hetzner

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

	"github.com/StackExchange/dnscontrol/v4/pkg/printer"
)

const (
	baseURL = "https://dns.hetzner.com/api/v1"
)

type hetznerProvider struct {
	apiKey             string
	zones              map[string]zone
	requestRateLimiter requestRateLimiter
}

func parseHeaderAsSeconds(header http.Header, headerName string, fallback time.Duration) (time.Duration, error) {
	retryAfter, err := parseHeaderAsInt(header, headerName, int64(fallback/time.Second))
	if err != nil {
		return 0, err
	}
	delay := time.Duration(retryAfter * int64(time.Second))
	return delay, nil
}

func parseHeaderAsInt(headers http.Header, headerName string, fallback int64) (int64, error) {
	v := headers.Get(headerName)
	if v == "" {
		return fallback, nil
	}
	if i, err := strconv.ParseInt(v, 10, 64); err == nil {
		return i, nil
	}
	return 0, fmt.Errorf("expected header %q to contain number, got %q", headerName, v)
}

func (api *hetznerProvider) bulkCreateRecords(records []record) error {
	request := bulkCreateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "POST", request, nil, nil)
}

func (api *hetznerProvider) bulkUpdateRecords(records []record) error {
	request := bulkUpdateRecordsRequest{
		Records: records,
	}
	return api.request("/records/bulk", "PUT", request, nil, nil)
}

func (api *hetznerProvider) createZone(name string) error {
	request := createZoneRequest{
		Name: name,
	}
	return api.request("/zones", "POST", request, nil, nil)
}

func (api *hetznerProvider) deleteRecord(record *record) error {
	url := fmt.Sprintf("/records/%s", record.ID)
	return api.request(url, "DELETE", nil, nil, nil)
}

func (api *hetznerProvider) getAllRecords(domain string) ([]record, error) {
	z, err := api.getZone(domain)
	if err != nil {
		return nil, err
	}
	page := 1
	var records []record
	for {
		response := getAllRecordsResponse{}
		url := fmt.Sprintf("/records?zone_id=%s&per_page=100&page=%d", z.ID, page)
		if err = api.request(url, "GET", nil, &response, nil); err != nil {
			return nil, fmt.Errorf("failed fetching zone records for %q: %w", domain, err)
		}
		if records == nil {
			records = make([]record, 0, response.Meta.Pagination.TotalEntries)
		}
		for _, r := range response.Records {
			if r.TTL == nil {
				r.TTL = &z.TTL
			}
			if r.Type == "SOA" {
				// SOA records are not available for editing, hide them.
				continue
			}
			records = append(records, r)
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	return records, nil
}

func (api *hetznerProvider) getAllZones() error {
	if api.zones != nil {
		return nil
	}
	var zones map[string]zone
	page := 1
	statusOK := func(code int) bool {
		switch code {
		case http.StatusOK:
			return true
		case http.StatusNotFound:
			// Accept a 404 when requesting the first page
			return page == 1
		default:
			return false
		}
	}
	for {
		response := getAllZonesResponse{}
		url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
		if err := api.request(url, "GET", nil, &response, statusOK); err != nil {
			return fmt.Errorf("failed fetching zones: %w", err)
		}
		if zones == nil {
			zones = make(map[string]zone, response.Meta.Pagination.TotalEntries)
		}
		for _, z := range response.Zones {
			zones[z.Name] = z
		}
		// meta.pagination may not be present. In that case LastPage is 0 and below the current page number.
		if page >= response.Meta.Pagination.LastPage {
			break
		}
		page++
	}
	api.zones = zones
	return nil
}

func (api *hetznerProvider) getZone(name string) (*zone, error) {
	if err := api.getAllZones(); err != nil {
		return nil, err
	}
	z, ok := api.zones[name]
	if !ok {
		return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
	}
	return &z, nil
}

func (api *hetznerProvider) request(endpoint string, method string, request interface{}, target interface{}, statusOK func(code int) bool) error {
	if statusOK == nil {
		statusOK = func(code int) bool {
			return code == http.StatusOK
		}
	}
	for {
		var requestBody io.Reader
		if request != nil {
			requestBodySerialised, err := json.Marshal(request)
			if err != nil {
				return err
			}
			requestBody = bytes.NewBuffer(requestBodySerialised)
		}
		req, err := http.NewRequest(method, baseURL+endpoint, requestBody)
		if err != nil {
			return err
		}
		req.Header.Add("Auth-API-Token", api.apiKey)

		api.requestRateLimiter.delayRequest()
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		cleanupResponseBody := func() {
			err2 := resp.Body.Close()
			if err2 != nil {
				printer.Printf("failed closing response body: %q\n", err2)
			}
		}

		retry, err := api.requestRateLimiter.handleResponse(resp)
		if err != nil {
			cleanupResponseBody()
			return err
		}
		if retry {
			cleanupResponseBody()
			continue
		}

		if !statusOK(resp.StatusCode) {
			data, _ := io.ReadAll(resp.Body)
			printer.Println(string(data))
			cleanupResponseBody()
			return fmt.Errorf("bad status code from HETZNER: %d not 200", resp.StatusCode)
		}
		if target == nil {
			cleanupResponseBody()
			return nil
		}
		err = json.NewDecoder(resp.Body).Decode(target)
		cleanupResponseBody()
		return err
	}
}

type requestRateLimiter struct {
	delay       time.Duration
	lastRequest time.Time
}

func (rrl *requestRateLimiter) delayRequest() {
	time.Sleep(time.Until(rrl.lastRequest.Add(rrl.delay)))

	// When not rate-limited, include network/server latency in delay.
	rrl.lastRequest = time.Now()
}

func (rrl *requestRateLimiter) handleResponse(resp *http.Response) (bool, error) {
	if resp.StatusCode == http.StatusTooManyRequests {
		printer.Printf("Rate-Limited. Consider contacting the Hetzner Support for raising your quota. URL: %q, Headers: %q\n", resp.Request.URL, resp.Header)

		retryAfter, err := parseHeaderAsSeconds(resp.Header, "Retry-After", time.Second)
		if err != nil {
			return false, err
		}
		rrl.delay = retryAfter

		// When rate-limited, exclude network/server latency from delay.
		rrl.lastRequest = time.Now()
		return true, nil
	}

	limit, err := parseHeaderAsInt(resp.Header, "Ratelimit-Limit", 1)
	if err != nil {
		return false, err
	}

	remaining, err := parseHeaderAsInt(resp.Header, "Ratelimit-Remaining", 1)
	if err != nil {
		return false, err
	}

	reset, err := parseHeaderAsSeconds(resp.Header, "Ratelimit-Reset", 0)
	if err != nil {
		return false, err
	}

	if remaining == 0 {
		// Quota exhausted. Wait until quota resets.
		rrl.delay = reset
	} else if remaining > limit/2 {
		// Burst through half of the quota, ...
		rrl.delay = 0
	} else {
		// ... then spread requests evenly throughout the window.
		rrl.delay = reset / time.Duration(remaining+1)
	}
	return false, nil
}
