package desec

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

// Domain a domain representation.
type Domain struct {
	Name       string      `json:"name,omitempty"`
	MinimumTTL int         `json:"minimum_ttl,omitempty"`
	Keys       []DomainKey `json:"keys,omitempty"`
	Created    *time.Time  `json:"created,omitempty"`
	Published  *time.Time  `json:"published,omitempty"`
	Touched    *time.Time  `json:"touched,omitempty"`
}

// DomainKey a domain key representation.
type DomainKey struct {
	DNSKey  string   `json:"dnskey,omitempty"`
	DS      []string `json:"ds,omitempty"`
	Flags   int      `json:"flags,omitempty"`
	KeyType string   `json:"keytype,omitempty"`
}

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

// Create creating a domain.
// https://desec.readthedocs.io/en/latest/dns/domains.html#creating-a-domain
func (s *DomainsService) Create(ctx context.Context, domainName string) (*Domain, error) {
	endpoint, err := s.client.createEndpoint("domains")
	if err != nil {
		return nil, fmt.Errorf("failed to create endpoint: %w", err)
	}

	req, err := s.client.newRequest(ctx, http.MethodPost, endpoint, Domain{Name: domainName})
	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 domain Domain

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

	return &domain, nil
}

// GetAll listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) GetAll(ctx context.Context) ([]Domain, error) {
	domains, _, err := s.GetAllPaginated(ctx, "")
	if err != nil {
		return nil, err
	}

	return domains, nil
}

// GetAllPaginated listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) GetAllPaginated(ctx context.Context, cursor string) ([]Domain, *Cursors, error) {
	queryValues := url.Values{}
	queryValues.Set("cursor", cursor)

	return s.getAll(ctx, queryValues)
}

// GetResponsible returns the responsible domain for a given DNS query name.
// https://desec.readthedocs.io/en/latest/dns/domains.html#identifying-the-responsible-domain-for-a-dns-name
func (s *DomainsService) GetResponsible(ctx context.Context, domainName string) (*Domain, error) {
	queryValues := url.Values{}
	queryValues.Set("owns_qname", domainName)

	domains, _, err := s.getAll(ctx, queryValues)
	if err != nil {
		return nil, err
	}

	if len(domains) == 0 {
		return nil, &NotFoundError{Detail: "no responsible domain found"}
	}

	return &domains[0], nil
}

// getAll listing domains.
// https://desec.readthedocs.io/en/latest/dns/domains.html#listing-domains
func (s *DomainsService) getAll(ctx context.Context, query url.Values) ([]Domain, *Cursors, error) {
	endpoint, err := s.client.createEndpoint("domains")
	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 domains []Domain

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

	return domains, cursors, nil
}

// Get retrieving a specific domain.
// https://desec.readthedocs.io/en/latest/dns/domains.html#retrieving-a-specific-domain
func (s *DomainsService) Get(ctx context.Context, domainName string) (*Domain, error) {
	endpoint, err := s.client.createEndpoint("domains", domainName)
	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 domains Domain

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

	return &domains, nil
}

// Delete deleting a domain.
// https://desec.readthedocs.io/en/latest/dns/domains.html#deleting-a-domain
func (s *DomainsService) Delete(ctx context.Context, domainName string) error {
	endpoint, err := s.client.createEndpoint("domains", domainName)
	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
}
