package internal

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

	"github.com/mitchellh/mapstructure"
)

const (
	authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php"
	apiEndpoint  = "https://kasapi.kasserver.com/soap/KasApi.php"
)

// Client a KAS server client.
type Client struct {
	login    string
	password string

	authEndpoint string
	apiEndpoint  string
	HTTPClient   *http.Client
	floodTime    time.Time
}

// NewClient creates a new Client.
func NewClient(login string, password string) *Client {
	return &Client{
		login:        login,
		password:     password,
		authEndpoint: authEndpoint,
		apiEndpoint:  apiEndpoint,
		HTTPClient:   &http.Client{Timeout: 10 * time.Second},
	}
}

// Authentication Creates a credential token.
// - sessionLifetime: Validity of the token in seconds.
// - sessionUpdateLifetime: with `true` the session is extended with every request.
func (c Client) Authentication(sessionLifetime int, sessionUpdateLifetime bool) (string, error) {
	sul := "N"
	if sessionUpdateLifetime {
		sul = "Y"
	}

	ar := AuthRequest{
		Login:                 c.login,
		AuthData:              c.password,
		AuthType:              "plain",
		SessionLifetime:       sessionLifetime,
		SessionUpdateLifetime: sul,
	}

	body, err := json.Marshal(ar)
	if err != nil {
		return "", fmt.Errorf("request marshal: %w", err)
	}

	payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))

	req, err := http.NewRequest(http.MethodPost, c.authEndpoint, bytes.NewReader(payload))
	if err != nil {
		return "", fmt.Errorf("request creation: %w", err)
	}

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("request execution: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		data, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("response read: %w", err)
	}

	var e KasAuthEnvelope
	decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
	err = decoder.Decode(&e)
	if err != nil {
		return "", fmt.Errorf("response xml decode: %w", err)
	}

	if e.Body.Fault != nil {
		return "", e.Body.Fault
	}

	return e.Body.KasAuthResponse.Return.Text, nil
}

// GetDNSSettings Reading out the DNS settings of a zone.
// - zone: host zone.
// - recordID: the ID of the resource record (optional).
func (c *Client) GetDNSSettings(credentialToken, zone, recordID string) ([]ReturnInfo, error) {
	requestParams := map[string]string{"zone_host": zone}

	if recordID != "" {
		requestParams["record_id"] = recordID
	}

	item, err := c.do(credentialToken, "get_dns_settings", requestParams)
	if err != nil {
		return nil, err
	}

	raw := getValue(item)

	var g GetDNSSettingsAPIResponse
	err = mapstructure.Decode(raw, &g)
	if err != nil {
		return nil, fmt.Errorf("response struct decode: %w", err)
	}

	c.updateFloodTime(g.Response.KasFloodDelay)

	return g.Response.ReturnInfo, nil
}

// AddDNSSettings Creation of a DNS resource record.
func (c *Client) AddDNSSettings(credentialToken string, record DNSRequest) (string, error) {
	item, err := c.do(credentialToken, "add_dns_settings", record)
	if err != nil {
		return "", err
	}

	raw := getValue(item)

	var g AddDNSSettingsAPIResponse
	err = mapstructure.Decode(raw, &g)
	if err != nil {
		return "", fmt.Errorf("response struct decode: %w", err)
	}

	c.updateFloodTime(g.Response.KasFloodDelay)

	return g.Response.ReturnInfo, nil
}

// DeleteDNSSettings Deleting a DNS Resource Record.
func (c *Client) DeleteDNSSettings(credentialToken, recordID string) (bool, error) {
	requestParams := map[string]string{"record_id": recordID}

	item, err := c.do(credentialToken, "delete_dns_settings", requestParams)
	if err != nil {
		return false, err
	}

	raw := getValue(item)

	var g DeleteDNSSettingsAPIResponse
	err = mapstructure.Decode(raw, &g)
	if err != nil {
		return false, fmt.Errorf("response struct decode: %w", err)
	}

	c.updateFloodTime(g.Response.KasFloodDelay)

	return g.Response.ReturnInfo, nil
}

func (c Client) do(credentialToken, action string, requestParams interface{}) (*Item, error) {
	time.Sleep(time.Until(c.floodTime))

	ar := KasRequest{
		Login:         c.login,
		AuthType:      "session",
		AuthData:      credentialToken,
		Action:        action,
		RequestParams: requestParams,
	}

	body, err := json.Marshal(ar)
	if err != nil {
		return nil, fmt.Errorf("request marshal: %w", err)
	}

	payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))

	req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(payload))
	if err != nil {
		return nil, fmt.Errorf("request creation: %w", err)
	}

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request execution: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		data, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("response read: %w", err)
	}

	var e KasAPIResponseEnvelope
	decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
	err = decoder.Decode(&e)
	if err != nil {
		return nil, fmt.Errorf("response xml decode: %w", err)
	}

	if e.Body.Fault != nil {
		return nil, e.Body.Fault
	}

	return e.Body.KasAPIResponse.Return, nil
}

func (c *Client) updateFloodTime(delay float64) {
	c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))
}

func getValue(item *Item) interface{} {
	switch {
	case item.Raw != "":
		v, _ := strconv.ParseBool(item.Raw)
		return v

	case item.Text != "":
		switch item.Type {
		case "xsd:string":
			return item.Text
		case "xsd:float":
			v, _ := strconv.ParseFloat(item.Text, 64)
			return v
		case "xsd:int":
			v, _ := strconv.ParseInt(item.Text, 10, 64)
			return v
		default:
			return item.Text
		}

	case item.Value != nil:
		return getValue(item.Value)

	case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array":
		var v []interface{}
		for _, i := range item.Items {
			v = append(v, getValue(i))
		}

		return v

	case len(item.Items) > 0:
		v := map[string]interface{}{}
		for _, i := range item.Items {
			v[getKey(i)] = getValue(i)
		}

		return v

	default:
		return ""
	}
}

func getKey(item *Item) string {
	if item.Key == nil {
		return ""
	}

	return item.Key.Text
}
