package store

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

// User mirrors the users table.
type User struct {
	ID                 string
	Username           string
	PasswordHash       string
	Role               Role
	Email              *string    // optional; nil = not set
	DisabledAt         *time.Time // nil = enabled
	MustChangePassword bool
	// AuthSource is "local" (created by admin or bootstrap) or
	// "oidc" (JIT-provisioned on first OIDC sign-in). Local users
	// authenticate via password; OIDC users via the IdP and have an
	// empty PasswordHash.
	AuthSource string
	// OIDCSubject is the stable 'sub' claim from the IdP. Set only
	// when AuthSource == "oidc". Used for fast lookup on subsequent
	// sign-ins; the username/email may change at the IdP but sub
	// stays stable.
	OIDCSubject *string
	CreatedAt   time.Time
	LastLoginAt *time.Time
}

// Role enumerates the access tiers from spec.md §7.2.
type Role string

// Defined Role values, in descending order of privilege.
const (
	RoleAdmin    Role = "admin"
	RoleOperator Role = "operator"
	RoleViewer   Role = "viewer"
)

// Session mirrors the sessions table. The ID is the (raw) session
// token; the DB stores its hash. Callers that hold a *Session have
// already authenticated.
type Session struct {
	ID        string
	UserID    string
	CreatedAt time.Time
	ExpiresAt time.Time
	IP        string
	UA        string
	// IDToken is the OIDC id_token captured at sign-in for OIDC
	// sessions; empty for local-user sessions. Used as
	// id_token_hint on RP-initiated logout.
	IDToken string
}

// Host mirrors the hosts table. The P2 redesign moved repo-related
// flags out (auto-init replaces RepoInitialisedAt; bandwidth lives
// here as a host-wide cap; "what to back up" lives on source_groups).
type Host struct {
	ID                     string
	Name                   string
	OS                     string
	Arch                   string
	AgentVersion           string
	ResticVersion          string
	ProtocolVersion        int
	EnrolledAt             time.Time
	LastSeenAt             *time.Time
	Status                 string
	RepoID                 *string
	Tags                   []string
	CurrentJobID           *string
	LastBackupAt           *time.Time
	LastBackupStatus       *string
	RepoSizeBytes          int64
	SnapshotCount          int
	OpenAlertCount         int
	AppliedScheduleVersion int64
	// Host-wide bandwidth caps applied to every restic invocation
	// (backup, restore, prune). nil = no cap.
	BandwidthUpKBps   *int
	BandwidthDownKBps *int

	// PreHookDefault / PostHookDefault are AEAD ciphertext (string
	// blob produced by crypto.AEAD.Encrypt). Per source group hooks
	// (SourceGroup.PreHook / PostHook) override these when set.
	// Empty = no default configured.
	PreHookDefault  string
	PostHookDefault string

	// RepoStatus tracks the outcome of the last init/probe attempt:
	// "unknown" (default), "ready", or "init_failed". Set by the WS
	// handler on every job.finished of kind=init, and reset to
	// "unknown" by repo-credentials saves so the next dispatch
	// re-tests the new creds. RepoStatusError carries the trimmed
	// agent-side message when RepoStatus == "init_failed".
	RepoStatus      string
	RepoStatusError string

	// AlwaysOn is true for 24x7 server hosts (the default). When false
	// the host is intermittent (laptop/workstation): offline alerts are
	// suppressed, the UI shows an "asleep" state, and a missed backup is
	// caught up ~1 min after reconnect. See the always-on-host-mode spec.
	AlwaysOn bool
}

// Schedule is now intentionally slim: cron + which groups + enabled.
// The "what" lives on SourceGroup (paths, excludes, retention, retry).
// The "kind" of operation a schedule can drive is implicit — backup
// only. forget/prune/check are repo-level cadences on
// HostRepoMaintenance, not schedule kinds.
type Schedule struct {
	ID        string
	HostID    string
	CronExpr  string
	Enabled   bool
	CreatedAt time.Time
	UpdatedAt time.Time
	// SourceGroupIDs is populated by ListSchedulesByHost (joins
	// schedule_source_groups) and accepted on Create / Update so the
	// caller passes the desired junction state in one shape.
	SourceGroupIDs []string
}

