package http_acme

import (
	"encoding/json"
	"fmt"
	"github.com/1f349/orchid/logger"
	"github.com/go-acme/lego/v4/challenge"
	"gopkg.in/yaml.v3"
	"net/http"
	"os"
	"strings"
)

var _ challenge.Provider = &HttpAcmeProvider{}

// HttpAcmeProvider sends HTTP requests to an API updating the outputted
// `.wellknown/acme-challenges` data
type HttpAcmeProvider struct {
	tokenFile                    string
	accessToken, refreshToken    string
	apiUrlPresent, apiUrlCleanUp string
	apiUrlRefreshToken           string
	trip                         http.RoundTripper
}

type AcmeLogin struct {
	Access  string `yaml:"access"`
	Refresh string `yaml:"refresh"`
}

// NewHttpAcmeProvider creates a new HttpAcmeProvider using http.DefaultTransport
// as the transport
func NewHttpAcmeProvider(tokenFile, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken string) (*HttpAcmeProvider, error) {
	// acme login token
	openTokens, err := os.Open(tokenFile)
	if err != nil {
		return nil, fmt.Errorf("failed to load acme tokens: %w", err)
	}

	var acmeLogins AcmeLogin
	err = yaml.NewDecoder(openTokens).Decode(&acmeLogins)
	if err != nil {
		return nil, fmt.Errorf("failed to load acme tokens: %w", err)
	}

	return &HttpAcmeProvider{tokenFile, acmeLogins.Access, acmeLogins.Refresh, apiUrlPresent, apiUrlCleanUp, apiUrlRefreshToken, http.DefaultTransport}, nil
}

// Present implements challenge.Provider and sends a put request to the specified
// path along with a bearer token to authenticate
func (h *HttpAcmeProvider) Present(domain, token, keyAuth string) error {
	// round trip
	trip, err := h.authCheckRequest(http.MethodPut, h.apiUrlPresent, domain, token, keyAuth)
	if err != nil {
		return err
	}
	if trip.StatusCode != http.StatusAccepted {
		return fmt.Errorf("trip response status code was not 202")
	}
	return nil
}

// CleanUp implements challenge.Provider and sends a delete request to the
// specified path along with a bearer token to authenticate
func (h *HttpAcmeProvider) CleanUp(domain, token, keyAuth string) error {
	// round trip
	trip, err := h.authCheckRequest(http.MethodDelete, h.apiUrlCleanUp, domain, token, keyAuth)
	if err != nil {
		return err
	}
	if trip.StatusCode != http.StatusAccepted {
		return fmt.Errorf("trip response status code was not 202")
	}
	return nil
}

// authCheckRequest call internalRequest and renews the access token if it is
// outdated and calls internalRequest again
func (h *HttpAcmeProvider) authCheckRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
	// call internal request and check the status code
	resp, err := h.internalRequest(method, url, domain, token, keyAuth)
	if err != nil {
		return nil, err
	}
	switch resp.StatusCode {
	case http.StatusAccepted:
		// just return
		return resp, nil
	case http.StatusForbidden:
		// send request to get renewed access and refresh tokens
		req, err := http.NewRequest(http.MethodPost, h.apiUrlRefreshToken, nil)
		if err != nil {
			return nil, fmt.Errorf("refresh token request failed: %w", err)
		}
		req.Header.Set("Authorization", "Bearer "+h.refreshToken)

		// round trip and status check
		trip, err := h.trip.RoundTrip(req)
		if err != nil {
			return nil, fmt.Errorf("refresh token request failed: %w", err)
		}
		if trip.StatusCode != http.StatusAccepted {
			return nil, fmt.Errorf("refresh token request failed: due to invalid status code, expected 202 got %d", trip.StatusCode)
		}

		// parse tokens from response body
		var tokens struct {
			Access  string `json:"access"`
			Refresh string `json:"refresh"`
		}
		if json.NewDecoder(trip.Body).Decode(&tokens) != nil {
			return nil, fmt.Errorf("refresh token parsing failed: %w", err)
		}
		h.accessToken = tokens.Access
		h.refreshToken = tokens.Refresh

		go h.saveLoginTokens()

		// call internal request again
		resp, err = h.internalRequest(method, url, domain, token, keyAuth)
		if err != nil {
			return nil, err
		}
		if resp.StatusCode == http.StatusAccepted {
			// just return
			return resp, nil
		}
		return nil, fmt.Errorf("invalid status code, expected 202 got %d", resp.StatusCode)
	}
	// first request had an invalid status code
	return nil, fmt.Errorf("invalid status code, expected 202/403 got %d", resp.StatusCode)
}

// internalRequest sends a request to the acme challenge hosting api
func (h *HttpAcmeProvider) internalRequest(method, url, domain, token, keyAuth string) (*http.Response, error) {
	v := strings.NewReplacer("$domain", domain, "$token", token, "$content", keyAuth).Replace(url)
	req, err := http.NewRequest(method, v, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+h.accessToken)
	return h.trip.RoundTrip(req)
}

func (h *HttpAcmeProvider) saveLoginTokens() {
	// acme login token
	openTokens, err := os.Create(h.tokenFile)
	if err != nil {
		logger.Logger.Info("[Orchid] Failed to open token file:", "err", err)
	}
	defer openTokens.Close()

	err = yaml.NewEncoder(openTokens).Encode(AcmeLogin{Access: h.accessToken, Refresh: h.refreshToken})
	if err != nil {
		logger.Logger.Info("[Orchid] Failed to write tokens file:", "err", err)
	}
}
