package internal

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

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

// DefaultBaseURL is url to the XML-RPC api.
const DefaultBaseURL = "https://api.loopia.se/RPCSERV"

// Client the Loopia client.
type Client struct {
	apiUser     string
	apiPassword string

	BaseURL    string
	HTTPClient *http.Client
}

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

// AddTXTRecord adds a TXT record.
func (c *Client) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error {
	call := &methodCall{
		MethodName: "addZoneRecord",
		Params: []param{
			paramString{Value: c.apiUser},
			paramString{Value: c.apiPassword},
			paramString{Value: domain},
			paramString{Value: subdomain},
			paramStruct{
				StructMembers: []structMember{
					structMemberString{Name: "type", Value: "TXT"},
					structMemberInt{Name: "ttl", Value: ttl},
					structMemberInt{Name: "priority", Value: 0},
					structMemberString{Name: "rdata", Value: value},
					structMemberInt{Name: "record_id", Value: 0},
				},
			},
		},
	}
	resp := &responseString{}

	err := c.rpcCall(ctx, call, resp)
	if err != nil {
		return err
	}

	return checkResponse(resp.Value)
}

// RemoveTXTRecord removes a TXT record.
func (c *Client) RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error {
	call := &methodCall{
		MethodName: "removeZoneRecord",
		Params: []param{
			paramString{Value: c.apiUser},
			paramString{Value: c.apiPassword},
			paramString{Value: domain},
			paramString{Value: subdomain},
			paramInt{Value: recordID},
		},
	}
	resp := &responseString{}

	err := c.rpcCall(ctx, call, resp)
	if err != nil {
		return err
	}

	return checkResponse(resp.Value)
}

// GetTXTRecords gets TXT records.
func (c *Client) GetTXTRecords(ctx context.Context, domain, subdomain string) ([]RecordObj, error) {
	call := &methodCall{
		MethodName: "getZoneRecords",
		Params: []param{
			paramString{Value: c.apiUser},
			paramString{Value: c.apiPassword},
			paramString{Value: domain},
			paramString{Value: subdomain},
		},
	}
	resp := &recordObjectsResponse{}

	err := c.rpcCall(ctx, call, resp)

	return resp.Params, err
}

// RemoveSubdomain remove a sub-domain.
func (c *Client) RemoveSubdomain(ctx context.Context, domain, subdomain string) error {
	call := &methodCall{
		MethodName: "removeSubdomain",
		Params: []param{
			paramString{Value: c.apiUser},
			paramString{Value: c.apiPassword},
			paramString{Value: domain},
			paramString{Value: subdomain},
		},
	}
	resp := &responseString{}

	err := c.rpcCall(ctx, call, resp)
	if err != nil {
		return err
	}

	return checkResponse(resp.Value)
}

// rpcCall makes an XML-RPC call to Loopia's RPC endpoint by marshaling the data given in the call argument to XML
// and sending that via HTTP Post to Loopia.
// The response is then unmarshalled into the resp argument.
func (c *Client) rpcCall(ctx context.Context, call *methodCall, result response) error {
	req, err := newXMLRequest(ctx, c.BaseURL, call)
	if err != nil {
		return err
	}

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

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

	if resp.StatusCode != http.StatusOK {
		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
	}

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

	err = xml.Unmarshal(raw, result)
	if err != nil {
		return fmt.Errorf("unmarshal error: %w", err)
	}

	if result.faultCode() != 0 {
		return RPCError{
			FaultCode:   result.faultCode(),
			FaultString: strings.TrimSpace(result.faultString()),
		}
	}

	return nil
}

func newXMLRequest(ctx context.Context, endpoint string, payload any) (*http.Request, error) {
	body := new(bytes.Buffer)
	body.WriteString(xml.Header)

	encoder := xml.NewEncoder(body)
	encoder.Indent("", "  ")

	err := encoder.Encode(payload)
	if err != nil {
		return nil, err
	}

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

	req.Header.Set("Content-Type", "text/xml")

	return req, nil
}

func checkResponse(value string) error {
	switch v := strings.TrimSpace(value); v {
	case "OK":
		return nil
	case "AUTH_ERROR":
		return errors.New("authentication error")
	default:
		return fmt.Errorf("unknown error: %q", v)
	}
}
