package coderd

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"database/sql"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strings"
	"time"

	"cdr.dev/slog"
	"github.com/go-chi/chi/v5"
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/xerrors"

	"github.com/coder/coder/coderd"
	"github.com/coder/coder/coderd/database"
	"github.com/coder/coder/coderd/httpapi"
	"github.com/coder/coder/coderd/rbac"
	"github.com/coder/coder/codersdk"
)

const (
	CurrentVersion        = 3
	HeaderKeyID           = "kid"
	AccountTypeSalesforce = "salesforce"
	VersionClaim          = "version"
	PubSubEventLicenses   = "licenses"
)

var ValidMethods = []string{"EdDSA"}

// key20220812 is the Coder license public key with id 2022-08-12 used to validate licenses signed
// by our signing infrastructure
//go:embed keys/2022-08-12
var key20220812 []byte

var keys = map[string]ed25519.PublicKey{"2022-08-12": ed25519.PublicKey(key20220812)}

type Features struct {
	UserLimit int64 `json:"user_limit"`
	AuditLog  int64 `json:"audit_log"`
}

type Claims struct {
	jwt.RegisteredClaims
	// LicenseExpires is the end of the legit license term, and the start of the grace period, if
	// there is one.  The standard JWT claim "exp" (ExpiresAt in jwt.RegisteredClaims, above) is
	// the end of the grace period (identical to LicenseExpires if there is no grace period).
	// The reason we use the standard claim for the end of the grace period is that we want JWT
	// processing libraries to consider the token "valid" until then.
	LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
	AccountType    string           `json:"account_type,omitempty"`
	AccountID      string           `json:"account_id,omitempty"`
	Version        uint64           `json:"version"`
	Features       Features         `json:"features"`
}

var (
	ErrInvalidVersion = xerrors.New("license must be version 3")
	ErrMissingKeyID   = xerrors.Errorf("JOSE header must contain %s", HeaderKeyID)
)

// parseLicense parses the license and returns the claims. If the license's signature is invalid or
// is not parsable, an error is returned.
func parseLicense(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error) {
	tok, err := jwt.Parse(
		l,
		keyFunc(keys),
		jwt.WithValidMethods(ValidMethods),
	)
	if err != nil {
		return nil, err
	}
	if claims, ok := tok.Claims.(jwt.MapClaims); ok && tok.Valid {
		version, ok := claims[VersionClaim].(float64)
		if !ok {
			return nil, ErrInvalidVersion
		}
		if int64(version) != CurrentVersion {
			return nil, ErrInvalidVersion
		}
		return claims, nil
	}
	return nil, xerrors.New("unable to parse Claims")
}

func keyFunc(keys map[string]ed25519.PublicKey) func(*jwt.Token) (interface{}, error) {
	return func(j *jwt.Token) (interface{}, error) {
		keyID, ok := j.Header[HeaderKeyID].(string)
		if !ok {
			return nil, ErrMissingKeyID
		}
		k, ok := keys[keyID]
		if !ok {
			return nil, xerrors.Errorf("no key with ID %s", keyID)
		}
		return k, nil
	}
}

// licenseAPI handles enterprise licenses, and attaches to the main coderd.API via the
// LicenseHandler option, so that it serves all routes under /api/v2/licenses
type licenseAPI struct {
	router   chi.Router
	logger   slog.Logger
	database database.Store
	pubsub   database.Pubsub
	auth     *coderd.HTTPAuthorizer
}

func newLicenseAPI(
	l slog.Logger,
	db database.Store,
	ps database.Pubsub,
	auth *coderd.HTTPAuthorizer,
) *licenseAPI {
	r := chi.NewRouter()
	a := &licenseAPI{router: r, logger: l, database: db, pubsub: ps, auth: auth}
	r.Post("/", a.postLicense)
	r.Get("/", a.licenses)
	return a
}

func (a *licenseAPI) handler() http.Handler {
	return a.router
}

