package license

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/hyperboloide/lk"
)

type State string

const (
	StateValid   State = "valid"
	StateExpired State = "expired"
	StateInvalid State = "invalid"
)

// Reasons a licence is not valid. These are stable identifiers: the API returns
// them and the UI maps them to messages, so do not reword them casually.
const (
	ReasonNoLicense          = "no_license"
	ReasonBadSignature       = "bad_signature"
	ReasonDeploymentMismatch = "deployment_mismatch"
	ReasonInstanceMismatch   = "instance_mismatch"
	ReasonExpired            = "expired"
)

// VerifyOpts is what the verifier knows about itself.
type VerifyOpts struct {
	InstanceID string    // this instance's own ID; required
	Deployment string    // "cloud" or "self_hosted"; required
	Now        time.Time // zero means time.Now()
}

type Result struct {
	License License
	State   State
	Reason  string
	// ClockSkewed is set when IssuedAt is in the future, which usually means
	// the host clock is wrong. It does not by itself invalidate the licence.
	ClockSkewed bool
}

// Verify checks a licence blob against this instance.
//
// The checks run in a fixed order and stop at the first failure:
//
//  1. signature against a trusted public key  -> bad_signature
//  2. deployment matches this install         -> deployment_mismatch
//  3. instance ID matches this instance       -> instance_mismatch
//  4. not past ExpiresAt                      -> expired
//
// The order matters. A blob that is both expired and bound to another instance
// reports instance_mismatch, not expired, because that is the more useful thing
// to tell the person holding it.
//
// No clock tolerance is applied. Terms are a month or a year; a host whose clock
// is wrong by enough to matter has larger problems, and a tolerance window is a
// thing to get wrong.
func Verify(blob string, opts VerifyOpts) Result {
	if blob == "" {
		return Result{State: StateInvalid, Reason: ReasonNoLicense}
	}

	l, err := Parse(blob)
	if err != nil {
		return Result{State: StateInvalid, Reason: ReasonBadSignature}
	}

	res := Result{License: l}

	if l.Deployment != opts.Deployment {
		res.State, res.Reason = StateInvalid, ReasonDeploymentMismatch
		return res
	}
	if l.InstanceID != opts.InstanceID {
		res.State, res.Reason = StateInvalid, ReasonInstanceMismatch
		return res
	}

	now := opts.Now
	if now.IsZero() {
		now = time.Now()
	}
	res.ClockSkewed = l.IssuedAt.After(now)

	if !now.Before(l.ExpiresAt) {
		res.State, res.Reason = StateExpired, ReasonExpired
		return res
	}

	res.State = StateValid
	return res
}

// Parse verifies the signature only, ignoring binding and expiry.
//
// Used to display a licence and to inspect a blob a customer has emailed in.
// Never use it for enforcement — it does not check who the licence is for.
func Parse(blob string) (License, error) {
	parsed, err := lk.LicenseFromB32String(blob)
	if err != nil {
		return License{}, fmt.Errorf("licence is not readable: %w", err)
	}

	keys, err := publicKeys()
	if err != nil {
		return License{}, err
	}
	if len(keys) == 0 {
		return License{}, fmt.Errorf("this build trusts no licence signing keys")
	}

	verified := false
	for _, k := range keys {
		ok, err := parsed.Verify(k)
		if err == nil && ok {
			verified = true
			break
		}
	}
	if !verified {
		return License{}, fmt.Errorf("licence signature does not match any trusted key")
	}

	var l License
	if err := json.Unmarshal(parsed.Data, &l); err != nil {
		return License{}, fmt.Errorf("licence contents are not readable: %w", err)
	}
	return l, nil
}
