package acme

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/smallstep/certificates/api/render"
	"github.com/smallstep/certificates/authority"
	"github.com/smallstep/certificates/authority/provisioner"
)

// LinkType captures the link type.
type LinkType int

const (
	// NewNonceLinkType new-nonce
	NewNonceLinkType LinkType = iota
	// NewAccountLinkType new-account
	NewAccountLinkType
	// AccountLinkType account
	AccountLinkType
	// OrderLinkType order
	OrderLinkType
	// NewOrderLinkType new-order
	NewOrderLinkType
	// OrdersByAccountLinkType list of orders owned by account
	OrdersByAccountLinkType
	// FinalizeLinkType finalize order
	FinalizeLinkType
	// NewAuthzLinkType authz
	NewAuthzLinkType
	// AuthzLinkType new-authz
	AuthzLinkType
	// ChallengeLinkType challenge
	ChallengeLinkType
	// CertificateLinkType certificate
	CertificateLinkType
	// DirectoryLinkType directory
	DirectoryLinkType
	// RevokeCertLinkType revoke certificate
	RevokeCertLinkType
	// KeyChangeLinkType key rollover
	KeyChangeLinkType
)

func (l LinkType) String() string {
	switch l {
	case NewNonceLinkType:
		return "new-nonce"
	case NewAccountLinkType:
		return "new-account"
	case AccountLinkType:
		return "account"
	case NewOrderLinkType:
		return "new-order"
	case OrderLinkType:
		return "order"
	case NewAuthzLinkType:
		return "new-authz"
	case AuthzLinkType:
		return "authz"
	case ChallengeLinkType:
		return "challenge"
	case CertificateLinkType:
		return "certificate"
	case DirectoryLinkType:
		return "directory"
	case RevokeCertLinkType:
		return "revoke-cert"
	case KeyChangeLinkType:
		return "key-change"
	default:
		return fmt.Sprintf("unexpected LinkType '%d'", int(l))
	}
}

func GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string {
	switch typ {
	case NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType:
		return fmt.Sprintf("/%s/%s", provisionerName, typ)
	case AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType:
		return fmt.Sprintf("/%s/%s/%s", provisionerName, typ, inputs[0])
	case ChallengeLinkType:
		return fmt.Sprintf("/%s/%s/%s/%s", provisionerName, typ, inputs[0], inputs[1]) //nolint:gosec // operating on internally defined inputs
	case OrdersByAccountLinkType:
		return fmt.Sprintf("/%s/%s/%s/orders", provisionerName, AccountLinkType, inputs[0])
	case FinalizeLinkType:
		return fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLinkType, inputs[0])
	default:
		return ""
	}
}

// NewLinker returns a new Directory type.
func NewLinker(dns, prefix string) Linker {
	_, _, err := net.SplitHostPort(dns)
	if err != nil && strings.Contains(err.Error(), "too many colons in address") {
		// this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334
		// in case a port was appended to this wrong format, we try to extract the port, then check if it's
		// still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of
		// these cases, then the input dns is not changed.
		lastIndex := strings.LastIndex(dns, ":")
		hostPart, portPart := dns[:lastIndex], dns[lastIndex+1:]
		if ip := net.ParseIP(hostPart); ip != nil {
			dns = "[" + hostPart + "]:" + portPart
		} else if ip := net.ParseIP(dns); ip != nil {
			dns = "[" + dns + "]"
		}
	}
	return &linker{prefix: prefix, dns: dns}
}

// Linker interface for generating links for ACME resources.
type Linker interface {
	GetLink(ctx context.Context, typ LinkType, inputs ...string) string
	Middleware(http.Handler) http.Handler
	LinkOrder(ctx context.Context, o *Order)
	LinkAccount(ctx context.Context, o *Account)
	LinkChallenge(ctx context.Context, o *Challenge, azID string)
	LinkAuthorization(ctx context.Context, o *Authorization)
	LinkOrdersByAccountID(ctx context.Context, orders []string)
}

type linkerKey struct{}

// NewLinkerContext adds the given linker to the context.
func NewLinkerContext(ctx context.Context, v Linker) context.Context {
	return context.WithValue(ctx, linkerKey{}, v)
}

// LinkerFromContext returns the current linker from the given context.
func LinkerFromContext(ctx context.Context) (v Linker, ok bool) {
	v, ok = ctx.Value(linkerKey{}).(Linker)
	return
}