// postLicense adds a new Enterprise license to the cluster.  We allow multiple different licenses
// in the cluster at one time for several reasons:
//
// 1. Upgrades --- if the license format changes from one version of Coder to the next, during a
//    rolling update you will have different Coder servers that need different licenses to function.
// 2. Avoid abrupt feature breakage --- when an admin uploads a new license with different features
//    we generally don't want the old features to immediately break without warning.  With a grace
//    period on the license, features will continue to work from the old license until its grace
//    period, then the users will get a warning allowing them to gracefully stop using the feature.
func (a *licenseAPI) postLicense(rw http.ResponseWriter, r *http.Request) {
	if !a.auth.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) {
		httpapi.Forbidden(rw)
		return
	}

	var addLicense codersdk.AddLicenseRequest
	if !httpapi.Read(rw, r, &addLicense) {
		return
	}

	claims, err := parseLicense(addLicense.License, keys)
	if err != nil {
		httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
			Message: "Invalid license",
			Detail:  err.Error(),
		})
		return
	}
	exp, ok := claims["exp"].(float64)
	if !ok {
		httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
			Message: "Invalid license",
			Detail:  "exp claim missing or not parsable",
		})
		return
	}
	expTime := time.Unix(int64(exp), 0)

	dl, err := a.database.InsertLicense(r.Context(), database.InsertLicenseParams{
		UploadedAt: database.Now(),
		JWT:        addLicense.License,
		Exp:        expTime,
	})
	if err != nil {
		httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Unable to add license to database",
			Detail:  err.Error(),
		})
		return
	}
	err = a.pubsub.Publish(PubSubEventLicenses, []byte("add"))
	if err != nil {
		a.logger.Error(context.Background(), "failed to publish license add", slog.Error(err))
		// don't fail the HTTP request, since we did write it successfully to the database
	}

	httpapi.Write(rw, http.StatusCreated, convertLicense(dl, claims))
}

func convertLicense(dl database.License, c jwt.MapClaims) codersdk.License {
	return codersdk.License{
		ID:         dl.ID,
		UploadedAt: dl.UploadedAt,
		Claims:     c,
	}
}

func (a *licenseAPI) licenses(rw http.ResponseWriter, r *http.Request) {
	licenses, err := a.database.GetLicenses(r.Context())
	if xerrors.Is(err, sql.ErrNoRows) {
		httpapi.Write(rw, http.StatusOK, []codersdk.License{})
		return
	}
	if err != nil {
		httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching licenses.",
			Detail:  err.Error(),
		})
		return
	}

	licenses, err = coderd.AuthorizeFilter(a.auth, r, rbac.ActionRead, licenses)
	if err != nil {
		httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error fetching licenses.",
			Detail:  err.Error(),
		})
		return
	}
	sdkLicenses, err := convertLicenses(licenses)
	if err != nil {
		httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
			Message: "Internal error parsing licenses.",
			Detail:  err.Error(),
		})
		return
	}
	httpapi.Write(rw, http.StatusOK, sdkLicenses)
}

func convertLicenses(licenses []database.License) ([]codersdk.License, error) {
	var out []codersdk.License
	for _, l := range licenses {
		c, err := decodeClaims(l)
		if err != nil {
			return nil, err
		}
		out = append(out, convertLicense(l, c))
	}
	return out, nil
}

// decodeClaims decodes the JWT claims from the stored JWT.  Note here we do not validate the JWT
// and just return the claims verbatim.  We want to include all licenses on the GET response, even
// if they are expired, or signed by a key this version of Coder no longer considers valid.
//
// Also, we do not return the whole JWT itself because a signed JWT is a bearer token and we
// want to limit the chance of it being accidentally leaked.
func decodeClaims(l database.License) (jwt.MapClaims, error) {
	parts := strings.Split(l.JWT, ".")
	if len(parts) != 3 {
		return nil, xerrors.Errorf("Unable to parse license %d as JWT", l.ID)
	}
	cb, err := base64.URLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, xerrors.Errorf("Unable to decode license %d claims: %w", l.ID, err)
	}
	c := make(jwt.MapClaims)
	d := json.NewDecoder(bytes.NewBuffer(cb))
	d.UseNumber()
	err = d.Decode(&c)
	return c, err
}
