// Package audit holds the persistence entity + canonical constants for
// the HLD-010 audit trail. One row per (actor, action, resource, outcome)
// observation. The whole point of this table is to be unforgeable record-
// keeping for "who did what" — code in this package must never mutate or
// delete rows except via the retention job.
package audit

import "time"

// Log is one audit observation.
//
// Field-level notes:
//
//   - UserID is *uint64 so the unauthenticated-actor path (failed login,
//     anon endpoint) can write NULL and still satisfy the FK semantics
//     a future migration may add.
//   - All TEXT columns are NOT NULL but **without** a GORM-level
//     default:'' tag — MySQL 8 rejects DEFAULT on TEXT (Error 1101), as
//     tripped by v0.7.43's investigation_report migration. Go's zero
//     value for string is "" so NOT NULL is satisfied at insert time.
//   - PayloadJSON is the only free-form field. The biz layer must
//     redact secrets BEFORE serialising (LLM keys, SSH keys, passwords,
//     tokens). What lives here is the **shape** of the change, not the
//     values — e.g. {"field":"openai_api_key","action":"set","hint":"sk-...XYZ"}.
//   - RequestID is the same id stamped onto the request's structured
//     log entries by the chi RequestID middleware (or generated by the
//     audit middleware itself when no upstream id exists). Lets an
//     operator join an audit row back to slog / future tracing output.
type Log struct {
	ID            uint64    `gorm:"primaryKey;autoIncrement"`
	OccurredAt    time.Time `gorm:"not null;index:idx_audit_occurred"`
	UserID        *uint64   `gorm:"index:idx_audit_user,priority:1"`
	UserEmail     string    `gorm:"size:255;not null"`
	Role          string    `gorm:"size:16;not null"`
	IP            string    `gorm:"size:45;not null"`
	UserAgent     string    `gorm:"size:512;not null"`
	Action        string    `gorm:"size:64;not null;index:idx_audit_action,priority:1"`
	ResourceType  string    `gorm:"size:32;not null;index:idx_audit_resource,priority:1"`
	ResourceID    string    `gorm:"size:128;not null;index:idx_audit_resource,priority:2"`
	ResourceName  string    `gorm:"size:256;not null"`
	Status        string    `gorm:"size:16;not null;index:idx_audit_status,priority:1"`
	ErrorCode     string    `gorm:"size:64;not null"`
	ErrorMessage  string    `gorm:"size:512;not null"`
	PayloadJSON   string    `gorm:"type:text"`
	RequestID     string    `gorm:"size:64;not null"`
	CreatedAt     time.Time `gorm:"autoCreateTime"`
}

// TableName pins the table so a package rename doesn't silently create a
// parallel empty table.
func (Log) TableName() string { return "audit_logs" }

// Status enum — bounded set used by both the writer and the list filter.
const (
	StatusSuccess = "success"
	StatusFailure = "failure"
	StatusDenied  = "denied"
)

// Canonical actions. The naming convention is verb_resource in
// snake_case — kept deliberately small. Sub-flavours (enable vs disable,
// role-change vs password-reset, single vs bulk) live in the payload,
// not the action name, so the UI's action dropdown stays short and the
// audit row's payload tells the operator exactly what changed.
//
// Operator feedback 2026-05-20: the prior 38-action enum mixed
// CRUD verbs with state-transition flavours (rule_enable / rule_disable)
// and per-setting names (llm_key_set / grafana_config_set), which made
// the action filter sprawl. The current set collapses those to the
// underlying verb + a payload that carries the specifics.
const (
	// Note 2026-05-21: auth_login / auth_logout / audit_view dropped.
	// Operator flagged read-only / session-bookkeeping rows as drowning
	// out the mutation signal. We keep auth_login_failed because a
	// brute-force pattern still wants to be visible.
	ActionAuthLoginFailed = "auth_login_failed"

	// User CRUD. role / password / profile field changes all surface as
	// user_update — the payload field carries which field flipped.
	ActionUserCreate = "user_create"
	ActionUserUpdate = "user_update"
	ActionUserDelete = "user_delete"
	ActionUserExport = "user_export"

	// Device CRUD. enable / disable / bulk-delete fold into update /
	// delete + a payload (e.g. {"enabled": false, "count": 3}).
	ActionDeviceUpdate = "device_update"
	ActionDeviceDelete = "device_delete"

	// Alert rule CRUD. enable / disable fold into update with payload
	// {"enabled": <bool>}.
	ActionRuleCreate = "rule_create"
	ActionRuleUpdate = "rule_update"
	ActionRuleDelete = "rule_delete"

	ActionIncidentAck     = "incident_ack"
	ActionIncidentResolve = "incident_resolve"
	ActionIncidentSilence = "incident_silence"

	// Settings umbrella. LLM key / Grafana config / SSH key writes all
	// land here; payload carries {"key": "...", "category": "..."}.
	// Sensitive values are redacted upstream.
	ActionSettingUpdate = "setting_update"
	ActionSettingDelete = "setting_delete"

	ActionChannelCreate = "channel_create"
	ActionChannelUpdate = "channel_update"
	ActionChannelDelete = "channel_delete"

	ActionRepoCreate = "repo_create"
	ActionRepoDelete = "repo_delete"
	ActionRepoSync   = "repo_sync"

	ActionSkillInstall   = "skill_install"
	ActionSkillUninstall = "skill_uninstall"
)

// ResourceType buckets used in the resource_type column. Same flat-list
// convention as Action — group in the UI, not the data.
const (
	ResourceUser     = "user"
	ResourceDevice   = "device"
	ResourceIncident = "incident"
	ResourceSetting  = "setting"
	ResourceRule     = "rule"
	ResourceChannel = "channel"
	ResourceRepo    = "repo"
	ResourceSkill   = "skill"
	ResourceLLM     = "llm"
	ResourceGitKey  = "git_ssh_key"
	ResourceGrafana = "grafana"
	ResourceRAG     = "rag"
	ResourceAudit   = "audit"
	ResourceAuth    = "auth"
)