// SourceGroup is the new home for "what gets backed up." A named
// bundle of include + exclude paths plus a retention policy. Group
// name doubles as the snapshot tag (restic --tag <name>) so retention
// can target it via `restic forget --tag`.
type SourceGroup struct {
	ID                  string
	HostID              string
	Name                string
	Includes            []string
	Excludes            []string
	RetentionPolicy     RetentionPolicy
	RetryMax            int
	RetryBackoffSeconds int
	// ConflictDimension is the cached name of the failing keep-* on
	// a granularity↔cadence mismatch (e.g. "hourly" when keep-hourly
	// is set but no schedule pointing at this group fires sub-daily).
	// Empty means no conflict. Refreshed on every schedule + group CRUD.
	ConflictDimension string
	CreatedAt         time.Time
	UpdatedAt         time.Time

	// PreHook / PostHook are AEAD ciphertext (string blob produced by
	// crypto.AEAD.Encrypt). Empty means "no hook configured."
	// Encryption/decryption happens at the HTTP layer (where AEAD
	// lives); the store layer just persists the bytes verbatim.
	PreHook  string
	PostHook string
}

// RetentionPolicy is the typed view of `restic forget --keep-*`.
// All fields nullable; empty struct = "keep everything for this group."
type RetentionPolicy struct {
	KeepLast    *int `json:"keep_last,omitempty"`
	KeepHourly  *int `json:"keep_hourly,omitempty"`
	KeepDaily   *int `json:"keep_daily,omitempty"`
	KeepWeekly  *int `json:"keep_weekly,omitempty"`
	KeepMonthly *int `json:"keep_monthly,omitempty"`
	KeepYearly  *int `json:"keep_yearly,omitempty"`
}

// Summary renders a compact human view — "last=7, d=14, w=4" or
// "—" when nothing is set. Used by templates and logs.
func (p RetentionPolicy) Summary() string {
	parts := []string{}
	for _, kv := range []struct {
		k string
		v *int
	}{
		{"last", p.KeepLast},
		{"h", p.KeepHourly},
		{"d", p.KeepDaily},
		{"w", p.KeepWeekly},
		{"m", p.KeepMonthly},
		{"y", p.KeepYearly},
	} {
		if kv.v != nil {
			parts = append(parts, fmt.Sprintf("%s=%d", kv.k, *kv.v))
		}
	}
	if len(parts) == 0 {
		return "—"
	}
	return strings.Join(parts, ", ")
}

// HostRepoMaintenance carries the host-level cron cadences for the
// three repo-wide verbs (forget / prune / check). 1:1 with hosts;
// row is auto-created at host enrolment with sensible defaults.
type HostRepoMaintenance struct {
	HostID         string
	ForgetCron     string
	ForgetEnabled  bool
	PruneCron      string
	PruneEnabled   bool
	CheckCron      string
	CheckEnabled   bool
	CheckSubsetPct int
}

// PendingRun queues a missed cron tick (agent was offline) for the
// server-side retry ticker to dispatch later.
type PendingRun struct {
	ID            string
	ScheduleID    string
	SourceGroupID string
	HostID        string
	Attempt       int
	NextAttemptAt time.Time
	ScheduledAt   time.Time // original cron tick — forensic / audit
	LastError     string
}

// FleetUpdate captures one "update all" invocation. Status moves
// running → one of {completed, halted, cancelled}. CurrentHostID
// tracks the host the worker is actively waiting on; cleared (empty)
// outside an active dispatch.
type FleetUpdate struct {
	ID              string
	StartedAt       time.Time
	StartedByUserID string
	TargetVersion   string
	Status          string
	CurrentHostID   string
	HaltedReason    string
	CompletedAt     *time.Time
}

// FleetUpdateHost is one host's slot in a fleet update. Position is
// the iteration order. JobID is set once the worker has dispatched
// command.update for this host; FailedReason on a failed/halted row.
type FleetUpdateHost struct {
	FleetUpdateID string
	HostID        string
	Position      int
	Status        string
	JobID         string
	FailedReason  string
}

// EnrollmentToken is the issuer's view of a one-time token.
type EnrollmentToken struct {
	Raw       string
	TokenHash string
	CreatedAt time.Time
	ExpiresAt time.Time
}

// Alert mirrors the alerts table.
type Alert struct {
	ID             string
	HostID         *string
	Kind           string
	DedupKey       string // empty for one-per-host alerts; source-group id otherwise
	Severity       string
	Message        string
	CreatedAt      time.Time
	LastSeenAt     *time.Time
	AcknowledgedAt *time.Time
	AcknowledgedBy *string
	ResolvedAt     *time.Time
}

// AuditEntry mirrors the audit_log table.
type AuditEntry struct {
	ID         string
	UserID     *string
	Actor      string
	Action     string
	TargetKind *string
	TargetID   *string
	TS         time.Time
	Payload    json.RawMessage
}

// SetupToken mirrors the user_setup_tokens table. The raw token
// itself is never stored; the field shown here is the sha256 hex
// digest of the raw token, which is what callers compare against.
type SetupToken struct {
	UserID    string
	TokenHash string
	ExpiresAt time.Time
	CreatedAt time.Time
	CreatedBy *string // admin user id; nil only after CASCADE SET NULL
}
