// Package models holds the database row types for paliad.* tables.
// Names are English throughout; only user-facing i18n strings live in the
// frontend. See internal/db/migrations/ for the canonical schema definitions.
package models

import (
	"encoding/json"
	"time"

	"github.com/google/uuid"
	"github.com/lib/pq"

	"mgit.msbls.de/m/paliad/pkg/litigationplanner"
)

// NullableJSON is a jsonb column that may be NULL. Canonical definition
// (with sql.Scanner / driver.Valuer / json.Marshaler / json.Unmarshaler)
// lives in pkg/litigationplanner — kept here as a type alias so every
// existing models.NullableJSON reference continues to compile.
type NullableJSON = litigationplanner.NullableJSON

// User extends auth.users with firm-specific profile fields. Created by the
// Phase D onboarding flow; without a row here, the user can't see any Projects.
type User struct {
	ID          uuid.UUID `db:"id"            json:"id"`
	Email       string    `db:"email"         json:"email"`
	DisplayName string    `db:"display_name"  json:"display_name"`
	Office      string    `db:"office"        json:"office"`
	// AdditionalOffices lists secondary offices a partner works across.
	// Informational only — office is not a visibility gate under the v2
	// data model (t-paliad-024).
	AdditionalOffices pq.StringArray  `db:"additional_offices" json:"additional_offices"`
	PracticeGroup     *string         `db:"practice_group"     json:"practice_group,omitempty"`
	// JobTitle is free-text display only ("Partner", "Counsel", "PA",
	// "Counsel Knowledge Lawyer", …). NULL is allowed for users who never
	// picked a title — typically global admins promoted via SQL.
	JobTitle *string `db:"job_title" json:"job_title"`
	// Profession is the structured firm-tier enum that drives the
	// t-paliad-138 / t-paliad-148 approval ladder (partner / of_counsel /
	// associate / senior_pa / pa / paralegal). NULL means "no firm tier"
	// — external collaborators (local counsel, expert) and admin
	// accounts that aren't practicing lawyers. NULL → ladder level 0,
	// ineligible to approve. Distinct from JobTitle (display) and
	// GlobalRole (tool admin gate). Added by migration 057.
	Profession *string `db:"profession" json:"profession,omitempty"`
	// GlobalRole is the global-permissions enum: 'standard' | 'global_admin'.
	// Drives every permission gate that used to look at the legacy
	// role='admin'. Per-project authority is on paliad.project_teams and
	// users.profession; this column is the tool-admin axis, unrelated.
	GlobalRole string `db:"global_role" json:"global_role"`
	Lang              string          `db:"lang"               json:"lang"`
	EmailPreferences  json.RawMessage `db:"email_preferences"  json:"email_preferences"`
	// ReminderMorningTime / ReminderEveningTime are stored as Postgres TIME and
	// scanned as strings in HH:MM:SS form so we don't need a separate type and
	// the JSON shape stays trivially editable from the settings page.
	ReminderMorningTime string    `db:"reminder_morning_time" json:"reminder_morning_time"`
	ReminderEveningTime string    `db:"reminder_evening_time" json:"reminder_evening_time"`
	ReminderTimezone    string    `db:"reminder_timezone"     json:"reminder_timezone"`
	// ReminderWarningOffsetDays controls how many days before each pending
	// deadline the heads-up section ("In einer Woche fällig") fires. Default
	// 7. Range 1..30 enforced by a CHECK constraint in migration 025.
	ReminderWarningOffsetDays int `db:"reminder_warning_offset_days" json:"reminder_warning_offset_days"`
	// EscalationContactID is an optional override of the escalation channel
	// for overdue / DRINGEND mail. NULL means "fall back to global_admins".
	// Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
	EscalationContactID *uuid.UUID `db:"escalation_contact_id" json:"escalation_contact_id,omitempty"`
	// ForumPref is the user's persisted Fristenrechner inbox-channel
	// preference (#15): "cms" → UPC; "bea" → national-DE;
	// "posteingang" → national-DE (slower channel, same forums). NULL =
	// no preference. URL ?inbox= overrides per-visit.
	ForumPref *string   `db:"forum_pref" json:"forum_pref,omitempty"`
	CreatedAt time.Time `db:"created_at" json:"created_at"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

// Project is one node in the paliad.projects tree. Visibility is team-based
// (direct or inherited via the materialised path) — see paliad.can_see_project.
// Type-specific fields are nullable; the service layer enforces the subset
// that applies to each type.
type Project struct {
	ID       uuid.UUID  `db:"id"        json:"id"`
	Type     string     `db:"type"      json:"type"`
	ParentID *uuid.UUID `db:"parent_id" json:"parent_id,omitempty"`
	// Path is the '.'-joined UUID list from root to self (inclusive).
	// Maintained by a Postgres trigger — writes from the service are ignored.
	Path        string     `db:"path"        json:"path"`
	Title       string     `db:"title"       json:"title"`
	Reference   *string    `db:"reference"   json:"reference,omitempty"`
	Description *string    `db:"description" json:"description,omitempty"`
	Status      string     `db:"status"      json:"status"`
	CreatedBy   *uuid.UUID `db:"created_by"  json:"created_by,omitempty"`

	// Client-specific (type='client'), nullable otherwise.
	Industry         *string `db:"industry"          json:"industry,omitempty"`
	Country          *string `db:"country"           json:"country,omitempty"`
	BillingReference *string `db:"billing_reference" json:"billing_reference,omitempty"`

	// ClientMatter numbers — external billing/DMS identifiers used by the firm.
	// Child rows inherit client_number from the root by default (resolved at
	// read time by the service); a child with its own client_number overrides.
	// matter_number is assigned independently at any level.
	ClientNumber    *string `db:"client_number"    json:"client_number,omitempty"`
	MatterNumber    *string `db:"matter_number"    json:"matter_number,omitempty"`
	NetDocumentsURL *string `db:"netdocuments_url" json:"netdocuments_url,omitempty"`

	// Patent-specific (type='patent').
	PatentNumber *string    `db:"patent_number" json:"patent_number,omitempty"`
	FilingDate   *time.Time `db:"filing_date"   json:"filing_date,omitempty"`
	GrantDate    *time.Time `db:"grant_date"    json:"grant_date,omitempty"`

	// Case-specific (type='case').
	Court            *string `db:"court"              json:"court,omitempty"`
	CaseNumber       *string `db:"case_number"        json:"case_number,omitempty"`
	ProceedingTypeID *int    `db:"proceeding_type_id" json:"proceeding_type_id,omitempty"`

	// OurSide is which side the firm represents on this project. Used
	// by the Fristenrechner Determinator to predefine the perspective
	// chip from the project context (t-paliad-164). NULL = unknown /
	// not set; Determinator falls back to free-pick.
	//
	// Allowed sub-roles (mig 112, t-paliad-222):
	//   Active   : claimant, applicant, appellant
	//   Reactive : defendant, respondent
	//   Other    : third_party, other
	//
	// The DB column name stays as `our_side`; the UI label has moved
	// to "Client Role" / "Mandantenrolle" on case projects and is
	// hidden on every other project type.
	OurSide *string `db:"our_side" json:"our_side,omitempty"`

	// OpponentCode is the short slug for the opposing party on a
	// litigation project (uppercase letters / digits / dashes, max 16
	// chars). Used as the middle segment when services.BuildProjectCode
	// assembles an auto-derived project code from the ancestor tree —
	// e.g. EXMPL.OPNT.567.INF.CFI (t-paliad-222 / m/paliad#50). NULL
	// → segment skipped silently. Only meaningful on type='litigation'
	// rows; CHECK constraint (mig 113) enforces the pairing.
	OpponentCode *string `db:"opponent_code" json:"opponent_code,omitempty"`

	// Code is the auto-derived (or override) project code, computed at
	// projection time by services.BuildProjectCode. Not a DB column —
	// no `db:` tag — populated by service-layer projection helpers
	// after the row is loaded. Empty on rows for which the helper has
	// not run (e.g. raw fixtures in tests, internal projection paths
	// that don't call the helper).
	Code string `db:"-" json:"code,omitempty"`

	// CounterclaimOf is the parent project this row is a counterclaim
	// (CCR) against (t-paliad-174 SmartTimeline Slice 3). NULL on
	// regular projects; non-NULL rows are CCR sub-projects rendered as
	// the parallel right-track on the parent's SmartTimeline. parent_id
	// keeps governing the project tree — the CCR child is placed as a
	// sibling under the same patent (§4.4 of the design doc).
	CounterclaimOf *uuid.UUID `db:"counterclaim_of" json:"counterclaim_of,omitempty"`

	// InstanceLevel is the procedural instance the project sits at:
	// 'first' (default) | 'appeal' | 'cassation'. Combined with the
	// proceeding code + jurisdiction by FristenrechnerService to pick
	// the effective proceeding (de.inf.lg + appeal → de.inf.olg, etc.).
	// NULL = unset / not applicable; the calculator treats NULL as
	// 'first'. Backfill happens via the project-detail picker UI
	// (Phase 3 Slice 8); this column ships in Slice 1 ahead of the
	// service rewrite (mig 080, t-paliad-182).
	InstanceLevel *string `db:"instance_level" json:"instance_level,omitempty"`

	Metadata  json.RawMessage `db:"metadata"   json:"metadata"`
	AISummary *string         `db:"ai_summary" json:"ai_summary,omitempty"`
	CreatedAt time.Time       `db:"created_at" json:"created_at"`
	UpdatedAt time.Time       `db:"updated_at" json:"updated_at"`
}

// ProjectTeamMember is one row of paliad.project_teams — direct membership
// only. Inherited memberships are computed at read time by walking the path;
// services set Inherited=true on the in-memory copy when annotating a list
// result that mixes direct + inherited rows.
//
// t-paliad-148 split: Responsibility is the per-project role (lead /
// member / observer / external). The legacy Role field is kept as a
// deprecated read-only shadow until follow-up migration 058 drops the
// underlying column.
type ProjectTeamMember struct {
	ID             uuid.UUID  `db:"id"             json:"id"`
	ProjectID      uuid.UUID  `db:"project_id"     json:"project_id"`
	UserID         uuid.UUID  `db:"user_id"        json:"user_id"`
	Responsibility string     `db:"responsibility" json:"responsibility"`
	// Role: deprecated shadow column. Reader populates it for backwards-
	// compatibility with any consumer still reading `role`; new code
	// should read .Responsibility instead.
	Role      string     `db:"role"       json:"role"`
	Inherited bool       `db:"inherited"  json:"inherited"`
	AddedBy   *uuid.UUID `db:"added_by"   json:"added_by,omitempty"`
	CreatedAt time.Time  `db:"created_at" json:"created_at"`
}

// ProjectTeamMemberWithUser enriches a team row with display fields so the
// UI can render "<DisplayName> (<Email>) — <Responsibility>" without a
// per-row lookup. Used by TeamService.ListMembers which unions direct +
// inherited memberships.
//
// UserProfession reflects paliad.users.profession at read time — the
// firm-tier badge shown next to the responsibility column on
// /projects/{id} (t-paliad-148 §6).
type ProjectTeamMemberWithUser struct {
	ProjectTeamMember
	UserEmail       string  `db:"user_email"        json:"user_email"`
	UserDisplayName string  `db:"user_display_name" json:"user_display_name"`
	UserOffice      string  `db:"user_office"       json:"user_office"`
	UserProfession  *string `db:"user_profession"   json:"user_profession,omitempty"`
	// InheritedFromID is the ancestor project_id the membership came from
	// when Inherited=true. NULL for direct rows.
	InheritedFromID    *uuid.UUID `db:"inherited_from_id"    json:"inherited_from_id,omitempty"`
	InheritedFromTitle *string    `db:"inherited_from_title" json:"inherited_from_title,omitempty"`
}

// PartnerUnit is one structural partner unit (Dezernat in legacy German).
// Membership is orthogonal to project teams — a user typically belongs to
// exactly one PartnerUnit but may work on projects across all of them.
type PartnerUnit struct {
	ID         uuid.UUID  `db:"id"           json:"id"`
	Name       string     `db:"name"         json:"name"`
	LeadUserID *uuid.UUID `db:"lead_user_id" json:"lead_user_id,omitempty"`
	Office     string     `db:"office"       json:"office"`
	CreatedAt  time.Time  `db:"created_at"   json:"created_at"`
	UpdatedAt  time.Time  `db:"updated_at"   json:"updated_at"`
}

// PartnerUnitMember is one user's membership in a PartnerUnit.
type PartnerUnitMember struct {
	PartnerUnitID uuid.UUID `db:"partner_unit_id" json:"partner_unit_id"`
	UserID        uuid.UUID `db:"user_id"         json:"user_id"`
	CreatedAt     time.Time `db:"created_at"      json:"created_at"`
}

// ProjectEvent is one row in the per-Project audit trail
// (paliad.project_events, renamed from paliad.project_events in migration 018).
//
// ProjectTitle is populated only by readers that join paliad.projects (e.g.
// ProjectService.ListEvents — Verlauf attribution for descendant events on
// /projects/{id}, t-paliad-139). Other readers leave it nil and the JSON
// serialiser omits it.
type ProjectEvent struct {
	ID           uuid.UUID       `db:"id"            json:"id"`
	ProjectID    uuid.UUID       `db:"project_id"    json:"project_id"`
	EventType    *string         `db:"event_type"    json:"event_type,omitempty"`
	Title        string          `db:"title"         json:"title"`
	Description  *string         `db:"description"   json:"description,omitempty"`
	EventDate    *time.Time      `db:"event_date"    json:"event_date,omitempty"`
	CreatedBy    *uuid.UUID      `db:"created_by"    json:"created_by,omitempty"`
	Metadata     json.RawMessage `db:"metadata"      json:"metadata"`
	CreatedAt    time.Time       `db:"created_at"    json:"created_at"`
	UpdatedAt    time.Time       `db:"updated_at"    json:"updated_at"`
	ProjectTitle *string         `db:"project_title" json:"project_title,omitempty"`
}

// Deadline is one persistent deadline attached to a Project (typically a
// case- or patent-level node). Visibility is inherited from the parent
// Project via paliad.can_see_project.
type Deadline struct {
	ID              uuid.UUID  `db:"id"                json:"id"`
	ProjectID       uuid.UUID  `db:"project_id"        json:"project_id"`
	Title           string     `db:"title"             json:"title"`
	Description     *string    `db:"description"       json:"description,omitempty"`
	DueDate         time.Time  `db:"due_date"          json:"due_date"`
	OriginalDueDate *time.Time `db:"original_due_date" json:"original_due_date,omitempty"`
	WarningDate     *time.Time `db:"warning_date"      json:"warning_date,omitempty"`
	Source          string     `db:"source"            json:"source"`
	// Slice B.4 (mig 140, t-paliad-305): paliad.deadlines.rule_id column
	// dropped; the back-link now lives on `sequencing_rule_id` (FK to
	// paliad.sequencing_rules). Same UUID values (sequencing_rules.id
	// inherited deadline_rules.id during mig 136 backfill), so internal
	// Go references to `RuleID` continue to carry the same semantic
	// pointer. The JSON name stays `rule_id` for frontend backward-compat
	// — B.5 will rename if/when frontend is updated.
	RuleID          *uuid.UUID `db:"sequencing_rule_id" json:"rule_id,omitempty"`
	// RuleCode is the legal citation ("RoP.023", "R.151") attached at
	// save time — see migration 032. Free text by design; survives
	// changes to paliad.deadline_rules and accepts citations from
	// outside that table.
	RuleCode        *string    `db:"rule_code"         json:"rule_code,omitempty"`
	// CustomRuleText holds the lawyer's free-text rule label when the
	// deadline form is in Custom mode (t-paliad-258 / m/paliad#89).
	// Mutually exclusive with RuleID at the application layer: the Auto
	// path sets RuleID and leaves this NULL; the Custom path sets this
	// and leaves RuleID NULL. Display surfaces prefer the joined
	// deadline_rules.name when RuleID is set, else fall back to this
	// text + a "Custom" badge.
	CustomRuleText  *string    `db:"custom_rule_text"  json:"custom_rule_text,omitempty"`
	Status          string     `db:"status"            json:"status"`
	CompletedAt     *time.Time `db:"completed_at"      json:"completed_at,omitempty"`
	CalDAVUID       *string    `db:"caldav_uid"        json:"caldav_uid,omitempty"`
	CalDAVEtag      *string    `db:"caldav_etag"       json:"caldav_etag,omitempty"`
	Notes           *string    `db:"notes"             json:"notes,omitempty"`
	CreatedBy       *uuid.UUID `db:"created_by"        json:"created_by,omitempty"`
	CreatedAt       time.Time  `db:"created_at"        json:"created_at"`
	UpdatedAt       time.Time  `db:"updated_at"        json:"updated_at"`

	// Approval-workflow columns added by migration 054 (t-paliad-138).
	// approval_status: 'approved' (default), 'pending' (a request is in
	// flight; pending_request_id is set), 'legacy' (predates 4-eye).
	// approved_by / approved_at: populated when a 4-eye approval flips
	// the row from 'pending' back to 'approved'. NULL on legacy rows
	// and rows that never went through 4-eye.
	ApprovalStatus    string     `db:"approval_status"    json:"approval_status"`
	PendingRequestID  *uuid.UUID `db:"pending_request_id" json:"pending_request_id,omitempty"`
	ApprovedBy        *uuid.UUID `db:"approved_by"        json:"approved_by,omitempty"`
	ApprovedAt        *time.Time `db:"approved_at"        json:"approved_at,omitempty"`

	// EventTypeIDs lists the paliad.event_types attached to this deadline
	// via the paliad.deadline_event_types junction. Always present (never
	// nil) once the row has been hydrated by DeadlineService.
	EventTypeIDs []uuid.UUID `db:"-" json:"event_type_ids"`
}

// DeadlineWithProject enriches a Deadline with parent-Project display fields
// (reference + title) for list views. RuleName/RuleNameEN are the
// human-readable label of the linked deadline-rule (e.g. "Replik" / "Reply"),
// pulled from the LEFT JOIN on paliad.deadline_rules.rule_id. The
// RuleCode field is inherited from the embedded Deadline (the row's own
// stored citation, see migration 032) — list views render it directly as
// REGEL.
type DeadlineWithProject struct {
	Deadline
	ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
	ProjectTitle     string  `db:"project_title"     json:"project_title"`
	ProjectType      string  `db:"project_type"      json:"project_type"`
	RuleName         *string `db:"rule_name"         json:"rule_name,omitempty"`
	RuleNameEN       *string `db:"rule_name_en"      json:"rule_name_en,omitempty"`
	// RequesterKind is the kind of the in-flight approval request (when
	// approval_status='pending'): 'user' or 'agent' (Paliadin-drafted —
	// t-paliad-161). NULL when the row has no pending request. Powers
	// the ✨ glyph alongside the eye-pill 👀.
	RequesterKind *string `db:"requester_kind" json:"requester_kind,omitempty"`
}

// Appointment is one appointment. project_id is nullable: NULL = personal
// (creator-only); set = follows the parent Project's team visibility.
type Appointment struct {
	ID              uuid.UUID  `db:"id"               json:"id"`
	ProjectID       *uuid.UUID `db:"project_id"       json:"project_id,omitempty"`
	Title           string     `db:"title"            json:"title"`
	Description     *string    `db:"description"      json:"description,omitempty"`
	StartAt         time.Time  `db:"start_at"         json:"start_at"`
	EndAt           *time.Time `db:"end_at"           json:"end_at,omitempty"`
	Location        *string    `db:"location"         json:"location,omitempty"`
	AppointmentType *string    `db:"appointment_type" json:"appointment_type,omitempty"`
	CalDAVUID       *string    `db:"caldav_uid"       json:"caldav_uid,omitempty"`
	CalDAVEtag      *string    `db:"caldav_etag"      json:"caldav_etag,omitempty"`
	CreatedBy       *uuid.UUID `db:"created_by"       json:"created_by,omitempty"`
	CreatedAt       time.Time  `db:"created_at"       json:"created_at"`
	UpdatedAt       time.Time  `db:"updated_at"       json:"updated_at"`

	// CompletedAt is non-NULL once the appointment is marked done. New
	// column added by migration 054 (t-paliad-138) — required to land the
	// appointment:complete lifecycle event.
	CompletedAt *time.Time `db:"completed_at" json:"completed_at,omitempty"`

	// Approval-workflow columns (see Deadline doc above for semantics).
	ApprovalStatus   string     `db:"approval_status"    json:"approval_status"`
	PendingRequestID *uuid.UUID `db:"pending_request_id" json:"pending_request_id,omitempty"`
	ApprovedBy       *uuid.UUID `db:"approved_by"        json:"approved_by,omitempty"`
	ApprovedAt       *time.Time `db:"approved_at"        json:"approved_at,omitempty"`
}

// AppointmentWithProject enriches an Appointment with its parent Project
// display fields for list views. All fields nullable because personal
// Appointments have no parent.
type AppointmentWithProject struct {
	Appointment
	ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
	ProjectTitle     *string `db:"project_title"     json:"project_title,omitempty"`
	ProjectType      *string `db:"project_type"      json:"project_type,omitempty"`
	// RequesterKind: see DeadlineWithProject (t-paliad-161 ✨).
	RequesterKind *string `db:"requester_kind" json:"requester_kind,omitempty"`
}

// Note is one polymorphic note attached to exactly one parent row
// (Project, Deadline, Appointment, or ProjectEvent). Visibility follows the
// parent.
type Note struct {
	ID             uuid.UUID  `db:"id"               json:"id"`
	ProjectID      *uuid.UUID `db:"project_id"       json:"project_id,omitempty"`
	DeadlineID     *uuid.UUID `db:"deadline_id"      json:"deadline_id,omitempty"`
	AppointmentID  *uuid.UUID `db:"appointment_id"   json:"appointment_id,omitempty"`
	ProjectEventID *uuid.UUID `db:"project_event_id" json:"project_event_id,omitempty"`
	Content        string     `db:"content"          json:"content"`
	CreatedBy      *uuid.UUID `db:"created_by"       json:"created_by,omitempty"`
	CreatedAt      time.Time  `db:"created_at"       json:"created_at"`
	UpdatedAt      time.Time  `db:"updated_at"       json:"updated_at"`

	// Author display fields populated by the service's LEFT JOIN to
	// paliad.users so the UI can render "von <Name>" without a lookup.
	AuthorName  *string `db:"author_name"  json:"author_name,omitempty"`
	AuthorEmail *string `db:"author_email" json:"author_email,omitempty"`
}

// ChecklistInstance is one user's instantiation of a checklist template
// (static catalog in internal/checklists OR authored row in
// paliad.checklists). Checkbox state lives in the `state` jsonb column.
//
// Visibility mirrors Appointment: project_id nullable. Personal instances
// (project_id NULL) are creator-only; Project-linked instances follow
// paliad.can_see_project.
//
// TemplateSnapshot captures the template body at instance create time so
// subsequent template edits / visibility narrowing don't affect existing
// instances (t-paliad-225 Slice A). NULL on pre-mig-114 rows; the
// service layer falls back to live catalog lookup in that case.
type ChecklistInstance struct {
	ID               uuid.UUID    `db:"id"                json:"id"`
	TemplateSlug     string       `db:"template_slug"     json:"template_slug"`
	Name             string       `db:"name"              json:"name"`
	ProjectID        *uuid.UUID   `db:"project_id"        json:"project_id,omitempty"`
	State            json.RawMessage `db:"state"          json:"state"`
	CreatedBy        uuid.UUID    `db:"created_by"        json:"created_by"`
	CreatedAt        time.Time    `db:"created_at"        json:"created_at"`
	UpdatedAt        time.Time    `db:"updated_at"        json:"updated_at"`
	TemplateSnapshot NullableJSON `db:"template_snapshot" json:"template_snapshot,omitempty"`
	// TemplateVersion is the checklists.version at instance create time.
	// NULL on pre-Slice-C rows where versioning wasn't captured; the
	// "outdated" badge stays off in that case.
	TemplateVersion *int `db:"template_version" json:"template_version,omitempty"`
}

// ChecklistInstanceWithProject enriches an instance with its parent Project
// reference fields for list views.
type ChecklistInstanceWithProject struct {
	ChecklistInstance
	ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
	ProjectTitle     *string `db:"project_title"     json:"project_title,omitempty"`
}

// Checklist is one authored template row in paliad.checklists. Augments
// the static Go catalog (internal/checklists/templates.go) at read time
// via ChecklistCatalogService. Body holds the groups + items as JSONB.
type Checklist struct {
	ID          uuid.UUID    `db:"id"          json:"id"`
	Slug        string       `db:"slug"        json:"slug"`
	OwnerID     uuid.UUID    `db:"owner_id"    json:"owner_id"`
	Title       string       `db:"title"       json:"title"`
	Description string       `db:"description" json:"description"`
	Regime      string       `db:"regime"      json:"regime"`
	Court       string       `db:"court"       json:"court"`
	Reference   string       `db:"reference"   json:"reference"`
	Deadline    string       `db:"deadline"    json:"deadline"`
	Lang        string       `db:"lang"        json:"lang"`
	Body        json.RawMessage `db:"body"     json:"body"`
	Visibility  string       `db:"visibility"  json:"visibility"`
	PromotedAt  *time.Time   `db:"promoted_at" json:"promoted_at,omitempty"`
	PromotedBy  *uuid.UUID   `db:"promoted_by" json:"promoted_by,omitempty"`
	Version     int          `db:"version"     json:"version"`
	CreatedAt   time.Time    `db:"created_at"  json:"created_at"`
	UpdatedAt   time.Time    `db:"updated_at"  json:"updated_at"`
}

// ChecklistWithOwner enriches a Checklist row with author display fields
// for list views (Meine Vorlagen, Gallery).
type ChecklistWithOwner struct {
	Checklist
	OwnerEmail       string `db:"owner_email"        json:"owner_email"`
	OwnerDisplayName string `db:"owner_display_name" json:"owner_display_name"`
}

// UserCalDAVConfig holds one user's external CalDAV connection. The password
// is never returned in API responses; only the public fields are exposed.
type UserCalDAVConfig struct {
	UserID             uuid.UUID  `db:"user_id"              json:"user_id"`
	URL                string     `db:"url"                  json:"url"`
	Username           string     `db:"username"             json:"username"`
	PasswordEncrypted  []byte     `db:"password_encrypted"   json:"-"`
	CalendarPath       string     `db:"calendar_path"        json:"calendar_path"`
	Enabled            bool       `db:"enabled"              json:"enabled"`
	LastSyncAt         *time.Time `db:"last_sync_at"         json:"last_sync_at,omitempty"`
	LastSyncError      *string    `db:"last_sync_error"      json:"last_sync_error,omitempty"`
	CreatedAt          time.Time  `db:"created_at"           json:"created_at"`
	UpdatedAt          time.Time  `db:"updated_at"           json:"updated_at"`
	// MKCALENDAR-capability tri-state (mig 108, Slice 2c). NULL = unprobed.
	SupportsMKCalendar  *bool      `db:"supports_mkcalendar"   json:"supports_mkcalendar,omitempty"`
	MKCalendarProbedAt  *time.Time `db:"mkcalendar_probed_at"  json:"mkcalendar_probed_at,omitempty"`
}

// CalDAVSyncLogEntry is one historical sync record. BindingID is populated
// for per-binding sync entries written by the post-Slice-2a sync engine;
// older rows have it NULL and the entry covers the user's default binding.
type CalDAVSyncLogEntry struct {
	ID          uuid.UUID  `db:"id"           json:"id"`
	UserID      uuid.UUID  `db:"user_id"      json:"user_id"`
	OccurredAt  time.Time  `db:"occurred_at"  json:"occurred_at"`
	Direction   string     `db:"direction"    json:"direction"`
	ItemsPushed int        `db:"items_pushed" json:"items_pushed"`
	ItemsPulled int        `db:"items_pulled" json:"items_pulled"`
	Error       *string    `db:"error"        json:"error,omitempty"`
	DurationMS  *int       `db:"duration_ms"  json:"duration_ms,omitempty"`
	BindingID   *uuid.UUID `db:"binding_id"   json:"binding_id,omitempty"`
}

// UserCalendarBinding is one of N (calendar, scope) bindings a user can
// configure on top of their single CalDAV server connection. The same
// Appointment can land in multiple bindings (e.g. master + per-project),
// with per-binding push state living in AppointmentCalDAVTarget.
type UserCalendarBinding struct {
	ID              uuid.UUID  `db:"id"               json:"id"`
	UserID          uuid.UUID  `db:"user_id"          json:"user_id"`
	CalendarPath    string     `db:"calendar_path"    json:"calendar_path"`
	DisplayName     string     `db:"display_name"     json:"display_name"`
	ScopeKind       string     `db:"scope_kind"       json:"scope_kind"`
	ScopeID         *uuid.UUID `db:"scope_id"         json:"scope_id,omitempty"`
	IncludePersonal bool       `db:"include_personal" json:"include_personal"`
	Enabled         bool       `db:"enabled"          json:"enabled"`
	LastSyncAt      *time.Time `db:"last_sync_at"     json:"last_sync_at,omitempty"`
	LastSyncError   *string    `db:"last_sync_error"  json:"last_sync_error,omitempty"`
	CreatedAt       time.Time  `db:"created_at"       json:"created_at"`
	UpdatedAt       time.Time  `db:"updated_at"       json:"updated_at"`
}

// Scope-kind enum mirrored from paliad.user_calendar_bindings_scope_kind_chk.
const (
	BindingScopeAllVisible   = "all_visible"
	BindingScopePersonalOnly = "personal_only"
	BindingScopeProject      = "project"
	BindingScopeClient       = "client"
	BindingScopeLitigation   = "litigation"
	BindingScopePatent       = "patent"
	BindingScopeCase         = "case"
)

// AppointmentCalDAVTarget is the per-(appointment, binding) push state.
// The caldav_uid is canonical per Appointment (same value across all of
// an appointment's targets); caldav_etag varies per binding.
type AppointmentCalDAVTarget struct {
	AppointmentID uuid.UUID `db:"appointment_id" json:"appointment_id"`
	BindingID     uuid.UUID `db:"binding_id"     json:"binding_id"`
	CalDAVUID     string    `db:"caldav_uid"     json:"caldav_uid"`
	CalDAVEtag    *string   `db:"caldav_etag"    json:"caldav_etag,omitempty"`
	LastPushedAt  time.Time `db:"last_pushed_at" json:"last_pushed_at"`
}

// Party is a party to a Project (Kläger, Beklagter, etc. — typically on
// a case-level project).
type Party struct {
	ID             uuid.UUID       `db:"id"             json:"id"`
	ProjectID      uuid.UUID       `db:"project_id"     json:"project_id"`
	Name           string          `db:"name"           json:"name"`
	Role           *string         `db:"role"           json:"role,omitempty"`
	Representative *string         `db:"representative" json:"representative,omitempty"`
	ContactInfo    json.RawMessage `db:"contact_info"   json:"contact_info"`
	CreatedAt      time.Time       `db:"created_at"     json:"created_at"`
	UpdatedAt      time.Time       `db:"updated_at"     json:"updated_at"`
}

// DeadlineRule is one rule in the proceeding-rule tree (UPC R.023, etc.).
// Canonical definition lives in pkg/litigationplanner.Rule — kept here
// as a type alias so every existing models.DeadlineRule reference (sqlx
// scans, hydration, projection service) continues to compile.
type DeadlineRule = litigationplanner.Rule

// SequencingRule is the Slice B.5 (t-paliad-305) canonical name for what
// the legacy schema called a "deadline rule". Alias to DeadlineRule so
// existing call-sites compile unchanged while new code can adopt the
// procedural-event vocabulary. Same struct, same db / json tags.
type SequencingRule = DeadlineRule

// ProceduralEvent mirrors paliad.procedural_events — the "what kind of
// step is this in the proceeding" identity row. New struct introduced
// in Slice B.5 (t-paliad-305) for code that needs the procedural-event
// columns alone. Most consumers still pull the merged shape via
// SequencingRule through the paliad.deadline_rules_unified view; this
// struct unlocks per-PE reads/writes without going through the view.
type ProceduralEvent struct {
	ID                  uuid.UUID  `db:"id"                    json:"id"`
	Code                string     `db:"code"                  json:"code"`
	Name                string     `db:"name"                  json:"name"`
	NameEN              string     `db:"name_en"               json:"name_en"`
	Description         *string    `db:"description"           json:"description,omitempty"`
	EventKind           *string    `db:"event_kind"            json:"event_kind,omitempty"`
	PrimaryPartyDefault *string    `db:"primary_party_default" json:"primary_party_default,omitempty"`
	LegalSourceID       *uuid.UUID `db:"legal_source_id"       json:"legal_source_id,omitempty"`
	ConceptID           *uuid.UUID `db:"concept_id"            json:"concept_id,omitempty"`
	LifecycleState      string     `db:"lifecycle_state"       json:"lifecycle_state"`
	DraftOf             *uuid.UUID `db:"draft_of"              json:"draft_of,omitempty"`
	PublishedAt         *time.Time `db:"published_at"          json:"published_at,omitempty"`
	IsActive            bool       `db:"is_active"             json:"is_active"`
	CreatedAt           time.Time  `db:"created_at"            json:"created_at"`
	UpdatedAt           time.Time  `db:"updated_at"            json:"updated_at"`
}

// LegalSource mirrors paliad.legal_sources — the source-of-law citation
// rows that procedural events anchor against. pretty_de / pretty_en are
// nullable on disk; readers fall back to
// internal/services/submission_vars.go:legalSourcePretty when missing.
type LegalSource struct {
	ID           uuid.UUID `db:"id"           json:"id"`
	Citation     string    `db:"citation"     json:"citation"`
	Jurisdiction string    `db:"jurisdiction" json:"jurisdiction"`
	PrettyDE     *string   `db:"pretty_de"    json:"pretty_de,omitempty"`
	PrettyEN     *string   `db:"pretty_en"    json:"pretty_en,omitempty"`
	Notes        *string   `db:"notes"        json:"notes,omitempty"`
	CreatedAt    time.Time `db:"created_at"   json:"created_at"`
	UpdatedAt    time.Time `db:"updated_at"   json:"updated_at"`
}

// DeadlineRuleAudit is one row of paliad.deadline_rule_audit — the
// append-only audit log for every change to paliad.deadline_rules.
// Written by the AFTER-trigger (raw create / update / delete) and by
// the Go rule-editor service (semantic publish / archive / restore).
// See migration 079 and design-fristen-phase2-2026-05-15.md §2.8.
type DeadlineRuleAudit struct {
	ID        uuid.UUID  `db:"id"         json:"id"`
	RuleID    uuid.UUID  `db:"rule_id"    json:"rule_id"`
	ChangedBy *uuid.UUID `db:"changed_by" json:"changed_by,omitempty"`
	ChangedAt time.Time  `db:"changed_at" json:"changed_at"`

	// Action is one of: create | update | delete (trigger-written) |
	// publish | archive | restore (Go-written by the rule editor).
	Action string `db:"action" json:"action"`

	// BeforeJSON is the row state pre-change (NULL on 'create').
	// AfterJSON is the row state post-change (NULL on 'delete').
	BeforeJSON NullableJSON `db:"before_json" json:"before_json,omitempty"`
	AfterJSON  NullableJSON `db:"after_json"  json:"after_json,omitempty"`

	// Reason is required on update / delete (the trigger raises if
	// paliad.audit_reason is unset). On create the trigger defaults
	// to 'create' so seed migrations don't need to bother.
	Reason string `db:"reason" json:"reason"`

	// MigrationExported flips to true once the Slice 11b export
	// endpoint folds this delta into a checked-in .up.sql.
	MigrationExported bool `db:"migration_exported" json:"migration_exported"`
}

// ProceedingType is one of the litigation conceptual codes (INF / REV /
// CCR / APM / APP / AMD / ZPO_CIVIL) or the lowercase dot-separated
// fristenrechner codes (upc.*.*, de.*.*, epa.*.*, dpma.*.*) — see
// docs/design-proceeding-code-taxonomy-2026-05-18.md. Canonical
// definition lives in pkg/litigationplanner.ProceedingType — kept here
// as a type alias so every existing models.ProceedingType reference
// continues to compile.
type ProceedingType = litigationplanner.ProceedingType

// TriggerEvent is a UPC procedural event referenced by deadline rules
// whose semantic anchor is an event rather than a parent rule.
// Canonical definition lives in pkg/litigationplanner.TriggerEvent.
type TriggerEvent = litigationplanner.TriggerEvent

// EventDeadline is a single deadline that flows from a TriggerEvent. Mirrors
// youpc data.deadlines + the trigger half of data.deadline_events.
//
// Composite-rule semantics: when CombineOp is non-nil, the calculator
// computes both (DurationValue, DurationUnit) and (AltDurationValue,
// AltDurationUnit) from the trigger date and applies CombineOp ('max'/'min').
// Used for R.198/R.213 ("31d OR 20 working_days, whichever is longer").
type EventDeadline struct {
	ID               int64     `db:"id"                 json:"id"`
	TriggerEventID   int64     `db:"trigger_event_id"   json:"trigger_event_id"`
	Title            string    `db:"title"              json:"title"`
	TitleDE          string    `db:"title_de"           json:"title_de"`
	DurationValue    int       `db:"duration_value"     json:"duration_value"`
	DurationUnit     string    `db:"duration_unit"      json:"duration_unit"`
	Timing           string    `db:"timing"             json:"timing"`
	Notes            string    `db:"notes"              json:"notes"`
	AltDurationValue *int      `db:"alt_duration_value" json:"alt_duration_value,omitempty"`
	AltDurationUnit  *string   `db:"alt_duration_unit"  json:"alt_duration_unit,omitempty"`
	CombineOp        *string   `db:"combine_op"         json:"combine_op,omitempty"`
	IsActive         bool      `db:"is_active"          json:"is_active"`
	CreatedAt        time.Time `db:"created_at"         json:"created_at"`
	UpdatedAt        time.Time `db:"updated_at"         json:"updated_at"`
}

// EventDeadlineRuleCode is one RoP citation attached to an EventDeadline.
// A single deadline can carry multiple rule codes (e.g. R.029.a + R.030).
type EventDeadlineRuleCode struct {
	EventDeadlineID int64  `db:"event_deadline_id" json:"event_deadline_id"`
	RuleCode        string `db:"rule_code"         json:"rule_code"`
	SortOrder       int    `db:"sort_order"        json:"sort_order"`
}

// EventType is a user-facing categorization tag for a Deadline (Statement
// of Defence, Reply, Decision on the merits, EPO opposition, …). Distinct
// from TriggerEvent: TriggerEvents are calc-engine state (UPC-only,
// verbatim youpc imports), EventTypes are the broader taxonomy users
// pick from when creating a Deadline.
//
// CreatedBy NULL on system seeds; set on user-created rows. IsFirmWide
// true for seeds and any firm-wide row a user explicitly publishes;
// false for personal taxonomy. TriggerEventID is a loose linkage column
// (no FK constraint) populated only for seeded UPC rows.
type EventType struct {
	ID             uuid.UUID  `db:"id"               json:"id"`
	Slug           string     `db:"slug"             json:"slug"`
	LabelDE        string     `db:"label_de"         json:"label_de"`
	LabelEN        string     `db:"label_en"         json:"label_en"`
	Category       string     `db:"category"         json:"category"`
	Jurisdiction   *string    `db:"jurisdiction"     json:"jurisdiction,omitempty"`
	Description    string     `db:"description"      json:"description"`
	TriggerEventID *int64     `db:"trigger_event_id" json:"trigger_event_id,omitempty"`
	CreatedBy      *uuid.UUID `db:"created_by"       json:"created_by,omitempty"`
	IsFirmWide     bool       `db:"is_firm_wide"     json:"is_firm_wide"`
	ArchivedAt     *time.Time `db:"archived_at"      json:"archived_at,omitempty"`
	CreatedAt      time.Time  `db:"created_at"       json:"created_at"`
	UpdatedAt      time.Time  `db:"updated_at"       json:"updated_at"`
}

// EventTypeCategory enumerates the values allowed on event_types.category.
// Mirrors the CHECK constraint in migration 030.
const (
	EventTypeCategorySubmission = "submission"
	EventTypeCategoryDecision   = "decision"
	EventTypeCategoryOrder      = "order"
	EventTypeCategoryService    = "service"
	EventTypeCategoryFee        = "fee"
	EventTypeCategoryHearing    = "hearing"
	EventTypeCategoryOther      = "other"
)

// EventTypeJurisdiction enumerates the values allowed on
// event_types.jurisdiction (NULL is also valid).
const (
	EventTypeJurisdictionUPC  = "UPC"
	EventTypeJurisdictionEPO  = "EPO"
	EventTypeJurisdictionDPMA = "DPMA"
	EventTypeJurisdictionDE   = "DE"
	EventTypeJurisdictionAny  = "any"
)

// ApprovalPolicy is one row of paliad.approval_policies — a rule that says
// "this (entity_type, lifecycle_event) needs 4-eye sign-off at the given
// role tier or above" within a scope. The scope is either a single project
// (ProjectID set, PartnerUnitID nil) OR a single partner unit (PartnerUnitID
// set, ProjectID nil) — XOR enforced by the DB CHECK
// approval_policies_scope_xor.
//
// Project rows act as the most-specific override; partner-unit rows act as
// firm-wide defaults for projects attached to that unit (t-paliad-154).
type ApprovalPolicy struct {
	ID             uuid.UUID  `db:"id"              json:"id"`
	ProjectID      *uuid.UUID `db:"project_id"      json:"project_id,omitempty"`
	PartnerUnitID  *uuid.UUID `db:"partner_unit_id" json:"partner_unit_id,omitempty"`
	EntityType     string     `db:"entity_type"     json:"entity_type"`
	LifecycleEvent string     `db:"lifecycle_event" json:"lifecycle_event"`
	// RequiresApproval is the gate (t-paliad-160). False = lifecycle event
	// auto-passes, no approval_request inserted.
	RequiresApproval bool `db:"requires_approval" json:"requires_approval"`
	// MinRole is the minimum profession tier qualified to approve. NULL
	// (nil) when RequiresApproval=false. Constraint: the two columns are
	// XOR-locked — either (false, NULL) or (true, role).
	MinRole   *string    `db:"min_role"     json:"min_role,omitempty"`
	CreatedAt time.Time  `db:"created_at"   json:"created_at"`
	UpdatedAt time.Time  `db:"updated_at"   json:"updated_at"`
	CreatedBy *uuid.UUID `db:"created_by"   json:"created_by,omitempty"`
}

// EffectivePolicy is the resolved policy for one (project, entity_type,
// lifecycle_event) cell — what the gate actually does, after the
// project-row / ancestor-row / unit-default cascade in
// paliad.approval_policy_effective(). Populated by
// ApprovalService.GetEffectivePoliciesMatrix and the form-time hint
// endpoint.
//
// RequiresApproval is the gate (true iff any candidate demands approval).
// MinRole is the seniority threshold among requires_approval=true
// candidates (nil when the gate is off). Source ∈ {"project", "ancestor",
// "unit_default"} attributes which row supplied the winning value.
// SourceID is the project_id for project / ancestor; partner_unit_id for
// unit_default.
type EffectivePolicy struct {
	EntityType     string `json:"entity_type"`
	LifecycleEvent string `json:"lifecycle_event"`
	// RequiresApproval is the gate (t-paliad-160 split-grammar). True iff
	// the resolver yielded a policy that demands approval.
	RequiresApproval bool `json:"requires_approval"`
	// MinRole is the seniority threshold (NULL when gate is off).
	MinRole    *string    `json:"min_role,omitempty"`
	Source     *string    `json:"source,omitempty"`
	SourceID   *uuid.UUID `json:"source_id,omitempty"`
	SourceName *string    `json:"source_name,omitempty"`
}

// PolicyAuditEntry is one row of paliad.policy_audit_log — admin-only audit
// trail for approval-policy CRUD (t-paliad-154). Surfaces on /admin/audit-log
// via AuditService union; never on per-project /verlauf.
type PolicyAuditEntry struct {
	ID              uuid.UUID  `db:"id"                json:"id"`
	ActorID         uuid.UUID  `db:"actor_id"          json:"actor_id"`
	EventType       string     `db:"event_type"        json:"event_type"`
	ScopeType       string     `db:"scope_type"        json:"scope_type"`
	ProjectID       *uuid.UUID `db:"project_id"        json:"project_id,omitempty"`
	PartnerUnitID   *uuid.UUID `db:"partner_unit_id"   json:"partner_unit_id,omitempty"`
	ScopeName       string     `db:"scope_name"        json:"scope_name"`
	EntityType      string     `db:"entity_type"       json:"entity_type"`
	LifecycleEvent  string     `db:"lifecycle_event"   json:"lifecycle_event"`
	OldRequiredRole *string    `db:"old_required_role" json:"old_required_role,omitempty"`
	NewRequiredRole *string    `db:"new_required_role" json:"new_required_role,omitempty"`
	CreatedAt       time.Time  `db:"created_at"        json:"created_at"`
}

// ApprovalRequest is one row of paliad.approval_requests — an in-flight
// state-change awaiting 4-eye sign-off.
//
// PreImage carries the field values needed to revert on rejection (NULL for
// 'create' since there's nothing to revert to). Payload echoes the diff or
// new values that were written, for audit display. RequiredRole is a
// snapshot of the policy at request time.
//
// DecisionKind discriminates 'peer' (normal in-team sign-off) from
// 'admin_override' (global_admin used the escape-hatch path).
type ApprovalRequest struct {
	ID             uuid.UUID       `db:"id"              json:"id"`
	ProjectID      uuid.UUID       `db:"project_id"      json:"project_id"`
	EntityType     string          `db:"entity_type"     json:"entity_type"`
	EntityID       uuid.UUID       `db:"entity_id"       json:"entity_id"`
	LifecycleEvent string          `db:"lifecycle_event" json:"lifecycle_event"`
	PreImage       NullableJSON    `db:"pre_image"       json:"pre_image,omitempty"`
	Payload        NullableJSON    `db:"payload"         json:"payload,omitempty"`
	RequestedBy    uuid.UUID       `db:"requested_by"    json:"requested_by"`
	RequestedAt    time.Time       `db:"requested_at"    json:"requested_at"`
	RequiredRole   string          `db:"required_role"   json:"required_role"`
	Status         string          `db:"status"          json:"status"`
	DecidedBy      *uuid.UUID      `db:"decided_by"      json:"decided_by,omitempty"`
	DecidedAt      *time.Time      `db:"decided_at"      json:"decided_at,omitempty"`
	DecisionKind   *string         `db:"decision_kind"   json:"decision_kind,omitempty"`
	DecisionNote   *string         `db:"decision_note"   json:"decision_note,omitempty"`
	// RequesterKind is 'user' (direct user create) or 'agent' (Paliadin
	// drafted the row from a chat turn — t-paliad-161). Agent rows render
	// alongside 👀 with a sparkle ✨ on the eye-pill surface.
	RequesterKind  string          `db:"requester_kind"  json:"requester_kind"`
	AgentTurnID    *uuid.UUID      `db:"agent_turn_id"   json:"agent_turn_id,omitempty"`
	// CounterPayload carries the approver's edited values on a
	// changes_requested row (mig 103, t-paliad-216). NULL for every
	// other status. Frontend renders it as a diff against the OLD
	// payload to show "approver suggested X→Y on the following fields".
	CounterPayload NullableJSON `db:"counter_payload" json:"counter_payload,omitempty"`
	// PreviousRequestID is the back-pointer from a row spawned by
	// SuggestChanges to the prior changes_requested row that birthed it
	// (mig 103, t-paliad-216). NULL on first-attempt rows.
	PreviousRequestID *uuid.UUID `db:"previous_request_id" json:"previous_request_id,omitempty"`
	CreatedAt         time.Time  `db:"created_at"          json:"created_at"`
	UpdatedAt         time.Time  `db:"updated_at"          json:"updated_at"`
}

// ProjectEventChoice is one per-event-card pick scoped to a project
// (t-paliad-265 / m/paliad#96). The join key SubmissionCode matches
// paliad.deadline_rules.submission_code — the same identifier the
// AnchorOverrides plumbing in fristenrechner.go already uses.
//
// ChoiceKind ∈ {appellant, include_ccr, skip}. ChoiceValue namespace
// per kind: appellant=claimant|defendant|both|none; include_ccr=true|false;
// skip=true|false. UNIQUE(project_id, submission_code, choice_kind)
// makes re-picks idempotent (Upsert path).
type ProjectEventChoice struct {
	ID             uuid.UUID  `db:"id"              json:"id"`
	ProjectID      uuid.UUID  `db:"project_id"      json:"project_id"`
	SubmissionCode string     `db:"submission_code" json:"submission_code"`
	ChoiceKind     string     `db:"choice_kind"     json:"choice_kind"`
	ChoiceValue    string     `db:"choice_value"    json:"choice_value"`
	CreatedBy      *uuid.UUID `db:"created_by"      json:"created_by,omitempty"`
	CreatedAt      time.Time  `db:"created_at"      json:"created_at"`
	UpdatedBy      *uuid.UUID `db:"updated_by"      json:"updated_by,omitempty"`
	UpdatedAt      time.Time  `db:"updated_at"      json:"updated_at"`
}
