package entity

import (
	"time"

	"github.com/shopspring/decimal"
)

const (
	PricingStatusToMerge     = "to_merge"
	PricingStatusMerging     = "merging"
	PricingStatusMerged      = "merged"
	PricingStatusMergeFailed = "merge_failed"
)

type PricingEvent struct {
	ID              int64           `gorm:"primaryKey;autoIncrement"`
	CostID          string          `gorm:"type:varchar(128);not null;index:idx_cost_id"`
	TenantID        uint            `gorm:"not null"`
	CostTimestamp   int64           `gorm:"not null;index:idx_cost_timestamp"`
	CostPrice       decimal.Decimal `gorm:"type:decimal(30,15);not null;default:0"`
	RawPrice        decimal.Decimal `gorm:"type:decimal(30,15);not null;default:0"`
	ConsumedSource  string          `gorm:"type:varchar(32);not null;default:''"`
	Internal        bool            `gorm:"not null;default:false"`
	BindedPackageID uint            `gorm:"not null;default:0"`
	BillingConfigID uint            `gorm:"not null;default:0"`
	Status          string          `gorm:"type:varchar(32);not null;default:'to_merge';index:idx_status_id"`
	MergedCostID    string          `gorm:"type:varchar(128);index:idx_merged_cost_id"`
	PricingDetail   string          `gorm:"type:text"`
	CreatedAt       time.Time       `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
	UpdatedAt       time.Time       `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
}

func (PricingEvent) TableName() string {
	return "pricing_events"
}

const (
	ComparisonKindMatch         = "match"
	ComparisonKindMismatch      = "mismatch"
	ComparisonKindNotFound      = "not_found"
	ComparisonKindInvalidDetail = "invalid_detail"
	// ComparisonKindReplayArtifact marks a mismatch-shaped divergence
	// that the replay-race fingerprint (pricing_replay_classify.go)
	// fully explained: the old path priced the event against state S0,
	// the shadow re-price saw S1 (the old path's own deduction plus
	// concurrent ones already landed), and the pipeline's choice was
	// verified consistent with S1 by replaying the selection ladder
	// over the persisted candidate snapshot. Rows are still written
	// with both sides in full; they are excluded from the mismatch
	// signal so the switchover blocker only counts real divergences.
	ComparisonKindReplayArtifact = "replay_artifact"
)

// PricingComparisonLog records one shadow-run comparison per cost_id:
// the pipeline-side pricing output and the old-path raw row are each
// preserved in full as JSON (pricing / raw), with the verdict (Kind)
// and per-field diffs (Diffs, JSON array, empty for match):
// [{"field":"cost_price","pipeline":"4.5","raw":"9.9"}].
//
// Shadow runs never persist to pricing_events — this table is their
// only footprint, so pricing_events carries authoritative rows only
// and the merge worker never sees comparison traffic.
type PricingComparisonLog struct {
	ID        int64     `gorm:"primaryKey;autoIncrement"`
	CostID    string    `gorm:"type:varchar(128);not null;index:idx_pcl_cost_id"`
	TenantID  uint      `gorm:"not null"`
	Kind      string    `gorm:"type:varchar(16);not null"`
	Pricing   string    `gorm:"type:text"` // complete pipeline-side pricing output, JSON
	Raw       string    `gorm:"type:text"` // complete old-path raw_user_unit_cost row, JSON (empty for not_found)
	Diffs     string    `gorm:"type:text"`
	CreatedAt time.Time `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3);index:idx_pcl_created_at"`
}

func (PricingComparisonLog) TableName() string {
	return "pricing_comparison_logs"
}

type TenantUnit struct {
	ID              uint                   `gorm:"primaryKey;autoIncrement"`
	TenantID        uint                   `gorm:"not null;default:0;index:idx_tenant_enabled"`
	PriceType       string                 `gorm:"type:varchar(64);not null;default:'normal'"`
	PriceExpression map[string]interface{} `gorm:"type:json;serializer:json"`
	Enabled         bool                   `gorm:"not null;default:true;index:idx_tenant_enabled"`
	UnitFilters     []*UnitFilter          `gorm:"many2many:tenant_unit_filter;"`
	CreatedAt       time.Time              `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
	UpdatedAt       time.Time              `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
}

func (*TenantUnit) TableName() string {
	return "tenant_units"
}

