package rimuhosting

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

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

// Base URL for the RimuHosting DNS services.
const (
	DefaultZonomiBaseURL      = "https://zonomi.com/app/dns/dyndns.jsp"
	DefaultRimuHostingBaseURL = "https://rimuhosting.com/dns/dyndns.jsp"
)

// Action names.
const (
	SetAction    = "SET"
	QueryAction  = "QUERY"
	DeleteAction = "DELETE"
)

// Client the RimuHosting/Zonomi client.
type Client struct {
	apiKey string

	HTTPClient *http.Client
	BaseURL    string
}

// NewClient Creates a RimuHosting/Zonomi client.
func NewClient(apiKey string) *Client {
	return &Client{
		apiKey:     apiKey,
		BaseURL:    DefaultZonomiBaseURL,
		HTTPClient: &http.Client{Timeout: 5 * time.Second},
	}
}

// FindTXTRecords Finds TXT records.
// ex:
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere
func (c Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, error) {
	action := ActionParameter{
		Action: QueryAction,
		Name:   domain,
		Type:   "TXT",
	}

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

	return resp.Actions.Action.Records, nil
}

// DoActions performs actions.
func (c Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DNSAPIResult, error) {
	if len(actions) == 0 {
		return nil, errors.New("no action")
	}

	resp := &DNSAPIResult{}

	if len(actions) == 1 {
		action := actionParameter{
			ActionParameter: actions[0],
			APIKey:          c.apiKey,
		}

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

	multi := c.toMultiParameters(actions)
	err := c.do(ctx, multi, resp)
	if err != nil {
		return nil, err
	}
	return resp, nil
}

func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter {
	multi := multiActionParameter{
		APIKey: c.apiKey,
	}

	for _, parameters := range params {
		multi.Action = append(multi.Action, parameters.Action)
		multi.Name = append(multi.Name, parameters.Name)
		multi.Type = append(multi.Type, parameters.Type)
		multi.Value = append(multi.Value, parameters.Value)
		multi.TTL = append(multi.TTL, parameters.TTL)
	}

	return multi
}

func (c Client) do(ctx context.Context, params, result any) error {
	baseURL, err := url.Parse(c.BaseURL)
	if err != nil {
		return err
	}

	v, err := querystring.Values(params)
	if err != nil {
		return err
	}

	exp := regexp.MustCompile(`(%5B)(%5D)(\d+)=`)
	baseURL.RawQuery = exp.ReplaceAllString(v.Encode(), "${1}${3}${2}=")

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

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

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

	if resp.StatusCode/100 != 2 {
		return parseError(req, resp)
	}

	if result == nil {
		return nil
	}

	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("unmarshaling %T error: %w: %s", result, err, string(raw))
	}

	return nil
}

func parseError(req *http.Request, resp *http.Response) error {
	raw, _ := io.ReadAll(resp.Body)

	errAPI := APIError{}
	err := xml.Unmarshal(raw, &errAPI)
	if err != nil {
		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
	}

	return errAPI
}

// NewAddRecordAction helper to create an action to add a TXT record.
func NewAddRecordAction(domain, content string, ttl int) ActionParameter {
	return ActionParameter{
		Action: SetAction,
		Name:   domain,
		Type:   "TXT",
		Value:  content,
		TTL:    ttl,
	}
}

// NewDeleteRecordAction helper to create an action to delete a TXT record.
func NewDeleteRecordAction(domain, content string) ActionParameter {
	return ActionParameter{
		Action: DeleteAction,
		Name:   domain,
		Type:   "TXT",
		Value:  content,
	}
}
