package mailinabox

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

// Record Represents a DNS record.
type Record struct {
	Name        string `json:"qname,omitempty"`
	Type        string `json:"rtype,omitempty"`
	Value       string `json:"value,omitempty"`
	Explanation string `json:"explanation,omitempty"`
}

// Zone Represents a DNS zone.
type Zone struct {
	Zone    string
	Records []Record
}

// Zones a slice of Zones.
// Use a custom unmarshalling method.
type Zones []Zone

// UnmarshalJSON customs unmarshalling.
func (z *Zones) UnmarshalJSON(data []byte) error {
	if string(data) == "null" || string(data) == `""` {
		return nil
	}

	var a []json.RawMessage
	if err := json.Unmarshal(data, &a); err != nil {
		return err
	}

	var all []Zone

	for _, message := range a {
		var b []json.RawMessage
		if err := json.Unmarshal(message, &b); err != nil {
			return err
		}

		zone := Zone{}

		if err := json.Unmarshal(b[0], &zone.Zone); err != nil {
			return err
		}

		if len(b) <= 1 {
			all = append(all, zone)
			continue
		}

		if err := json.Unmarshal(b[1], &zone.Records); err != nil {
			return err
		}

		all = append(all, zone)
	}

	*z = all

	return nil
}

// Nameserver Represents DNS nameservers.
type Nameserver struct {
	Hostnames []string `json:"hostnames"`
}

// DNSService DNS API.
// https://mailinabox.email/api-docs.html#tag/DNS
type DNSService service

// GetSecondaryNameserver Returns a list of nameserver hostnames.
// https://mailinabox.email/api-docs.html#operation/getDnsSecondaryNameserver
func (s *DNSService) GetSecondaryNameserver(ctx context.Context) ([]string, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "secondary-nameserver")

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

	var results Nameserver

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results.Hostnames, nil
}

// AddSecondaryNameserver Adds one or more secondary nameservers.
// https://mailinabox.email/api-docs.html#operation/addDnsSecondaryNameserver
func (s *DNSService) AddSecondaryNameserver(ctx context.Context, hostnames []string) (string, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "secondary-nameserver")

	data := url.Values{}
	data.Set("hostnames", strings.Join(hostnames, ","))

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

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// GetZones Returns an array of all managed top-level domains.
// https://mailinabox.email/api-docs.html#operation/getDnsZones
func (s *DNSService) GetZones(ctx context.Context) ([]string, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "zones")

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

	var results []string

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}

// GetZoneFile Returns a DNS zone file for a hostname.
// https://mailinabox.email/api-docs.html#operation/getDnsZonefile
func (s *DNSService) GetZoneFile(ctx context.Context, zone string) (string, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "zonefile", zone)

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

	var results string

	err = s.client.doJSON(req, &results)
	if err != nil {
		return "", err
	}

	return results, nil
}

// UpdateDNS Updates the DNS. Involves creating zone files and restarting `nsd`.
// https://mailinabox.email/api-docs.html#operation/updateDns
func (s *DNSService) UpdateDNS(ctx context.Context, force bool) (string, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "update")

	data := url.Values{}
	data.Set("force", boolToIntStr(force))

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

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// GetAllRecords Returns all custom DNS records.
// https://mailinabox.email/api-docs.html#operation/getDnsCustomRecords
func (s *DNSService) GetAllRecords(ctx context.Context) ([]Record, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom")

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

	var results []Record

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}

// GetRecords Returns all custom records for the specified query name and type.
// https://mailinabox.email/api-docs.html#operation/getDnsCustomRecordsForQNameAndType
func (s *DNSService) GetRecords(ctx context.Context, name, rType string) ([]Record, error) {
	if name == "" || rType == "" {
		return nil, errors.New("qname and rtype are required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name, rType)

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

	var results []Record

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}

// AddRecord Adds a custom DNS record for the specified query name and type.
// https://mailinabox.email/api-docs.html#operation/addDnsCustomRecord
func (s *DNSService) AddRecord(ctx context.Context, record Record) (string, error) {
	if record.Name == "" || record.Type == "" {
		return "", errors.New("qname and rtype are required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// UpdateRecord Updates an existing DNS custom record value for the specified qname and type.
// https://mailinabox.email/api-docs.html#operation/updateDnsCustomRecord
func (s *DNSService) UpdateRecord(ctx context.Context, record Record, value string) (string, error) {
	if record.Name == "" || record.Type == "" {
		return "", errors.New("qname and rtype are required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// RemoveRecord Removes a DNS custom record for the specified domain, type & value.
// https://mailinabox.email/api-docs.html#operation/removeDnsCustomRecord
func (s *DNSService) RemoveRecord(ctx context.Context, record Record) (string, error) {
	if record.Name == "" || record.Type == "" {
		return "", errors.New("qname and rtype are required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", record.Name, record.Type)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// GetARecords Returns all custom A records for the specified query name.
// https://mailinabox.email/api-docs.html#operation/getDnsCustomARecordsForQName
func (s *DNSService) GetARecords(ctx context.Context, name string) ([]Record, error) {
	if name == "" {
		return nil, errors.New("qname is required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)

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

	var results []Record

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}

// AddARecord Adds a custom DNS A record for the specified query name.
// https://mailinabox.email/api-docs.html#operation/addDnsCustomARecord
func (s *DNSService) AddARecord(ctx context.Context, name, value string) (string, error) {
	if name == "" {
		return "", errors.New("qname is required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// UpdateARecord Updates an existing DNS custom A record value for the specified qname.
// https://mailinabox.email/api-docs.html#operation/updateDnsCustomARecord
func (s *DNSService) UpdateARecord(ctx context.Context, name, value string) (string, error) {
	if name == "" {
		return "", errors.New("qname is required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// RemoveARecord Removes a DNS custom A record for the specified domain & value.
// https://mailinabox.email/api-docs.html#operation/removeDnsCustomARecord
func (s *DNSService) RemoveARecord(ctx context.Context, name, value string) (string, error) {
	if name == "" {
		return "", errors.New("qname is required")
	}

	endpoint := s.client.baseURL.JoinPath("admin", "dns", "custom", name)

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

	resp, err := s.client.doPlain(req)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(string(resp)), nil
}

// GetDump Returns all DNS records.
// https://mailinabox.email/api-docs.html#operation/getDnsDump
func (s *DNSService) GetDump(ctx context.Context) ([]Zone, error) {
	endpoint := s.client.baseURL.JoinPath("admin", "dns", "dump")

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

	var results Zones

	err = s.client.doJSON(req, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}
