package internal

import (
	"context"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)

// Default API endpoints.
const (
	DefaultBaseURL = "https://api.namecheap.com/xml.response"
	SandboxBaseURL = "https://api.sandbox.namecheap.com/xml.response"
)

// Client the API client for Namecheap.
type Client struct {
	apiUser  string
	apiKey   string
	clientIP string

	BaseURL    string
	HTTPClient *http.Client
}

// NewClient creates a new Client.
func NewClient(apiUser string, apiKey string, clientIP string) *Client {
	return &Client{
		apiUser:    apiUser,
		apiKey:     apiKey,
		clientIP:   clientIP,
		BaseURL:    DefaultBaseURL,
		HTTPClient: &http.Client{Timeout: 5 * time.Second},
	}
}

// GetHosts reads the full list of DNS host records.
// https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx
func (c *Client) GetHosts(ctx context.Context, sld, tld string) ([]Record, error) {
	request, err := c.newRequestGet(ctx, "namecheap.domains.dns.getHosts",
		addParam("SLD", sld),
		addParam("TLD", tld),
	)
	if err != nil {
		return nil, err
	}

	var ghr getHostsResponse
	err = c.do(request, &ghr)
	if err != nil {
		return nil, err
	}

	if len(ghr.Errors) > 0 {
		return nil, ghr.Errors[0]
	}

	return ghr.Hosts, nil
}

// SetHosts writes the full list of DNS host records .
// https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
func (c *Client) SetHosts(ctx context.Context, sld, tld string, hosts []Record) error {
	req, err := c.newRequestPost(ctx, "namecheap.domains.dns.setHosts",
		addParam("SLD", sld),
		addParam("TLD", tld),
		func(values url.Values) {
			for i, h := range hosts {
				ind := strconv.Itoa(i + 1)
				values.Add("HostName"+ind, h.Name)
				values.Add("RecordType"+ind, h.Type)
				values.Add("Address"+ind, h.Address)
				values.Add("MXPref"+ind, h.MXPref)
				values.Add("TTL"+ind, h.TTL)
			}
		},
	)
	if err != nil {
		return err
	}

	var shr setHostsResponse
	err = c.do(req, &shr)
	if err != nil {
		return err
	}

	if len(shr.Errors) > 0 {
		return shr.Errors[0]
	}
	if shr.Result.IsSuccess != "true" {
		return errors.New("setHosts failed")
	}

	return nil
}

func (c *Client) do(req *http.Request, result any) error {
	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return errutils.NewHTTPDoError(req, err)
	}

	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode >= http.StatusBadRequest {
		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
	}

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return errutils.NewReadResponseError(req, resp.StatusCode, err)
	}

	return xml.Unmarshal(raw, result)
}

func (c *Client) newRequestGet(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {
	query := c.makeQuery(cmd, params...)

	endpoint, err := url.Parse(c.BaseURL)
	if err != nil {
		return nil, err
	}

	endpoint.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("unable to create request: %w", err)
	}

	return req, nil
}

func (c *Client) newRequestPost(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {
	query := c.makeQuery(cmd, params...)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL, strings.NewReader(query.Encode()))
	if err != nil {
		return nil, fmt.Errorf("unable to create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	return req, nil
}

func (c *Client) makeQuery(cmd string, params ...func(url.Values)) url.Values {
	queryParams := make(url.Values)
	queryParams.Set("ApiUser", c.apiUser)
	queryParams.Set("ApiKey", c.apiKey)
	queryParams.Set("UserName", c.apiUser)
	queryParams.Set("Command", cmd)
	queryParams.Set("ClientIp", c.clientIP)

	for _, param := range params {
		param(queryParams)
	}

	return queryParams
}

func addParam(key, value string) func(url.Values) {
	return func(values url.Values) {
		values.Set(key, value)
	}
}