type TenantUsage struct {
	ID              uint                   `gorm:"primaryKey;autoIncrement"`
	TenantID        uint                   `gorm:"not null;default:0;index:idx_tenant_enabled"`
	UsageType       string                 `gorm:"type:varchar(64);not null"`
	UsageExpression map[string]interface{} `gorm:"type:json;serializer:json"`
	Enabled         bool                   `gorm:"not null;default:true;index:idx_tenant_enabled"`
	UnitFilters     []*UnitFilter          `gorm:"many2many:tenant_usage_filter;"`
	CreatedAt       time.Time              `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
	UpdatedAt       time.Time              `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
}

func (*TenantUsage) TableName() string {
	return "tenant_usages"
}

// FreeCapAccumulator is the generic per-window exemption ledger for
// the Stage-2 free_cap usage adjuster (usage_type=free_cap in
// tenant_usages above). It is the RU-denominated sibling of
// tiered_billing_accumulators (which accumulates money at Stage 5,
// monthly, per tiered rule): this one accumulates usage (RU), at any
// configured window (hourly v1), keyed by policy + tenant + window +
// cluster.
//
// The dimension columns are NOT NULL DEFAULT ” on purpose: MySQL
// unique indexes do not dedupe NULLs, so a nullable dimension would
// let tenant-scope ledgers fragment into unlimited "unique" rows and
// silently inflate the exemption — a classic billing money-loss
// shape. ” is an ordinary value and participates in uniqueness.
type FreeCapAccumulator struct {
	ID         uint            `gorm:"primaryKey;autoIncrement"`
	PolicyID   string          `gorm:"type:varchar(128);not null;default:'';uniqueIndex:uk_acc,priority:1;comment:names the free_cap policy instance (carried in tenant_usages.usage_expression JSON, key policy_id)"`
	TenantID   uint            `gorm:"not null;default:0;uniqueIndex:uk_acc,priority:2"`
	WindowKey  int64           `gorm:"not null;uniqueIndex:uk_acc,priority:3;comment:window start as Unix seconds (hourly = truncated from the event cost_timestamp)"`
	ClusterID  string          `gorm:"type:varchar(128);not null;default:'';uniqueIndex:uk_acc,priority:4;comment:scope=cluster key; '' for tenant scope"`
	Cumulative decimal.Decimal `gorm:"type:decimal(30,15);not null;default:0;comment:exempted usage already consumed in this window"`

	CreatedAt time.Time `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3)"`
	UpdatedAt time.Time `gorm:"type:timestamp(3);not null;default:CURRENT_TIMESTAMP(3);index:idx_age"`
}

func (*FreeCapAccumulator) TableName() string {
	return "free_cap_accumulators"
}

// WorkerMeteringRecords is the generic trigger/watermark ledger for
// derived-fee workers: one row per successfully handed-off trigger
// hour, discriminated by worker_type (the same role the Unit column
// plays in support_plan_metering_records). It exists so a worker's
// catch-up loop never has to scan metering_events — that table has no
// (tenant_id, unit) index and is the largest in the service, while
// this one is tiny and indexed exactly for the watermark seek.
//
// Semantics (agentic plan as the reference worker, design doc §4):
//   - the row is written only AFTER the hour's full trigger set is
//     enqueued (mark-after-pair). A torn enqueue (some events in,
//     the record not written) is retried whole on the next tick —
//     the events' deterministic uk_cost_id absorbs the duplicates;
//   - the watermark is the handed-off point, not the billed point:
//     pricing retries/deadletters are the pipeline's business
//     (same "enqueueing is the commit point" contract as before);
//   - hours strictly older than the watermark are never revisited —
//     the remove→re-add gap-safety argument is unchanged.
type WorkerMeteringRecords struct {
	CostID        string    `gorm:"primarykey;type:varchar(128);comment:deterministic trigger record ID: worker_type + tenant + hour unix"`
	WorkerType    string    `gorm:"type:varchar(64);not null;index:idx_worker_tenant_time,priority:1;comment:worker discriminator, e.g. agentic_plan"`
	TenantID      uint      `gorm:"not null;index:idx_worker_tenant_time,priority:2;comment:tenant ID"`
	CostTimeStamp time.Time `gorm:"not null;index:idx_worker_tenant_time,priority:3;comment:the trigger hour, hour-truncated UTC"`
}

func (*WorkerMeteringRecords) TableName() string {
	return "worker_metering_records"
}
