// package license provides functions for reading and printing
// licenses generated by Chef's license generation service.
//
// # WARNING WARNING WARNING WARNING WARNING
//
// The code in this package is a copy of code in the chef/license
// library. chef/license is still proprietary and thus cannot be
// included in full as a dependency. Please take care that any changes
// to this file are compatible with the chef/license library.
//
// WARNING WARNING WARNING WARNING WARNING
package license

import (
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"strings"
	"time"

	"google.golang.org/protobuf/types/known/timestamppb"

	jwt "github.com/golang-jwt/jwt/v5"
	"github.com/golang/protobuf/ptypes/timestamp"
)

type ParsedLicense struct {
	Id                string                 `json:"id,omitempty"`         // Unique ID for this license; defaults to auto-generated
	Version           string                 `json:"version,omitempty"`    // License version
	Type              string                 `json:"type,omitempty"`       // commercial, eval, developer, partner
	Generator         string                 `json:"generator,omitempty"`  // chef/license, chef/license-generation-service
	KeySha256         string                 `json:"key_sha256,omitempty"` // public key checksum
	GenerationDate    *timestamppb.Timestamp `json:"generation_date,omitempty"`
	Customer          string                 `json:"customer,omitempty"`
	V0CustomerId      string                 `json:"customer_id,omitempty"`
	V1CustomerId      string                 `json:"customerId,omitempty"`
	CustomerIdVersion string                 `json:"customer_id_version,omitempty"`
	Entitlements      []*Entitlement         `json:"entitlements,omitempty"`
}

type licenseWithClaims struct {
	ParsedLicense
	jwt.RegisteredClaims
}

// GetKeySha256 fetches the SHA256 of the public key from the JWT encoded license
func GetKeySha256(jwtLicense string) (string, error) {
	var jwtPayload map[string]interface{}

	// Current JWT standard is a base64 string separated by 3 periods
	jwtParts := strings.Split(jwtLicense, ".")
	if len(jwtParts) != 3 {
		return "", errors.New("Not Valid JWT String")
	}

	// Use base64.RawURLEncoding to decode the JWT payload
	data, err := base64.RawURLEncoding.DecodeString(jwtParts[1])
	if err != nil {
		return "", err
	}

	if err := json.Unmarshal(data, &jwtPayload); err != nil {
		return "", err
	}

	if jwtPayload["key_sha256"] == nil {
		return "", errors.New("Couldn't find key_sha256 in token's claims")
	}

	return jwtPayload["key_sha256"].(string), nil
}

// Read and parse a license from a string and return the config
// Ex: sha, _ := license.GetKeySha256(license)
//
//	lic, _ := license.Read(license, publicKeys[sha])
func Read(license string, publicKey []byte) (*License, error) {
	block, _ := pem.Decode(publicKey)
	if block == nil || block.Type != "PUBLIC KEY" {
		return nil, fmt.Errorf("Failed to decode PEM block containing public key")
	}

	key, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse public key: %+v", err)
	}

	decrypter := func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return key, nil
	}

	var lwc = licenseWithClaims{}

	token, err := jwt.ParseWithClaims(license, &lwc, decrypter)
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		return nil, fmt.Errorf("Token is not valid")
	}

	claims, ok := token.Claims.(*licenseWithClaims)
	if !ok {
		return nil, fmt.Errorf(
			"Couldn't map data fields to the right type. This is an internal error with the license",
		)
	}

	l := &License{}
	p := claims.ParsedLicense
	l.Id = p.Id
	l.Version = p.Version
	l.Type = p.Type
	l.Generator = p.Generator
	l.KeySha256 = p.KeySha256
	l.GenerationDate = p.GenerationDate
	l.Customer = p.Customer
	l.CustomerId = p.V0CustomerId
	if l.CustomerId == "" {
		l.CustomerId = p.V1CustomerId
	}
	l.CustomerIdVersion = p.CustomerIdVersion
	l.Entitlements = p.Entitlements
	return l, nil
}

// PrettyPrint a license into a nice string for text output
func (l *License) PrettyPrint() string {
	entitlements := make([]string, 0, len(l.Entitlements))
	for _, ent := range l.Entitlements {
		entf := fmt.Sprintf(`
		Name: %s
		Measure: %s, Limit: %d
		Start: %s, End: %s
		`,
			ent.Name,
			ent.Measure, ent.Limit,
			timestampToRFC3339(ent.Start), timestampToRFC3339(ent.End),
		)
		entitlements = append(entitlements, entf)
	}

	return fmt.Sprintf(`
	License ID:          %s
	License Type:        %s
	License Version:     v%s
	License Generation:  %s

	Signing key SHA256:  %s

	Generation date:     %s
	Customer:            %s (ID: %s v%s)

	Entitlements:
	%s
	`,
		l.Id,
		l.Type,
		l.Version,
		l.Generator,

		l.KeySha256,

		timestampToRFC3339(l.GenerationDate),
		l.Customer, l.CustomerId, l.CustomerIdVersion,

		strings.Join(entitlements, "\n"))
}

// timestampToRFC3339 converts protobuf timestamp to RFC 3339 string
func timestampToRFC3339(t *timestamp.Timestamp) string {
	if t == nil {
		t = &timestamp.Timestamp{}
	}
	return time.Unix(t.Seconds, 0).UTC().Format(time.RFC3339)
}
