// Package models holds admin's own documents.
//
// These are admin-owned and never shared with the control plane. The two
// structs that ARE shared — Instance and User on the control-plane side — come
// from shared/models, so there is no second copy of those shapes to drift.
package models

import (
	"encoding/json"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// Features is a list of feature flags that marshals as `[]` rather than `null`.
//
// A nil Go slice becomes JSON null. The Free plan has no features, so every
// Free licence served `"features": null`, and the portal — whose type said
// string[] — called .length on it and took the page down with it.
//
// The guarantee lives on the type rather than at each of the six places a
// licence or plan is serialised, because the seventh is the one that would have
// been forgotten. It also fixes rows already holding null in Mongo, since it
// applies at marshal time rather than at write time.
type Features []string

func (f Features) MarshalJSON() ([]byte, error) {
	if f == nil {
		return []byte("[]"), nil
	}
	return json.Marshal([]string(f))
}

// OrEmpty is the same guarantee for values headed to Mongo rather than to JSON,
// so a null never enters the database in the first place.
func (f Features) OrEmpty() Features {
	if f == nil {
		return Features{}
	}
	return f
}

// Instance statuses.
const (
	StatusAwaitingLink = "awaiting_link"
	StatusActive       = "active"
	StatusLapsed       = "lapsed"
	StatusCancelled    = "cancelled"

	// StatusDeleted marks an instance the control plane has reaped. The row is
	// kept because the licence history references it and support questions
	// outlive the instance.
	StatusDeleted = "deleted"
)

// Account statuses.
const (
	AccountActive    = "active"
	AccountSuspended = "suspended"
)

// Licence issuance reasons. These end up in support conversations, so they are
// stable identifiers rather than prose.
const (
	ReasonNew        = "new"
	ReasonRenewal    = "renewal"
	ReasonTierChange = "tier_change"
	ReasonRelink     = "relink"
	ReasonManual     = "manual"

	// ReasonEntitlementChange is a mid-term change to what an instance is
	// allowed — servers added, a feature toggled — at the same expiry.
	//
	// It is deliberately NOT ReasonRenewal: a renewal resets relink_count
	// because a new term has begun, and adding a server does not begin one.
	ReasonEntitlementChange = "entitlement_change"
)

// Subscription statuses, mirrored from Paddle. Ours, not a vendor SDK's, so the
// billing package does not import anything Paddle.
const (
	SubActive   = "active"
	SubCanceled = "canceled"
	SubPastDue  = "past_due"
	SubTrialing = "trialing"
)

// Billing terms. These match catalogue price-ID keys and license.TermsFor.
const (
	TermMonthly = "monthly"
	TermAnnual  = "annual"
)

// MaxRelinksPerTerm is the customer-facing relink cap.
//
// This is an abuse SIGNAL, not abuse prevention — offline licences cannot be
// revoked, so a determined customer is not stopped by a counter. Its real job is
// to put a human in front of the fourth attempt.
const MaxRelinksPerTerm = 3

// GracePeriod is added to every licence expiry beyond the billing period end,
// so a renewal webhook arriving slightly late does not create a gap in which a
// paying customer's instance goes read-only.
const GracePeriod = 3 * 24 * time.Hour

// RenewWindow is how long before expiry a Free instance may be renewed.
//
// Renewal stays available after expiry too, right up until the reaper takes the
// instance, so the same button rescues a lapsed instance instead of needing a
// second mechanism.
const RenewWindow = 7 * 24 * time.Hour

type Account struct {
	ID               bson.ObjectID `bson:"_id,omitempty" json:"-"`
	AccountID        string        `bson:"account_id" json:"account_id"`
	Name             string        `bson:"name" json:"name"`
	BillingEmail     string        `bson:"billing_email" json:"billing_email"`
	PaddleCustomerID string        `bson:"paddle_customer_id,omitempty" json:"paddle_customer_id,omitempty"`
	Status           string        `bson:"status" json:"status"`
	CreatedAt        time.Time     `bson:"created_at" json:"created_at"`
}

// Instance is admin's record of one deployment.
//
// For cloud, InstanceID equals the control-plane instance_id. For self-hosted it
// is the UUID the customer pasted — their database is theirs, and we cannot see
// it, so this row is the only thing that exists on our side.
type Instance struct {
	ID             bson.ObjectID `bson:"_id,omitempty" json:"-"`
	InstanceID     string        `bson:"instance_id" json:"instance_id"`
	AccountID      string        `bson:"account_id" json:"account_id"`
	Name           string        `bson:"name" json:"name"`
	Slug           string        `bson:"slug,omitempty" json:"slug,omitempty"`
	Deployment     string        `bson:"deployment" json:"deployment"`
	Tier           string        `bson:"tier,omitempty" json:"tier,omitempty"`
	Status         string        `bson:"status" json:"status"`
	CurrentLicense string        `bson:"current_license,omitempty" json:"current_license,omitempty"`
	RelinkCount    int           `bson:"relink_count" json:"relink_count"`
	InjectFailedAt *time.Time    `bson:"inject_failed_at,omitempty" json:"inject_failed_at,omitempty"`
	// NoticesSent holds the lifecycle notice keys already emailed for the
	// CURRENT term ("expiring", "expired", "delete_7", "delete_1"). Renewal
	// clears it, so the next term starts the sequence again. It is what stops a
	// restart re-sending a notice.
	NoticesSent []string `bson:"notices_sent,omitempty" json:"notices_sent,omitempty"`
	// Placeholder is true while a self-hosted instance row exists only so a
	// checkout has something to attach custom_data to, before the customer has
	// pasted their install's real UUID. Cleared when the instance is linked.
	Placeholder bool `bson:"placeholder,omitempty" json:"placeholder,omitempty"`
	// PreviousInstanceIDs is every id this row has carried before its current one.
	// A self-hosted row's identity is rewritten twice over its life — once when a
	// paid placeholder is claimed, and again on each relink to a rebuilt server —
	// and Paddle keeps its own copy of custom_data written at checkout. That copy
	// is patched on each rewrite, but the patch is best-effort and any event
	// already in flight still names an old id, so this is what lets a webhook
	// resolve to the right instance instead of erroring as unknown.
	PreviousInstanceIDs []string `bson:"previous_instance_ids,omitempty" json:"-"`
	// PendingOwnerUserID is the customer_user who bought a paid-cloud placeholder,
	// remembered so the confirmed-payment webhook can provision the instance with
	// them as owner. Cleared once provisioned. Only ever set on a cloud placeholder.
	PendingOwnerUserID string    `bson:"pending_owner_user_id,omitempty" json:"-"`
	CreatedAt          time.Time `bson:"created_at" json:"created_at"`
}

// License is append-only. A renewal writes a new row and sets SupersededBy on
// the old one. Nothing here is ever edited or deleted: when a support question
// arrives about why an instance stopped working on a given date, the answer has
// to still be in the table.
type License struct {
	ID           bson.ObjectID  `bson:"_id,omitempty" json:"-"`
	LicenseID    string         `bson:"license_id" json:"license_id"`
	InstanceID   string         `bson:"instance_id" json:"instance_id"`
	AccountID    string         `bson:"account_id" json:"account_id"`
	Tier         string         `bson:"tier" json:"tier"`
	Deployment   string         `bson:"deployment" json:"deployment"`
	Limits       license.Limits `bson:"limits" json:"limits"`
	Features     Features       `bson:"features" json:"features"`
	IssuedAt     time.Time      `bson:"issued_at" json:"issued_at"`
	ExpiresAt    time.Time      `bson:"expires_at" json:"expires_at"`
	Blob         string         `bson:"blob" json:"-"`
	SupersededBy string         `bson:"superseded_by,omitempty" json:"superseded_by,omitempty"`
	IssuedBy     string         `bson:"issued_by" json:"issued_by"`
	Reason       string         `bson:"reason" json:"reason"`
}

type Subscription struct {
	ID                   bson.ObjectID `bson:"_id,omitempty" json:"-"`
	SubscriptionID       string        `bson:"subscription_id" json:"subscription_id"`
	AccountID            string        `bson:"account_id" json:"account_id"`
	InstanceID           string        `bson:"instance_id,omitempty" json:"instance_id,omitempty"`
	PaddleSubscriptionID string        `bson:"paddle_subscription_id,omitempty" json:"paddle_subscription_id,omitempty"`
	Tier                 string        `bson:"tier" json:"tier"`
	Term                 string        `bson:"term" json:"term"`
	Status               string        `bson:"status" json:"status"`
	CurrentPeriodEnd     time.Time     `bson:"current_period_end" json:"current_period_end"`
	// Items is the full line-item list. Spec 7 made a subscription several
	// prices — a base, a per-server unit at quantity N, an item per paid
	// feature — so a single price ID can no longer describe it.
	Items []SubItem `bson:"items,omitempty" json:"items,omitempty"`
}

// SubItem is one line of a subscription: a price and its quantity, the shape
// catalogue.ResolveItems reads back into a plan and configuration.
type SubItem struct {
	PriceID  string `bson:"price_id" json:"price_id"`
	Quantity int    `bson:"quantity" json:"quantity"`
}

// PaddleEvent is the idempotency record for one webhook delivery. The unique
// index on EventID is what makes a retry a no-op rather than a second licence.
type PaddleEvent struct {
	ID          bson.ObjectID `bson:"_id,omitempty" json:"-"`
	EventID     string        `bson:"event_id" json:"event_id"`
	EventType   string        `bson:"event_type" json:"event_type"`
	ReceivedAt  time.Time     `bson:"received_at" json:"received_at"`
	ProcessedAt *time.Time    `bson:"processed_at,omitempty" json:"processed_at,omitempty"`
	Error       string        `bson:"error,omitempty" json:"error,omitempty"`
}

// Plan is the authoritative definition of one (deployment, tier) pair, seeded
// from shared/license.
//
// It lives in the database so tier contents change without a deploy. Every
// issued licence snapshots it, so editing a plan never rewrites an existing
// licence — the same rule as workflow_runs.steps_snapshot.
//
// It holds NO Paddle identifiers. Every price ID lives in `catalogue`, because a
// metered plan is priced by several components and a single map on this row
// cannot express that.
type Plan struct {
	ID         bson.ObjectID `bson:"_id,omitempty" json:"-"`
	Deployment string        `bson:"deployment" json:"deployment"`
	Tier       string        `bson:"tier" json:"tier"`
	Name       string        `bson:"name" json:"name"`
	// BaseLimits is the allowance before anything is bought. The field is named
	// `base_` rather than `limits` because that is a different claim from the one
	// the old field made, and a reader must not assume it is the total.
	BaseLimits   license.Limits `bson:"base_limits" json:"base_limits"`
	BaseFeatures Features       `bson:"base_features" json:"base_features"`
	SupportLevel string         `bson:"support_level" json:"support_level"`
	Active       bool           `bson:"active" json:"active"`
}

type StaffUser struct {
	ID           bson.ObjectID `bson:"_id,omitempty" json:"-"`
	UserID       string        `bson:"user_id" json:"user_id"`
	Email        string        `bson:"email" json:"email"`
	PasswordHash string        `bson:"password_hash" json:"-"`
	Name         string        `bson:"name" json:"name"`
	CreatedAt    time.Time     `bson:"created_at" json:"created_at"`
}

// CustomerUser is one person on an HQ account.
//
// AccountRole governs what they may do to the ACCOUNT — invite people, create
// instances, grant access. It says nothing about what they may do inside any
// instance; that is the role on their InstanceMember row.
type CustomerUser struct {
	ID                bson.ObjectID `bson:"_id,omitempty" json:"-"`
	UserID            string        `bson:"user_id" json:"user_id"`
	AccountID         string        `bson:"account_id" json:"account_id"`
	Email             string        `bson:"email" json:"email"`
	PasswordHash      string        `bson:"password_hash" json:"-"`
	AccountRole       string        `bson:"account_role" json:"account_role"`
	VerifiedAt        *time.Time    `bson:"verified_at,omitempty" json:"verified_at,omitempty"`
	VerifyTokenHash   string        `bson:"verify_token_hash,omitempty" json:"-"`
	VerifyTokenExpiry *time.Time    `bson:"verify_token_expiry,omitempty" json:"-"`
	// HQSyncFailedAt is set when a password change could not be written to
	// every projected control-plane row. It is visibility only — hqsync repairs
	// by comparing hashes, not by reading this field.
	HQSyncFailedAt *time.Time `bson:"hq_sync_failed_at,omitempty" json:"hq_sync_failed_at,omitempty"`
	CreatedAt      time.Time  `bson:"created_at" json:"created_at"`
}

type AuditEntry struct {
	ID        bson.ObjectID `bson:"_id,omitempty" json:"-"`
	Actor     string        `bson:"actor" json:"actor"`
	Action    string        `bson:"action" json:"action"`
	AccountID string        `bson:"account_id,omitempty" json:"account_id,omitempty"`
	Target    string        `bson:"target,omitempty" json:"target,omitempty"`
	Detail    string        `bson:"detail,omitempty" json:"detail,omitempty"`
	IP        string        `bson:"ip,omitempty" json:"ip,omitempty"`
	CreatedAt time.Time     `bson:"created_at" json:"created_at"`
}
