package desec

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"time"
)

// ApexZone apex zone name.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#accessing-the-zone-apex
const ApexZone = "@"

// IgnoreFilter is a specific value used to ignore a filter field.
const IgnoreFilter = "#IGNORE#"

// RRSet DNS Record Set.
type RRSet struct {
	Name    string     `json:"name,omitempty"`
	Domain  string     `json:"domain,omitempty"`
	SubName string     `json:"subname,omitempty"`
	Type    string     `json:"type,omitempty"`
	Records []string   `json:"records"`
	TTL     int        `json:"ttl,omitempty"`
	Created *time.Time `json:"created,omitempty"`
	Touched *time.Time `json:"touched,omitempty"`
}

// RRSetFilter a RRSets filter.
type RRSetFilter struct {
	Type    string
	SubName string
}

// FilterRRSetOnlyOnType creates an RRSetFilter that ignore SubName.
func FilterRRSetOnlyOnType(t string) RRSetFilter {
	return RRSetFilter{
		Type:    t,
		SubName: IgnoreFilter,
	}
}

// FilterRRSetOnlyOnSubName creates an RRSetFilter that ignore Type.
func FilterRRSetOnlyOnSubName(n string) RRSetFilter {
	return RRSetFilter{
		Type:    IgnoreFilter,
		SubName: n,
	}
}

// RecordsService handles communication with the records related methods of the deSEC API.
//
// https://desec.readthedocs.io/en/latest/dns/rrsets.html
type RecordsService struct {
	client *Client
}

/*
	Domains
*/

// GetAll retrieving all RRSets in a zone.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
func (s *RecordsService) GetAll(ctx context.Context, domainName string, filter *RRSetFilter) ([]RRSet, error) {
	rrSets, _, err := s.GetAllPaginated(ctx, domainName, filter, "")
	if err != nil {
		return nil, err
	}

	return rrSets, nil
}

// GetAllPaginated retrieving all RRSets in a zone.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
func (s *RecordsService) GetAllPaginated(ctx context.Context, domainName string, filter *RRSetFilter, cursor string) ([]RRSet, *Cursors, error) {
	queryValues := url.Values{}

	if filter != nil {
		if filter.Type != IgnoreFilter {
			queryValues.Set("type", filter.Type)
		}

		if filter.SubName != IgnoreFilter {
			queryValues.Set("subname", filter.SubName)
		}
	}

	queryValues.Set("cursor", cursor)

	rrSets, cursors, err := s.getAll(ctx, domainName, queryValues)
	if err != nil {
		return nil, nil, err
	}

	return rrSets, cursors, nil
}

func (s *RecordsService) getAll(ctx context.Context, domainName string, query url.Values) ([]RRSet, *Cursors, error) {
	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, nil, err
	}

	if len(query) > 0 {
		req.URL.RawQuery = query.Encode()
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		return nil, nil, handleError(resp)
	}

	cursors, err := parseCursor(resp.Header)
	if err != nil {
		return nil, nil, err
	}

	var rrSets []RRSet

	err = handleResponse(resp, &rrSets)
	if err != nil {
		return nil, nil, err
	}

	return rrSets, cursors, nil
}

// Create creates a new RRSet.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#creating-a-tlsa-rrset
func (s *RecordsService) Create(ctx context.Context, rrSet RRSet) (*RRSet, error) {
	endpoint, err := s.client.createEndpoint("domains", rrSet.Domain, "rrsets")
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodPost, endpoint, rrSet)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusCreated {
		return nil, handleError(resp)
	}

	var newRRSet RRSet

	err = handleResponse(resp, &newRRSet)
	if err != nil {
		return nil, err
	}

	return &newRRSet, nil
}

/*
	Domains + subname + type
*/

