package mythicbeasts

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"path"
	"strings"
	"time"
)

const (
	apiBaseURL  = "https://api.mythic-beasts.com/dns/v2"
	authBaseURL = "https://auth.mythic-beasts.com/login"
)

type authResponse struct {
	// The bearer token for use in API requests
	Token string `json:"access_token"`

	// The maximum lifetime of the token in seconds
	Lifetime int `json:"expires_in"`

	// The token type (must be 'bearer')
	TokenType string `json:"token_type"`

	Deadline time.Time `json:"-"`
}

type authResponseError struct {
	ErrorMsg         string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

func (a authResponseError) Error() string {
	return fmt.Sprintf("%s: %s", a.ErrorMsg, a.ErrorDescription)
}

type createTXTRequest struct {
	Records []createTXTRecord `json:"records"`
}

type createTXTRecord struct {
	Host string `json:"host"`
	TTL  int    `json:"ttl"`
	Type string `json:"type"`
	Data string `json:"data"`
}

type createTXTResponse struct {
	Added   int    `json:"records_added"`
	Removed int    `json:"records_removed"`
	Message string `json:"message"`
}

type deleteTXTResponse struct {
	Removed int    `json:"records_removed"`
	Message string `json:"message"`
}

// Logs into mythic beasts and acquires a bearer token for use in future API calls.
// https://www.mythic-beasts.com/support/api/auth#sec-obtaining-a-token
func (d *DNSProvider) login() error {
	d.muToken.Lock()
	defer d.muToken.Unlock()

	if d.token != nil && time.Now().Before(d.token.Deadline) {
		// Already authenticated, stop now
		return nil
	}

	req, err := http.NewRequest(http.MethodPost, d.config.AuthAPIEndpoint.String(), strings.NewReader("grant_type=client_credentials"))
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(d.config.UserName, d.config.Password)

	resp, err := d.config.HTTPClient.Do(req)
	if err != nil {
		return err
	}

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

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

	if resp.StatusCode != http.StatusOK {
		if resp.StatusCode < 400 || resp.StatusCode > 499 {
			return fmt.Errorf("login: unknown error in auth API: %d", resp.StatusCode)
		}

		// Returned body should be a JSON thing
		errResp := &authResponseError{}
		err = json.Unmarshal(body, errResp)
		if err != nil {
			return fmt.Errorf("login: error parsing error: %w", err)
		}

		return fmt.Errorf("login: %d: %w", resp.StatusCode, errResp)
	}

	authResp := authResponse{}
	err = json.Unmarshal(body, &authResp)
	if err != nil {
		return fmt.Errorf("login: error parsing response: %w", err)
	}

	if authResp.TokenType != "bearer" {
		return fmt.Errorf("login: received unexpected token type: %s", authResp.TokenType)
	}

	authResp.Deadline = time.Now().Add(time.Duration(authResp.Lifetime) * time.Second)
	d.token = &authResp

	// Success
	return nil
}

// https://www.mythic-beasts.com/support/api/dnsv2#ep-get-zoneszonerecords
func (d *DNSProvider) createTXTRecord(zone, leaf, value string) error {
	if d.token == nil {
		return fmt.Errorf("createTXTRecord: not logged in")
	}

	createReq := createTXTRequest{
		Records: []createTXTRecord{{
			Host: leaf,
			TTL:  d.config.TTL,
			Type: "TXT",
			Data: value,
		}},
	}

	reqBody, err := json.Marshal(createReq)
	if err != nil {
		return fmt.Errorf("createTXTRecord: marshaling request body failed: %w", err)
	}

	endpoint, err := d.config.APIEndpoint.Parse(path.Join(d.config.APIEndpoint.Path, "zones", zone, "records", leaf, "TXT"))
	if err != nil {
		return fmt.Errorf("createTXTRecord: failed to parse URL: %w", err)
	}

	req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(reqBody))
	if err != nil {
		return fmt.Errorf("createTXTRecord: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.token.Token))
	req.Header.Set("Content-Type", "application/json")

	resp, err := d.config.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("createTXTRecord: unable to perform HTTP request: %w", err)
	}

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

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

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("createTXTRecord: error in API: %d", resp.StatusCode)
	}

	createResp := createTXTResponse{}
	err = json.Unmarshal(body, &createResp)
	if err != nil {
		return fmt.Errorf("createTXTRecord: error parsing response: %w", err)
	}

	if createResp.Added != 1 {
		return errors.New("createTXTRecord: did not add TXT record for some reason")
	}

	// Success
	return nil
}

// https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords
func (d *DNSProvider) removeTXTRecord(zone, leaf, value string) error {
	if d.token == nil {
		return fmt.Errorf("removeTXTRecord: not logged in")
	}

	endpoint, err := d.config.APIEndpoint.Parse(path.Join(d.config.APIEndpoint.Path, "zones", zone, "records", leaf, "TXT"))
	if err != nil {
		return fmt.Errorf("removeTXTRecord: failed to parse URL: %w", err)
	}

	query := endpoint.Query()
	query.Add("data", value)
	endpoint.RawQuery = query.Encode()

	req, err := http.NewRequest(http.MethodDelete, endpoint.String(), nil)
	if err != nil {
		return fmt.Errorf("removeTXTRecord: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.token.Token))

	resp, err := d.config.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("removeTXTRecord: unable to perform HTTP request: %w", err)
	}

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

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

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("removeTXTRecord: error in API: %d", resp.StatusCode)
	}

	deleteResp := deleteTXTResponse{}
	err = json.Unmarshal(body, &deleteResp)
	if err != nil {
		return fmt.Errorf("removeTXTRecord: error parsing response: %w", err)
	}

	if deleteResp.Removed != 1 {
		return errors.New("removeTXTRecord: did not add TXT record for some reason")
	}

	// Success
	return nil
}