// MustLinkerFromContext returns the current linker from the given context. It
// will panic if it's not in the context.
func MustLinkerFromContext(ctx context.Context) Linker {
	var (
		v  Linker
		ok bool
	)
	if v, ok = LinkerFromContext(ctx); !ok {
		panic("acme linker is not the context")
	}
	return v
}

type baseURLKey struct{}

func newBaseURLContext(ctx context.Context, r *http.Request) context.Context {
	var u *url.URL
	if r.Host != "" {
		u = &url.URL{Scheme: "https", Host: r.Host}
	}
	return context.WithValue(ctx, baseURLKey{}, u)
}

func baseURLFromContext(ctx context.Context) *url.URL {
	if u, ok := ctx.Value(baseURLKey{}).(*url.URL); ok {
		return u
	}
	return nil
}

// linker generates ACME links.
type linker struct {
	prefix string
	dns    string
}

// Middleware gets the provisioner and current url from the request and sets
// them in the context so we can use the linker to create ACME links.
func (l *linker) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Add base url to the context.
		ctx := newBaseURLContext(r.Context(), r)

		// Add provisioner to the context.
		nameEscaped := chi.URLParam(r, "provisionerID")
		name, err := url.PathUnescape(nameEscaped)
		if err != nil {
			render.Error(w, r, WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped))
			return
		}

		p, err := authority.MustFromContext(ctx).LoadProvisionerByName(name)
		if err != nil {
			render.Error(w, r, err)
			return
		}

		var acmeProv *provisioner.ACME
		switch prov := p.(type) {
		case *provisioner.ACME:
			acmeProv = prov
		case provisioner.Uninitialized:
			render.Error(w, r, NewDetailedError(ErrorUnauthorizedType, "provisioner is disabled due to an initialization error"))
			return
		default:
			render.Error(w, r, NewDetailedError(ErrorUnauthorizedType, "provisioner must be of type ACME"))
			return
		}

		ctx = NewProvisionerContext(ctx, Provisioner(acmeProv))
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// GetLink is a helper for GetLinkExplicit.
func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string {
	var name string
	if p, ok := ProvisionerFromContext(ctx); ok {
		name = p.GetName()
	}

	var u url.URL
	if baseURL := baseURLFromContext(ctx); baseURL != nil {
		u = *baseURL
	}
	if u.Scheme == "" {
		u.Scheme = "https"
	}
	if u.Host == "" {
		u.Host = l.dns
	}

	u.Path = l.prefix + GetUnescapedPathSuffix(typ, name, inputs...)
	return u.String()
}

// LinkOrder sets the ACME links required by an ACME order.
func (l *linker) LinkOrder(ctx context.Context, o *Order) {
	o.AuthorizationURLs = make([]string, len(o.AuthorizationIDs))
	for i, azID := range o.AuthorizationIDs {
		o.AuthorizationURLs[i] = l.GetLink(ctx, AuthzLinkType, azID)
	}
	o.FinalizeURL = l.GetLink(ctx, FinalizeLinkType, o.ID)
	if o.CertificateID != "" {
		o.CertificateURL = l.GetLink(ctx, CertificateLinkType, o.CertificateID)
	}
}

// LinkAccount sets the ACME links required by an ACME account.
func (l *linker) LinkAccount(ctx context.Context, acc *Account) {
	acc.OrdersURL = l.GetLink(ctx, OrdersByAccountLinkType, acc.ID)
}

// LinkChallenge sets the ACME links required by an ACME challenge.
func (l *linker) LinkChallenge(ctx context.Context, ch *Challenge, azID string) {
	ch.URL = l.GetLink(ctx, ChallengeLinkType, azID, ch.ID)
}

// LinkAuthorization sets the ACME links required by an ACME authorization.
func (l *linker) LinkAuthorization(ctx context.Context, az *Authorization) {
	for _, ch := range az.Challenges {
		l.LinkChallenge(ctx, ch, az.ID)
	}
}

// LinkOrdersByAccountID converts each order ID to an ACME link.
func (l *linker) LinkOrdersByAccountID(ctx context.Context, orders []string) {
	for i, id := range orders {
		orders[i] = l.GetLink(ctx, OrderLinkType, id)
	}
}