// Get gets a RRSet.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-a-specific-rrset
func (s *RecordsService) Get(ctx context.Context, domainName, subName, recordType string) (*RRSet, error) {
	if subName == "" {
		subName = ApexZone
	}

	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		return nil, handleError(resp)
	}

	var rrSet RRSet

	err = handleResponse(resp, &rrSet)
	if err != nil {
		return nil, err
	}

	return &rrSet, nil
}

// Update updates RRSet (PATCH).
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#modifying-an-rrset
func (s *RecordsService) Update(ctx context.Context, domainName, subName, recordType string, rrSet RRSet) (*RRSet, error) {
	if subName == "" {
		subName = ApexZone
	}

	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodPatch, endpoint, rrSet)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	// when a RRSet is deleted (empty records)
	if resp.StatusCode == http.StatusNoContent {
		return nil, nil
	}

	if resp.StatusCode != http.StatusOK {
		return nil, handleError(resp)
	}

	var updatedRRSet RRSet

	err = handleResponse(resp, &updatedRRSet)
	if err != nil {
		return nil, err
	}

	return &updatedRRSet, nil
}

// Replace replaces a RRSet (PUT).
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#modifying-an-rrset
func (s *RecordsService) Replace(ctx context.Context, domainName, subName, recordType string, rrSet RRSet) (*RRSet, error) {
	if subName == "" {
		subName = ApexZone
	}

	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodPut, endpoint, rrSet)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	// when a RRSet is deleted (empty records)
	if resp.StatusCode == http.StatusNoContent {
		return nil, nil
	}

	if resp.StatusCode != http.StatusOK {
		return nil, handleError(resp)
	}

	var updatedRRSet RRSet

	err = handleResponse(resp, &updatedRRSet)
	if err != nil {
		return nil, err
	}

	return &updatedRRSet, nil
}

// Delete deletes a RRSet.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#deleting-an-rrset
func (s *RecordsService) Delete(ctx context.Context, domainName, subName, recordType string) error {
	if subName == "" {
		subName = ApexZone
	}

	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
	if err != nil {
		return fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusNoContent {
		return handleError(resp)
	}

	return nil
}

/*
	Bulk operations
*/

// UpdateMode the mode used to bulk update operations.
type UpdateMode string

const (
	// FullResource the full resource must be specified.
	FullResource UpdateMode = http.MethodPut
	// OnlyFields only fields you would like to modify need to be provided.
	OnlyFields UpdateMode = http.MethodPatch
)

// BulkCreate creates new RRSets in bulk.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-creation-of-rrsets
func (s *RecordsService) BulkCreate(ctx context.Context, domainName string, rrSets []RRSet) ([]RRSet, error) {
	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodPost, endpoint, rrSets)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusCreated {
		return nil, handleError(resp)
	}

	var newRRSets []RRSet

	err = handleResponse(resp, &newRRSets)
	if err != nil {
		return nil, err
	}

	return newRRSets, nil
}

// BulkUpdate updates RRSets in bulk.
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-modification-of-rrsets
func (s *RecordsService) BulkUpdate(ctx context.Context, mode UpdateMode, domainName string, rrSets []RRSet) ([]RRSet, error) {
	endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, string(mode), endpoint, rrSets)
	if err != nil {
		return nil, err
	}

	resp, err := s.client.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call API: %w", err)
	}

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

	if resp.StatusCode != http.StatusOK {
		return nil, handleError(resp)
	}

	var results []RRSet

	err = handleResponse(resp, &results)
	if err != nil {
		return nil, err
	}

	return results, nil
}

// BulkDelete deletes RRSets in bulk (uses FullResourceUpdateMode).
// https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-deletion-of-rrsets
func (s *RecordsService) BulkDelete(ctx context.Context, domainName string, rrSets []RRSet) error {
	deleteRRSets := make([]RRSet, len(rrSets))

	for i, rrSet := range rrSets {
		rrSet.Records = []string{}
		deleteRRSets[i] = rrSet
	}

	_, err := s.BulkUpdate(ctx, FullResource, domainName, deleteRRSets)
	if err != nil {
		return err
	}

	return nil
}
