generator client {
    provider      = "prisma-client-js"
    output        = "../server/generated/prisma"
    binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}

datasource db {
    provider = "postgresql"
    schemas  = ["rebreak"]
}

model Profile {
    id               String    @id @db.Uuid
    username         String?
    nickname         String?
    avatar           String?
    plan             String    @default("legend")
    streak           Int       @default(0)
    followersCount   Int       @default(0) @map("followers_count")
    stripeCustomerId String?   @map("stripe_customer_id")
    stripeSubId      String?   @map("stripe_subscription_id")
    premiumUntil     DateTime? @map("premium_until")
    createdAt        DateTime  @default(now()) @map("created_at")
    updatedAt        DateTime  @default(now()) @updatedAt @map("updated_at")

    // ─── DiGA-Demographie (optional, user-initiated only) ───────────────────
    // Diese Felder werden ausschließlich vom User über die Profile-Form
    // gesetzt — niemals durch Lyra-Extraction oder Memory-Inference.
    // Siehe memory/feedback_demographics_user_initiated.md
    birthYear               Int?      @map("birth_year")
    gender                  String?
    maritalStatus           String?   @map("marital_status")
    profession              String? // legacy — deprecated, nicht mehr im Frontend, DB-Spalte bleibt
    employmentStatus        String?   @map("employment_status")
    shiftWork               Boolean?  @map("shift_work")
    industry                String?
    jobTenure               String?   @map("job_tenure")
    bundesland              String?
    city                    String?
    demographicsConsentAt   DateTime? @map("demographics_consent_at")
    demographicsWithdrawnAt DateTime? @map("demographics_withdrawn_at")

    // ─── Lyra Voice-Picker (Legend-only Premium) ────────────────────────────
    lyraVoiceId String? @map("lyra_voice_id")

    // ─── Pro-Trial-Reward (für Demographics-completion) ─────────────────────
    proTrialStartedAt DateTime? @map("pro_trial_started_at")
    proTrialExpiresAt DateTime? @map("pro_trial_expires_at")
    proTrialSource    String?   @map("pro_trial_source")
    proTrialUsedAt    DateTime? @map("pro_trial_used_at")

    // ─── DiGA-Banner Persistence (server-side, überlebt Re-Install) ─────────
    digaBannerDismissedAt DateTime? @map("diga_banner_dismissed_at")

    // ─── Interaktives Onboarding (Welcome → Nickname → Block → Done) ────────
    // Werte: "welcome" (Default für neue Profile) | "nickname" | "block" | "done"
    // Frontend nutzt diesen Wert um den Onboarding-Spotlight-Tour-Stand
    // wiederherzustellen — auch nach App-Reinstall (DB-State statt AsyncStorage).
    onboardingStep String @default("welcome") @map("onboarding_step")

    // ─── Protection-Disable-State (post-cooldown anti-auto-reactivation) ───
    // Wird gesetzt wenn der User per Cooldown-Resolve den Schutz explizit
    // abschaltet. Solange `!= null`, gibt protection/state.protectionShouldBeActive
    // false zurück → Frontend macht KEINE Auto-Reactivation. User muss explizit
    // im UI re-aktivieren (POST /api/protection/mark-active setzt das Feld zurück).
    // Anti-Pattern: ohne dieses Feld würde der Schutz nach Cooldown sofort wieder
    // anspringen, was den Sinn des Cooldowns aushebelt.
    protectionDisabledAt DateTime? @map("protection_disabled_at")

    // ─── DiGA Rezept-Code-Audit ─────────────────────────────────────────────
    // Wenn ein User per Krankenkassen-Rezept reinkommt (DiGA-Pfad), wird der
    // Einlöse-Zeitpunkt hier persistiert. Reverse-Lookup auf den Code selbst
    // läuft über diga_codes.used_by_profile_id (1:n).
    digaCodeRedeemedAt DateTime? @map("diga_code_redeemed_at")

    digaCodes DigaCode[]

    // ─── Install-Tracking (für Streak: max(last_resolved_cooldown, last_install)) ──
    lastInstallAt DateTime? @map("last_install_at")

    // ─── Presence / Online-Status (Insta-style green dot + "vor X min") ─────
    // Wird via POST /api/me/last-seen (Heartbeat, Phase 1) aktualisiert.
    // Phase 2: Supabase-Edge-Function auf presence-leave-Events ersetzt den
    // Heartbeat-Fallback für bessere Genauigkeit (kein 60s-Lag).
    lastSeenAt      DateTime? @map("last_seen_at")
    presenceVisible Boolean   @default(true) @map("presence_visible")

    // ─── Voice-Calls (DM, nur zwischen gegenseitigen Follows) ───────────────
    // Opt-out: User kann eingehende Anrufe komplett abschalten. Server-seitig
    // erzwungen in social.canCall (mutual-follow + callsEnabled). Default an.
    callsEnabled Boolean @default(true) @map("calls_enabled")

    // ─── Voice-Quota (tages-basiert, UTC-Reset) ─────────────────────────────
    // Tracked per plan: Free=60s/day, Pro=300s/day, Legend=unlimited (no tracking).
    // voiceQuotaResetAt wird auf UTC-Mitternacht des aktuellen Tages gesetzt.
    voiceSecondsUsedToday Int       @default(0) @map("voice_seconds_used_today")
    voiceQuotaResetAt     DateTime? @map("voice_quota_reset_at")

    // ─── Founding Members (erste 100 Signups → automatisch Pro, lifetime) ────
    // foundingMember=true → exempt von Downgrade-Reconciliation (ihr Pro ist Geschenk).
    // Wird beim Signup gesetzt wenn profile-count < 100.
    // Manuell setzbar via /api/dev/set-plan (für Testing).
    foundingMember Boolean @default(false) @map("founding_member")

    // ─── Globale Blocklist Grace-Period (nach Downgrade auf free) ────────────
    // Wenn gesetzt: User sieht noch die volle Blocklist bis zu diesem Datum.
    // Nach Ablauf: nur noch kuratierte Kernliste. 14-Tage-Grace.
    globalBlocklistGraceUntil DateTime? @map("global_blocklist_grace_until")

    // ─── Screen Time Passcode (iOS Layer 3, Migration 20260601) ─────────────
    // Vom App-generierten 4-stelligen Code beim Onboarding. User setzt ihn
    // manuell als Screen Time Passcode, wir speichern ihn damit er nach dem
    // Cooldown abrufbar ist. Kein Hash — muss plain abrufbar sein.
    // Abruf NUR via GET /api/protection/screentime-passcode + canDisableProtection=true.
    screentimePasscode String? @map("screentime_passcode")

    // ─── MDM-Managed Flag (Build 19, Migration 20260526) ────────────────────
    // mdmManaged: true wenn User's Device via MDM verwaltet wird (NEFilter-
    // Profil sideloaded + non-removable). Wird vom App-Code nach nativem
    // NEFilterManager.isEnabled-Check via POST /api/users/me/mdm-status gesetzt.
    // Effekt: Cooldown-Selfdeactivation blockiert, Schutz nur via Trustee/
    // Apple Configurator deaktivierbar.
    //
    // mdmDetectedAt: Zeitstempel des ersten mdmManaged=true-Writes (Audit-Trail).
    // Wird nur beim Übergang false→true gesetzt, nie überschrieben.
    mdmManaged    Boolean   @default(false) @map("mdm_managed")
    mdmDetectedAt DateTime? @map("mdm_detected_at")

    // ─── Push-Notifications (Migration 20260530) ──────────────────────────
    // Per-User Opt-out für Chat-Push (DM + Room). Default ON. Token-spezifischer
    // Disable (z.B. nach Permission-Revoke) wird in PushToken.enabled gesetzt.
    chatPushEnabled Boolean @default(true) @map("chat_push_enabled")

    // ─── Admin-Management (Phase E, Migration 20260509) ─────────────────────
    // banned: User wird auf API-Ebene blockiert (kein Login-Block — Supabase
    // bleibt unberührt). Soft-Delete scrubbt PII statt Hard-Delete (DSGVO).
    banned       Boolean   @default(false)
    bannedAt     DateTime? @map("banned_at")
    bannedReason String?   @map("banned_reason")
    deletedAt    DateTime? @map("deleted_at")

    communityPosts    CommunityPost[]
    communityReplies  CommunityReply[]
    domainSubmissions DomainSubmission[]
    pushTokens        PushToken[]

    @@index([deletedAt])
    @@index([plan])
    @@map("profiles")
    @@schema("rebreak")
}

// ─── Push-Tokens (Expo) ──────────────────────────────────────────────────────
//
// Ein User kann mehrere Geräte (iOS + Android etc.) haben, jedes mit eigenem
// ExponentPushToken[xxx]. Token ist von Expo serverseitig unique.
// Genutzt von server/services/push.ts sendChatPush().
model PushToken {
    id         String    @id @default(uuid()) @db.Uuid
    userId     String    @map("user_id") @db.Uuid
    token      String    @unique
    platform   String // "ios" | "android"
    deviceId   String?   @map("device_id")
    enabled    Boolean   @default(true)
    /// iOS-only: PushKit VoIP-Token (64-char hex). Rotiert unabhängig vom Expo-Token.
    /// NULL → kein CallKit-Wake-Push möglich (Android, Web, alte iOS-Builds).
    /// Versendet via backend/server/services/voip-push.ts mit .p12-Cert (APNs HTTP/2).
    voipToken  String?   @map("voip_token")
    createdAt  DateTime  @default(now()) @map("created_at")
    updatedAt  DateTime  @default(now()) @updatedAt @map("updated_at")
    lastUsedAt DateTime? @map("last_used_at")

    profile Profile @relation(fields: [userId], references: [id], onDelete: Cascade)

    @@index([userId])
    @@map("push_tokens")
    @@schema("rebreak")
}

// ─── DiGA-Codes (Rezept-Einlösung für Krankenkassen-Pfad) ─────────────────────
//
// Codes werden vom Backend ausgegeben (später per Krankenkassen-API erstellt
// + an Patient per E-Mail/Brief geschickt) und im Onboarding eingelöst. Bei
// Einlösung wird der User auf den DiGA-Vollzugang (`grants_plan`, Default
// 'legend') hochgestuft und das Trial-Modell übersprungen.
//
// Test-Codes (label='test_*') werden vom Seed angelegt. Wiederverwendung
// nur per SQL-Reset: UPDATE diga_codes SET used_at=NULL, used_by_profile_id=NULL.
model DigaCode {
    id              String    @id @default(uuid()) @db.Uuid
    code            String    @unique
    label           String? // z.B. 'test_batch_2026-05' oder Krankenkassen-Code
    expiresAt       DateTime? @map("expires_at")
    usedAt          DateTime? @map("used_at")
    usedByProfileId String?   @map("used_by_profile_id") @db.Uuid
    usedByProfile   Profile?  @relation(fields: [usedByProfileId], references: [id], onDelete: SetNull)
    grantsPlan      String    @default("legend") @map("grants_plan")
    notes           String?
    createdAt       DateTime  @default(now()) @map("created_at")

    @@index([usedByProfileId])
    @@map("diga_codes")
    @@schema("rebreak")
}

model Streak {
    id                String   @id @default(uuid()) @db.Uuid
    userId            String   @map("user_id") @db.Uuid
    startDate         DateTime @map("start_date") @db.Date
    currentDays       Int      @default(0) @map("current_days")
    longestDays       Int      @default(0) @map("longest_days")
    avgMonthlySavings Float?   @map("avg_monthly_savings")
    isActive          Boolean  @default(true) @map("is_active")

    @@map("streaks")
    @@schema("rebreak")
}

model StreakEvent {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    type      String // "started" | "reset" | "milestone" | "relapse"
    meta      Json? // z.B. { days: 30 } für Meilensteine
    createdAt DateTime @default(now()) @map("created_at")

    @@map("streak_events")
    @@schema("rebreak")
}

model UrgeLog {
    id            String   @id @default(uuid()) @db.Uuid
    userId        String   @map("user_id") @db.Uuid
    timestamp     DateTime @default(now())
    emotion       String
    wasOvercome   Boolean  @default(false) @map("was_overcome")
    breathingDone Boolean  @default(false) @map("breathing_done")

    @@map("urge_logs")
    @@schema("rebreak")
}

/// SOS-Session für DiGA-Auswertung — kompletter Verlauf einer Notfall-Session
model SosSession {
    id             String    @id @default(uuid()) @db.Uuid
    userId         String    @map("user_id") @db.Uuid
    startedAt      DateTime  @default(now()) @map("started_at")
    endedAt        DateTime? @map("ended_at")
    durationSec    Int?      @map("duration_sec")
    /// Voller Chat-Verlauf [{role, content, timestamp}]
    messages       Json      @default("[]")
    /// [{game, score, durationSec}]
    gamesPlayed    Json      @default("[]")
    breathingCount Int       @default(0) @map("breathing_count")
    wasOvercome    Boolean   @default(false) @map("was_overcome")
    feedbackBetter Boolean?  @map("feedback_better")
    feedbackRating Int?      @map("feedback_rating") // 1-5
    feedbackText   String?   @map("feedback_text")
    locale         String?

    @@index([userId, startedAt])
    @@map("sos_sessions")
    @@schema("rebreak")
}

model CommunityPost {
    id            String    @id @default(uuid()) @db.Uuid
    userId        String    @map("user_id") @db.Uuid
    category      String
    content       String
    imageUrl      String?   @map("image_url")
    upvotes       Int       @default(0)
    likesCount    Int       @default(0) @map("likes_count")
    dislikesCount Int       @default(0) @map("dislikes_count")
    commentsCount Int       @default(0) @map("comments_count")
    repostsCount  Int       @default(0) @map("reposts_count")
    isAnonymous   Boolean   @default(false) @map("is_anonymous")
    /// Reported-Marker: true wenn ein User den Post gemeldet hat. Admin-Queue
    /// listet alle Posts mit isModerated=true. Dismiss → false, Delete → bleibt
    /// true zusammen mit isDeleted=true (für audit/spätere Re-Review).
    isModerated   Boolean   @default(false) @map("is_moderated")
    /// Soft-Delete durch Moderation. content → "" damit Public-API nichts mehr
    /// rendert; Audit-Log behält Original (siehe ModerationAction.contentSnapshot).
    isDeleted     Boolean   @default(false) @map("is_deleted")
    deletedAt     DateTime? @map("deleted_at")
    /// Wann der Post zum ersten Mal gemeldet wurde (queue-Sortierung).
    reportedAt    DateTime? @map("reported_at")
    gameName      String?   @map("game_name")
    repostOfId    String?   @map("repost_of_id") @db.Uuid
    challengeId   String?   @map("challenge_id") @db.Uuid
    /// Template-ID aus dem Lyra-Post-Catalog (z.B. "motivation_quiet_01").
    /// Nullable: Legacy-Posts ohne Template-Key nutzen content direkt.
    /// Frontend rendert t('lyra_posts.<i18nKey>') wenn gesetzt.
    i18nKey       String?   @map("i18n_key")
    createdAt     DateTime  @default(now()) @map("created_at")

    author         Profile?         @relation(fields: [userId], references: [id])
    repostOf       CommunityPost?   @relation("Reposts", fields: [repostOfId], references: [id], onDelete: SetNull)
    reposts        CommunityPost[]  @relation("Reposts")
    PostLike       PostLike[]
    CommunityReply CommunityReply[]

    @@index([isModerated, reportedAt])
    @@map("community_posts")
    @@schema("rebreak")
}

model PostLike {
    userId String @map("user_id") @db.Uuid
    postId String @map("post_id") @db.Uuid
    type   String // "like" | "dislike"

    post CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade)

    @@id([userId, postId])
    @@map("post_likes")
    @@schema("rebreak")
}

model CommunityReply {
    id            String    @id @default(uuid()) @db.Uuid
    postId        String    @map("post_id") @db.Uuid
    userId        String    @map("user_id") @db.Uuid
    content       String
    parentReplyId String?   @map("parent_reply_id") @db.Uuid
    isAnonymous   Boolean   @default(false) @map("is_anonymous")
    likesCount    Int       @default(0) @map("likes_count")
    /// Reported-Marker analog CommunityPost.isModerated.
    isModerated   Boolean   @default(false) @map("is_moderated")
    isDeleted     Boolean   @default(false) @map("is_deleted")
    deletedAt     DateTime? @map("deleted_at")
    reportedAt    DateTime? @map("reported_at")
    createdAt     DateTime  @default(now()) @map("created_at")

    post        CommunityPost @relation(fields: [postId], references: [id], onDelete: Cascade)
    author      Profile?      @relation(fields: [userId], references: [id])
    CommentLike CommentLike[]

    @@index([isModerated, reportedAt])
    @@map("community_replies")
    @@schema("rebreak")
}

model CommentLike {
    userId    String @map("user_id") @db.Uuid
    commentId String @map("comment_id") @db.Uuid

    reply CommunityReply @relation(fields: [commentId], references: [id], onDelete: Cascade)

    @@id([userId, commentId])
    @@map("comment_likes")
    @@schema("rebreak")
}

model ChatRoom {
    id          String   @id @default(uuid()) @db.Uuid
    name        String
    description String?
    isPublic    Boolean  @default(false) @map("is_public")
    avatarUrl   String?  @map("avatar_url")
    createdBy   String   @map("created_by") @db.Uuid
    joinMode    String   @default("open") @map("join_mode") // "open" | "approval" | "invite_only"
    inviteCode  String?  @unique @map("invite_code")
    memberCount Int      @default(0) @map("member_count")
    isDefault   Boolean  @default(false) @map("is_default")
    createdAt   DateTime @default(now()) @map("created_at")
    updatedAt   DateTime @updatedAt @map("updated_at")

    members  ChatRoomMember[]
    messages ChatMessage[]

    @@map("chat_rooms")
    @@schema("rebreak")
}

model ChatRoomMember {
    roomId   String   @map("room_id") @db.Uuid
    userId   String   @map("user_id") @db.Uuid
    role     String   @default("member") // "owner" | "admin" | "member"
    status   String   @default("active") // "active" | "pending"
    joinedAt DateTime @default(now()) @map("joined_at")

    room ChatRoom @relation(fields: [roomId], references: [id], onDelete: Cascade)

    @@id([roomId, userId])
    @@map("chat_room_members")
    @@schema("rebreak")
}

model ChatMessage {
    id             String    @id @default(uuid()) @db.Uuid
    userId         String    @map("user_id") @db.Uuid
    content        String
    roomId         String?   @map("room_id") @db.Uuid
    replyToId      String?   @map("reply_to_id") @db.Uuid
    attachmentUrl  String?   @map("attachment_url")
    attachmentType String?   @map("attachment_type")
    attachmentName String?   @map("attachment_name")
    likesCount     Int       @default(0) @map("likes_count")
    createdAt      DateTime  @default(now()) @map("created_at")
    deletedAt      DateTime? @map("deleted_at")

    room      ChatRoom?             @relation(fields: [roomId], references: [id], onDelete: Cascade)
    replyTo   ChatMessage?          @relation("ChatReplies", fields: [replyToId], references: [id], onDelete: SetNull)
    replies   ChatMessage[]         @relation("ChatReplies")
    likes     ChatMessageLike[]
    reactions ChatMessageReaction[]

    @@map("chat_messages")
    @@schema("rebreak")
}

model ChatMessageLike {
    userId    String @map("user_id") @db.Uuid
    messageId String @map("message_id") @db.Uuid

    message ChatMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)

    @@id([userId, messageId])
    @@map("chat_message_likes")
    @@schema("rebreak")
}

model ChatMessageReaction {
    userId    String   @map("user_id") @db.Uuid
    messageId String   @map("message_id") @db.Uuid
    emoji     String
    createdAt DateTime @default(now()) @map("created_at")

    message ChatMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)

    // Eine Reaktion pro User pro Message (neues Emoji ersetzt das alte — WhatsApp-Verhalten)
    @@id([userId, messageId])
    @@index([messageId])
    @@map("chat_message_reactions")
    @@schema("rebreak")
}

model DirectMessage {
    id             String    @id @default(uuid()) @db.Uuid
    senderId       String    @map("sender_id") @db.Uuid
    receiverId     String    @map("receiver_id") @db.Uuid
    content        String
    replyToId      String?   @map("reply_to_id") @db.Uuid
    attachmentUrl  String?   @map("attachment_url")
    attachmentType String?   @map("attachment_type")
    attachmentName String?   @map("attachment_name")
    likesCount     Int       @default(0) @map("likes_count")
    createdAt      DateTime  @default(now()) @map("created_at")
    readAt         DateTime? @map("read_at")
    deletedAt      DateTime? @map("deleted_at")

    replyTo   DirectMessage?          @relation("DmReplies", fields: [replyToId], references: [id], onDelete: SetNull)
    replies   DirectMessage[]         @relation("DmReplies")
    likes     DirectMessageLike[]
    reactions DirectMessageReaction[]

    // Conversation-Liste (DISTINCT ON partner, neueste zuerst) + Unread-Badge.
    // Ohne diese Indizes ist jeder Conversation-Load ein Full-Table-Scan + Sort.
    @@index([senderId, createdAt(sort: Desc)])
    @@index([receiverId, createdAt(sort: Desc)])
    @@index([receiverId, readAt])
    @@map("direct_messages")
    @@schema("rebreak")
}

model DirectMessageLike {
    userId    String @map("user_id") @db.Uuid
    messageId String @map("message_id") @db.Uuid

    message DirectMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)

    @@id([userId, messageId])
    @@map("direct_message_likes")
    @@schema("rebreak")
}

model DirectMessageReaction {
    userId    String   @map("user_id") @db.Uuid
    messageId String   @map("message_id") @db.Uuid
    emoji     String
    createdAt DateTime @default(now()) @map("created_at")

    message DirectMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)

    @@id([userId, messageId])
    @@index([messageId])
    @@map("direct_message_reactions")
    @@schema("rebreak")
}

model UserFollow {
    followerId  String @map("follower_id") @db.Uuid
    followingId String @map("following_id") @db.Uuid

    @@id([followerId, followingId])
    @@map("user_follows")
    @@schema("rebreak")
}

model UserScore {
    userId      String   @id @map("user_id") @db.Uuid
    totalPoints Int      @default(0) @map("total_points")
    tier        String   @default("beginner")
    updatedAt   DateTime @default(now()) @updatedAt @map("updated_at")

    @@map("user_scores")
    @@schema("rebreak")
}

model ScoreEvent {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    eventType String   @map("event_type")
    points    Int
    createdAt DateTime @default(now()) @map("created_at")
    meta      Json?

    @@map("score_events")
    @@schema("rebreak")
}

model UserCustomDomain {
    id      String   @id @default(uuid()) @db.Uuid
    userId  String   @map("user_id") @db.Uuid
    domain  String
    source  String   @default("manual")
    // "active" | "submitted" | "approved" | "rejected"
    status  String   @default("active")
    // "web" | "mail_domain" | "mail_display_name"
    // Alle Types teilen den gleichen Slot-Pool (countActiveCustomDomains() zählt total).
    // mail_display_name: Substring-Heuristik gegen Sender-Display-Name (kein PII, Art. 4 DSGVO)
    type    String   @default("web")
    postId  String?  @map("post_id") @db.Uuid
    addedAt DateTime @default(now()) @map("added_at")

    // Layer-2-Country-Pivot (2026-05-25): vipDeferUntil + vipEvictAt entfernt.
    // Layer 2 ist nicht mehr User-Custom-gespeist — Pure Country-Curated.
    // DB-Columns werden via drop_vip_swap_fields.sql gedroppt (nach Code-Deploy).

    submission DomainSubmission?

    @@unique([userId, domain])
    @@index([userId, type])
    @@map("user_custom_domains")
    @@schema("rebreak")
}

// Länderspezifische Curated-Domain-Vorschläge für die VIP-Layer-2-Liste.
// User schlagen Glücksspielseiten ihres Landes vor; `approved`-Einträge
// ergänzen im webcontent-domains-Endpoint die statische gambling-domains.json.
model CuratedDomain {
    id                String    @id @default(uuid()) @db.Uuid
    domain            String
    country           String
    // "suggested" | "approved" | "rejected"
    status            String    @default("suggested")
    suggestedByUserId String?   @map("suggested_by_user_id") @db.Uuid
    createdAt         DateTime  @default(now()) @map("created_at")
    reviewedAt        DateTime? @map("reviewed_at")

    @@unique([country, domain])
    @@index([country, status])
    @@map("curated_domains")
    @@schema("rebreak")
}

model DomainSubmission {
    id             String    @id @default(uuid()) @db.Uuid
    userId         String    @map("user_id") @db.Uuid
    domain         String
    customDomainId String    @unique @map("custom_domain_id") @db.Uuid
    postId         String?   @map("post_id") @db.Uuid
    // "pending" | "approved" | "rejected"
    status         String    @default("pending")
    // "web" | "mail_domain" — spiegelt den Type der zugehörigen UserCustomDomain-Row
    type           String    @default("web")
    yesVotes       Int       @default(0) @map("yes_votes")
    noVotes        Int       @default(0) @map("no_votes")
    reviewNote     String?   @map("review_note")
    createdAt      DateTime  @default(now()) @map("created_at")
    reviewedAt     DateTime? @map("reviewed_at")

    customDomain UserCustomDomain @relation(fields: [customDomainId], references: [id], onDelete: Cascade)
    user         Profile          @relation(fields: [userId], references: [id])
    votes        DomainVote[]

    @@index([status, createdAt])
    @@index([type, status])
    @@map("domain_submissions")
    @@schema("rebreak")
}

model DomainVote {
    userId       String   @map("user_id") @db.Uuid
    submissionId String   @map("submission_id") @db.Uuid
    vote         String // "yes" | "no"
    createdAt    DateTime @default(now()) @map("created_at")

    submission DomainSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)

    @@id([userId, submissionId])
    @@map("domain_votes")
    @@schema("rebreak")
}

enum FeedbackStatus {
    PENDING
    REVIEWING
    PLANNED
    SHIPPED
    REJECTED

    @@schema("rebreak")
}

model FeedbackItem {
    id        String         @id @default(uuid()) @db.Uuid
    userId    String         @map("user_id") @db.Uuid
    content   String
    category  String?
    status    FeedbackStatus @default(PENDING)
    adminNote String?        @map("admin_note")
    createdAt DateTime       @default(now()) @map("created_at")
    updatedAt DateTime       @updatedAt @map("updated_at")

    @@map("feedback_items")
    @@schema("rebreak")
}

model BlocklistDomain {
    id          String  @id @default(uuid()) @db.Uuid
    domain      String  @unique
    source      String
    isActive    Boolean @default(true) @map("is_active")
    reportCount Int     @default(0) @map("report_count")

    @@map("blocklist_domains")
    @@schema("rebreak")
}

/// Admin-kuratierte Glücksspiel-Marken für Mail-Display-Name-Matching.
/// Analog BlocklistDomain — kein User-Bezug, keine Mail-Inhalte (Art. 5 DSGVO).
/// Source: "seed" (initiale 30 Brands) | "manual" (Admin-App) | "community" (zukünftig).
/// SCHEMA hinzugefügt 2026-05-28, Migration: 20260528_add_global_mail_display_names.
model GlobalMailDisplayName {
    id       String   @id @default(uuid()) @db.Uuid
    pattern  String   @unique // z.B. "Tipico", "Bet365"
    isActive Boolean  @default(true) @map("is_active")
    source   String   @default("manual") // "manual" | "seed" | "community"
    addedAt  DateTime @default(now()) @map("added_at")

    @@map("global_mail_display_names")
    @@schema("rebreak")
}

model TrustedContact {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    name      String
    phone     String?
    email     String?
    createdAt DateTime @default(now()) @map("created_at")

    @@map("trusted_contacts")
    @@schema("rebreak")
}

model CoachSession {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    content   Json
    createdAt DateTime @default(now()) @map("created_at")

    @@map("coach_sessions")
    @@schema("rebreak")
}

model MailConnection {
    id                  String    @id @default(uuid()) @db.Uuid
    userId              String    @map("user_id") @db.Uuid
    email               String
    provider            String    @default("imap")
    providerName        String?   @map("provider_name")
    imapHost            String    @map("imap_host")
    imapPort            Int       @map("imap_port")
    rejectUnauthorized  Boolean   @default(true) @map("reject_unauthorized")
    useStarttls         Boolean   @default(false) @map("use_starttls")
    passwordEncrypted   String    @map("password_encrypted")
    isActive            Boolean   @default(true) @map("is_active")
    /// Wenn gesetzt: Account wurde durch Downgrade-Reconciliation pausiert (nicht gelöscht).
    /// Re-Upgrade setzt pausedAt=null + pausedReason=null + isActive=true zurück.
    pausedAt            DateTime? @map("paused_at")
    pausedReason        String?   @map("paused_reason") // z.B. "plan_downgrade"
    scanInterval        Int       @default(24) @map("scan_interval")
    lastScannedAt       DateTime? @map("last_scanned_at")
    nextScanAt          DateTime? @map("next_scan_at")
    emailsBlocked       Int       @default(0) @map("emails_blocked")
    emailsScanned       Int       @default(0) @map("emails_scanned")
    lastConnectError    String?   @map("last_connect_error")
    lastConnectErrorAt  DateTime? @map("last_connect_error_at")
    lastIdleHeartbeatAt DateTime? @map("last_idle_heartbeat_at")
    createdAt           DateTime  @default(now()) @map("created_at")

    // ─── OAuth2-Auth-Framework (additiv, Phase 0) ────────────────────────────
    // authMethod: 'app_password' (default, alle bestehenden Connections)
    //           | 'oauth2_microsoft' (Outlook/Hotmail/Live — Phase 1)
    //           | 'oauth2_google' (Gmail, future — wenn Google Basic-Auth deprecated)
    // Bestehende Connections: app_password per DEFAULT, keine Migration nötig.
    // OAuth-Felder bleiben NULL für app_password-Connections.
    authMethod        String    @default("app_password") @map("auth_method")
    /// AES-256-GCM encrypted (gleiches Verfahren wie passwordEncrypted, gleicher ENCRYPTION_KEY).
    /// Format: iv(24hex):tag(32hex):ciphertext(hex) — via server/utils/crypto.ts encrypt()
    oauthAccessToken  String?   @map("oauth_access_token")
    /// AES-256-GCM encrypted. Microsoft kann bei Refresh neues refresh_token liefern
    /// (Token-Rotation) — bei jedem Refresh-Call persistieren.
    oauthRefreshToken String?   @map("oauth_refresh_token")
    /// UTC-Zeitstempel wann access_token abläuft. Daemon prüft on-connect:
    /// wenn expiry < now+5min → refresh vor IMAP-Connect.
    oauthTokenExpiry  DateTime? @map("oauth_token_expiry")
    /// Gespeicherter Scope-String des erteilten Konsents (z.B. "IMAP.AccessAsUser.All offline_access openid").
    /// Für Audit und zukünftige Scope-Vergleiche bei Re-Auth.
    oauthScope        String?   @map("oauth_scope")

    // ─── User-definierter Anzeige-Titel (optional, max 60 chars auf API-Ebene) ──
    // Wird statt der vollen Email-Adresse angezeigt (z.B. "Privat-Gmail").
    // NULL → Frontend fällt auf Email-Domain zurück.
    title String?

    // ─── Inkrementeller UID-Scan (Phase 2, Migration 20260605_mail_uid_scan_state) ─
    // folder_scan_state: pro Ordner {lastUid, uidvalidity} — ermöglicht inkrementellen
    // IMAP-Scan (nur neue UIDs seit letztem Run). Format:
    //   { "INBOX": {"lastUid":1234,"uidvalidity":5678}, "Junk Email": {...} }
    // DEFAULT '{}' → erster Lauf behandelt alle Ordner als lastUid=0 (Full-Sweep).
    // UIDVALIDITY-Wächter: wenn status.uidValidity != gespeicherter Wert → Reset auf 0
    // (Ordner wurde server-seitig resettet, alle UIDs ungültig).
    //
    // last_full_sweep_at: Zeitstempel des letzten Quality-Full-Sweeps (1×/Tag).
    // Wächter gegen Blocklist-Updates die ältere Mails nicht treffen würden.
    // NULL → noch kein Full-Sweep ausgeführt → erster Lauf wird als Full-Sweep gezählt.
    folderScanState  Json      @default("{}") @map("folder_scan_state")
    lastFullSweepAt  DateTime? @map("last_full_sweep_at") @db.Timestamptz(6)

    // ─── Art. 9-Einwilligung (DSGVO-Compliance, Mail-Auto-Delete) ───────────
    // consentAt=NULL für Bestandsrows → "Re-Consent pending".
    // Daemon pausiert Mail-Verarbeitung wenn consentAt=NULL (kein Auto-Delete).
    // consentVersion: z.B. "art9-mail-v1-2026-05-13". Bump → alle Consents invalid.
    // consentIpAddress: Beweispflicht Art. 7 Abs. 1 DSGVO.
    consentAt        DateTime? @map("consent_at")
    consentVersion   String?   @map("consent_version")
    consentIpAddress String?   @map("consent_ip_address")

    blockedMails MailBlocked[]
    blockedStats MailBlockedStat[]

    @@unique([userId, email])
    @@map("mail_connections")
    @@schema("rebreak")
}

enum GameChallengeStatus {
    OPEN
    ACTIVE
    FINISHED
    CANCELLED

    @@schema("rebreak")
}

model GameChallenge {
    id             String              @id @default(uuid()) @db.Uuid
    challengerId   String              @map("challenger_id") @db.Uuid
    challengerName String              @map("challenger_name")
    opponentId     String?             @map("opponent_id") @db.Uuid
    opponentName   String?             @map("opponent_name")
    status         GameChallengeStatus @default(OPEN)
    board          String              @default("---------")
    currentTurn    String              @default("X") @map("current_turn")
    winner         String?
    postId         String?             @map("post_id") @db.Uuid
    gameType       String              @default("tictactoe") @map("game_type")
    isLive         Boolean             @default(false) @map("is_live")
    memoryState    Json?               @map("memory_state")
    createdAt      DateTime            @default(now()) @map("created_at")
    updatedAt      DateTime            @updatedAt @map("updated_at")

    @@map("game_challenges")
    @@schema("rebreak")
}

model Notification {
    id          String    @id @default(uuid()) @db.Uuid
    recipientId String    @map("recipient_id") @db.Uuid
    type        String // "new_comment" | "new_like" | "domain_vote"
    actorName   String    @map("actor_name")
    actorAvatar String?   @map("actor_avatar")
    postId      String?   @map("post_id") @db.Uuid
    preview     String?
    readAt      DateTime? @map("read_at")
    createdAt   DateTime  @default(now()) @map("created_at")

    @@index([recipientId, readAt])
    @@map("notifications")
    @@schema("rebreak")
}

model GameScore {
    userId     String   @id @map("user_id") @db.Uuid
    playerName String   @map("player_name")
    wins       Int      @default(0)
    losses     Int      @default(0)
    draws      Int      @default(0)
    points     Int      @default(0)
    updatedAt  DateTime @updatedAt @map("updated_at")

    @@map("game_scores")
    @@schema("rebreak")
}

model GameRating {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    gameName  String   @map("game_name")
    stars     Int
    feedback  String?
    score     Int      @default(0)
    createdAt DateTime @default(now()) @map("created_at")

    @@map("game_ratings")
    @@schema("rebreak")
}

model GameHighScore {
    id        String   @id @default(uuid()) @db.Uuid
    userId    String   @map("user_id") @db.Uuid
    nickname  String
    gameName  String   @map("game_name")
    score     Int
    updatedAt DateTime @updatedAt @map("updated_at")

    @@unique([userId, gameName])
    @@index([gameName, score(sort: Desc)])
    @@map("game_high_scores")
    @@schema("rebreak")
}

model MailBlocked {
    id             String   @id @default(uuid()) @db.Uuid
    userId         String   @map("user_id") @db.Uuid
    connectionId   String   @map("connection_id") @db.Uuid
    gmailMessageId String   @map("gmail_message_id")
    senderEmail    String   @map("sender_email")
    senderName     String?  @map("sender_name")
    subject        String
    receivedAt     DateTime @map("received_at")
    action         String
    /// Welcher Layer die Blockierung ausgelöst hat (z.B. "domain", "brand+random", "score:85", "llm:0.92").
    /// NULL für ältere Einträge (vor Migration 20260514).
    triggerSource  String?  @map("trigger_source") @db.VarChar(64)
    createdAt      DateTime @default(now()) @map("created_at")

    connection MailConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)

    @@unique([gmailMessageId, userId])
    @@map("mail_blocked")
    @@schema("rebreak")
}

/// Klassifikations-Samples für ML-Phase 3 (zukünftiges Fine-Tuning / Modell-Evaluation).
/// Enthält Features + Outcomes jeder Mail-Klassifikation.
/// KEIN Mail-Body — nur Metadaten (Sender-Domain, Subject, Score-Komponenten).
/// Cascade-Delete bei User-Löschung (Art. 17 DSGVO).
model MailClassificationSample {
    id           String  @id @default(cuid())
    userId       String  @map("user_id") @db.Uuid
    connectionId String? @map("connection_id") @db.Uuid

    // Raw features (was analysiert wurde):
    senderName         String? @map("sender_name") @db.VarChar(255)
    senderDomain       String? @map("sender_domain") @db.VarChar(255)
    relayDecodedDomain String? @map("relay_decoded_domain") @db.VarChar(255)
    subject            String? @db.VarChar(998) // RFC 5322 max

    // Computed features (Score-Komponenten als JSON):
    features Json // { score, brandMatch, randomTokens, keywordHits, styleFlags, … }

    // Outcome:
    finalAction   String @map("final_action") // "blocked" | "passed"
    triggerSource String @map("trigger_source") // "domain", "brand+random", "score:NN", "llm:0.XX", "whitelist"

    // Groq verdict (nur wenn Layer 4 lief):
    groqIsGambling Boolean? @map("groq_is_gambling")
    groqConfidence Float?   @map("groq_confidence")
    groqReason     String?  @map("groq_reason") @db.Text

    // User-Feedback (für später):
    userFeedback String?   @map("user_feedback") // null | "correct" | "false-positive" | "false-negative"
    feedbackAt   DateTime? @map("feedback_at")

    createdAt DateTime @default(now()) @map("created_at")

    @@index([userId])
    @@index([createdAt])
    @@index([finalAction, triggerSource])
    @@map("mail_classification_samples")
    @@schema("rebreak")
}

/// Permanente Aggregat-Statistiken blockierter Mails pro Tag + Connection.
/// Befüllt live beim Scan (vor dem 24h-Cleanup von mail_blocked).
/// Enthält KEINE Mail-Inhalte — nur counts/dates (Datenminimierung Art. 5 DSGVO).
/// Bei Connection-Disconnect werden Stats mitgelöscht (Cascade) — Konsistenz
/// mit Art. 17 DSGVO: User-initiierter Disconnect = Recht auf Löschung aller
/// zu dieser Connection gehörenden Daten.
model MailBlockedStat {
    id               String   @id @default(cuid())
    userId           String   @map("user_id") @db.Uuid
    /// UTC-Datum (time=00:00:00) — ein Eintrag pro User+Tag+Connection
    date             DateTime @map("date") @db.Date
    mailConnectionId String   @map("mail_connection_id") @db.Uuid
    /// IMAP-Host-Slug, z.B. "imap.gmail.com" (raw, für resolveProviderMeta zur Read-Zeit)
    provider         String   @map("provider")
    /// Human-readable Label, z.B. "Gmail" (wird bei Scan-Zeit aus resolveProviderMeta befüllt)
    providerLabel    String   @map("provider_label")
    count            Int      @default(0)

    connection MailConnection @relation(fields: [mailConnectionId], references: [id], onDelete: Cascade)

    @@unique([userId, date, mailConnectionId], map: "mail_blocked_stats_unique_day")
    @@index([userId, date])
    @@index([userId, mailConnectionId])
    @@map("mail_blocked_stats")
    @@schema("rebreak")
}

model ImapProxyAccount {
    id            String   @id @default(uuid()) @db.Uuid
    userId        String   @map("user_id") @db.Uuid
    proxyUsername String   @unique @map("proxy_username")
    proxyPassword String   @map("proxy_password")
    connectionId  String   @unique @map("connection_id") @db.Uuid
    createdAt     DateTime @default(now()) @map("created_at")

    @@map("imap_proxy_accounts")
    @@schema("rebreak")
}

model CooldownRequest {
    id                String    @id @default(uuid()) @db.Uuid
    userId            String    @map("user_id") @db.Uuid
    reason            String?
    cooldownStartedAt DateTime  @default(now()) @map("cooldown_started_at")
    cooldownEndsAt    DateTime  @map("cooldown_ends_at")
    resolvedAt        DateTime? @map("resolved_at")
    cancelledAt       DateTime? @map("cancelled_at")
    tokenJti          String    @unique @map("token_jti")

    @@index([userId, cooldownEndsAt])
    @@map("cooldown_requests")
    @@schema("rebreak")
}

enum LyraMemoryType {
    trigger
    habit
    strength
    relationship
    milestone
    pain_point
    goal
    preference

    @@schema("rebreak")
}

/// Persistente Erinnerungen von Lyra über den User — injiziert in System-Prompt jeder Session.
/// Enthält Art-9-Gesundheitsdaten (Glücksspielkontext) — strenge RLS: nur service-role schreibt.
model LyraMemory {
    id               String         @id @default(uuid()) @db.Uuid
    userId           String         @map("user_id") @db.Uuid
    type             LyraMemoryType
    content          String         @db.VarChar(500)
    confidence       Float          @default(0.7)
    source           String? // session-id | "manual" | "observed"
    createdAt        DateTime       @default(now()) @map("created_at") @db.Timestamptz(6)
    updatedAt        DateTime       @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
    lastReferencedAt DateTime?      @map("last_referenced_at") @db.Timestamptz(6)

    @@index([userId, type])
    @@map("lyra_memories")
    @@schema("rebreak")
}

/// Admin-Allowlist — nur Einträge hier erhalten Zugang zur Admin-App.
/// Seed: INSERT INTO "rebreak"."admin_users" ("user_id", "created_at") VALUES ('128df360-2008-4d6f-8aa1-bdb41ec1362f', NOW());
model AdminUser {
    userId    String   @id @map("user_id") @db.Uuid
    createdAt DateTime @default(now()) @map("created_at")
    addedBy   String?  @map("added_by")

    @@map("admin_users")
    @@schema("rebreak")
}

/// Audit-Log für Moderation-Aktionen (DSGVO-konform: Original-Inhalt bleibt für
/// 90 Tage erhalten, danach Cron-Cleanup). Wird von /api/admin/moderation/[id]/*
/// nach jeder Aktion (dismiss / delete / ban-user) geschrieben.
model ModerationAction {
    id              String   @id @default(uuid()) @db.Uuid
    /// "post" | "comment"
    targetType      String   @map("target_type")
    /// CommunityPost.id oder CommunityReply.id
    targetId        String   @map("target_id") @db.Uuid
    /// "dismiss" | "delete" | "ban_user"
    action          String
    /// Profile.id des Admins (aus admin_users-Allowlist).
    adminUserId     String?  @map("admin_user_id") @db.Uuid
    /// Snapshot des Original-Contents zum Zeitpunkt der Aktion (Audit-Trail).
    contentSnapshot String?  @map("content_snapshot")
    /// Optionale Begründung vom Admin.
    reason          String?
    createdAt       DateTime @default(now()) @map("created_at")

    @@index([targetType, targetId])
    @@index([createdAt])
    @@map("moderation_actions")
    @@schema("rebreak")
}

/// Append-only Audit-Log für DSGVO-Einwilligungen und -Widerrufe.
/// Jede Einwilligung (grant) UND jeder Widerruf (revoke) wird als eigener Eintrag geschrieben.
/// Beweispflicht Art. 7 Abs. 1 DSGVO — niemals löschen, nur archivieren.
model ConsentLog {
    id               String    @id @default(cuid())
    userId           String    @map("user_id") @db.Uuid
    /// 'art9-mail' | künftige Consent-Typen (z.B. 'art9-lyra-memory')
    consentType      String    @map("consent_type")
    /// z.B. "art9-mail-v1-2026-05-13". Gleich der consentVersion in MailConnection.
    consentVersion   String    @map("consent_version")
    consentAt        DateTime  @map("consent_at")
    ipAddress        String?   @map("ip_address")
    userAgent        String?   @map("user_agent")
    /// Optional: welche MailConnection-Row dieser Consent betrifft.
    mailConnectionId String?   @map("mail_connection_id") @db.Uuid
    /// Gesetzt wenn Einwilligung widerrufen wurde.
    revokedAt        DateTime? @map("revoked_at")
    /// 'user_disconnect' | 'account_deleted' | 'text_version_updated'
    revokeReason     String?   @map("revoke_reason")

    @@index([userId, consentType])
    @@map("consent_logs")
    @@schema("rebreak")
}

// Device-Binding pro User: Free=1, Pro=1, Legend=3 (siehe plan-features.ts maxDevices).
// Frontend liefert via Capacitor Device.getId() eine persistente UUID — diese wird
// bei jedem authentifizierten Request via x-device-id Header geprüft.
//
// Device-Account-Lock (Bypass-Schutz):
//   boundToPlan      — Plan des Users zum Zeitpunkt der Bindung. NULL = noch nicht
//                      gebunden (Free-User oder Device vor Migration). Lock gilt
//                      nur wenn boundToPlan IN (pro, legend, standard, premium).
//   releaseRequestedAt — wann Original-User "Gerät freigeben" angeklickt hat.
//                      releaseRequestedAt + 24h = Freigabe (Drang-Cooldown).
//   lockNotifiedAt   — Rate-Limit-Marker: letzte Mail-Notification bei Login-Versuch
//                      auf gebundenem Gerät. Max 1 Mail / 6h / Device.
//
// DSGVO Hans-Müller: Art-17-Konto-Löschung kaskadiert user_devices via ON DELETE CASCADE
// im DB-FK → alle Device-Locks automatisch released. Keine gesonderte Logik nötig.
model UserDevice {
    id         String   @id @default(uuid()) @db.Uuid
    userId     String   @map("user_id") @db.Uuid
    deviceId   String   @map("device_id") // Capacitor persistent UUID
    platform   String // "ios" | "android" | "web"
    model      String? // z.B. "iPhone15,2"
    name       String? // z.B. "Chahines iPhone"
    osVersion  String?  @map("os_version") // z.B. "18.4.1"
    lastSeenAt DateTime @default(now()) @map("last_seen_at")
    createdAt  DateTime @default(now()) @map("created_at")

    // ─── Device-Account-Lock ────────────────────────────────────────────────
    /// Plan des Users zum Zeitpunkt der Bindung. NULL → kein Lock aktiv.
    /// Lock gilt wenn: boundToPlan != null AND releaseRequestedAt + 24h > NOW()
    boundToPlan        String?   @map("bound_to_plan")
    /// Wann der Original-User "Gerät freigeben" beantragt hat.
    /// NULL → noch kein Release-Request. Freigabe wird aktiv nach +24h.
    releaseRequestedAt DateTime? @map("release_requested_at")
    /// Letzte Mail-Notification bei fremdem Login-Versuch. Rate-Limit 6h.
    lockNotifiedAt     DateTime? @map("lock_notified_at")

    // ─── RebreakMagic DNS-Device-Binding ────────────────────────────────────
    /// 48+ char URL-safe random token für DNS-over-HTTPS Client-ID.
    /// NULL → Device nicht als Magic-Client gebunden. Unique → pro Token nur 1 Device.
    magicDnsToken   String?   @unique @map("magic_dns_token")
    /// Wann Magic-Binding aktiviert wurde (Config-Profil installiert).
    magicEnrolledAt DateTime? @map("magic_enrolled_at")
    /// Killswitch: wenn gesetzt → DNS-Token serverseitig invalidiert (AdGuard-Client deleted).
    /// User kann Config-Profil manuell deinstallieren, aber DNS-Queries werden abgelehnt.
    magicRevokedAt  DateTime? @map("magic_revoked_at")
    /// Mac-Hostname für UI (z.B. "Chahines MacBook Pro"). Nur bei Magic-Devices.
    magicHostname   String?   @map("magic_hostname")
    /// Server-gehaltenes Removal-Passwort für das Mac/Win-Config-Profil.
    /// Wird in com.apple.profileRemovalPassword injiziert — User sieht es NIE,
    /// nur nach Cooldown-Release (Offboarding). NULL → noch keins generiert.
    magicRemovalPassword    String?   @map("magic_removal_password")
    /// Wann der User die Entfernung des Magic-Profils beantragt hat.
    /// Removal-Passwort wird erst nach +MAGIC_RELEASE_COOLDOWN_H sichtbar.
    magicReleaseRequestedAt DateTime? @map("magic_release_requested_at")
    /// Temporärer Sleep-Mode für Magic-Desktop-Geräte. NULL = kein Cooldown aktiv.
    magicCooldownUntil DateTime? @map("magic_cooldown_until")

    // ─── NanoMDM iOS Enrollment ─────────────────────────────────────────────
    /// Apple-Geräte-UDID wie von NanoMDM verwendet (z.B. 00008101-000544261E87001E).
    /// NULL → Gerät ist nicht mit einem MDM-UDID verknüpft.
    mdmId String? @map("mdm_id")

    /// Gespiegelter Enrollment-Status aus NanoMDM enrollments.enabled.
    mdmEnrolled Boolean? @map("mdm_enrolled")
    /// Gespiegelter Supervision-Status aus NanoMDM devices.unlock_token IS NOT NULL.
    mdmSupervised Boolean? @map("mdm_supervised")
    /// Letzter NanoMDM Check-In (enrollments.last_seen_at).
    mdmLastSeenAt DateTime? @map("mdm_last_seen_at")

    @@unique([userId, deviceId])
    @@index([userId])
    @@index([deviceId])
    @@index([mdmId])
    @@map("user_devices")
    @@schema("rebreak")
}

/// RebreakMagic Pairing — Native-App generiert 6-stelligen Code, Mac-App tauscht
/// gegen MagicSession-Token. Code ist single-use, läuft nach 10min ab.
// ─── Protection State Log (DiGA-Kernmetrik, additiv) ─────────────────────────
//
// Append-only Transitions-Log des Schutz-Zustands pro User.
// Ein Eintrag beschreibt ab `occurredAt` den neuen aktiven Zustand.
// Dedup: kein neues Event, wenn active == letztem bekanntem Zustand des Users.
//
// Source-Values:
//   'vpn'             — VPN-Filter-App meldet Aktivierung/Deaktivierung
//   'mdm'             — MDM-Profil-Status-Wechsel
//   'cooldown_disable' — Server: Cooldown abgelaufen, Schutz automatisch AUS
//   'client'          — Generischer Client-Event
//   'system'          — Server-seitig (Migration, Seed, etc.)
//
// Nicht-entfernen: Alte streaks/streak_events/profiles.streak bleiben (coach, scores).
model ProtectionStateLog {
    id         String   @id @default(uuid()) @db.Uuid
    userId     String   @map("user_id") @db.Uuid
    /// true = Schutz AN ab occurredAt, false = Schutz AUS ab occurredAt
    active     Boolean
    source     String   // 'vpn' | 'mdm' | 'cooldown_disable' | 'client' | 'system'
    occurredAt DateTime @map("occurred_at") @db.Timestamptz(6)
    createdAt  DateTime @default(now()) @map("created_at") @db.Timestamptz(6)

    @@index([userId, occurredAt])
    @@map("protection_state_log")
    @@schema("rebreak")
}

model DeviceProtectionState {
    id             String    @id @default(uuid()) @db.Uuid
    userId         String    @map("user_id") @db.Uuid
    deviceId       String    @map("device_id")
    platform       String // ios | android | mac | windows
    protectionType String    @map("protection_type")
    active         Boolean
    lastSeenAt     DateTime? @map("last_seen_at")
    changedAt      DateTime  @default(now()) @map("changed_at")
    reason         String?

    @@unique([userId, deviceId, protectionType])
    @@index([userId])
    @@index([deviceId])
    @@index([protectionType])
    @@map("device_protection_states")
    @@schema("rebreak")
}

model DeviceProtectionStateLog {
    id             String   @id @default(uuid()) @db.Uuid
    userId         String   @map("user_id") @db.Uuid
    deviceId       String   @map("device_id")
    protectionType String   @map("protection_type")
    active         Boolean
    occurredAt     DateTime @map("occurred_at")
    reason         String?
    source         String // app | mdm | system | heartbeat

    @@index([userId, deviceId])
    @@map("device_protection_state_logs")
    @@schema("rebreak")
}

model MagicPairingCode {
    id         String    @id @default(uuid()) @db.Uuid
    userId     String    @map("user_id") @db.Uuid
    /// 6-stelliger numerischer Code (z.B. "482913"). Unique während gültig.
    code       String    @unique
    expiresAt  DateTime  @map("expires_at")
    /// Wenn redeemed: Zeitpunkt + erstellte MagicSession-ID. Code danach nicht mehr nutzbar.
    redeemedAt DateTime? @map("redeemed_at")
    sessionId  String?   @map("session_id") @db.Uuid
    createdAt  DateTime  @default(now()) @map("created_at")

    @@index([userId])
    @@index([expiresAt])
    @@map("magic_pairing_codes")
    @@schema("rebreak")
}

/// RebreakMagic Session — Mac-App erhält bei Pair-Redeem einen mgc_*-Token,
/// der statt Supabase-JWT in /api/magic/* Endpoints akzeptiert wird.
/// Wird in Mac-Keychain gespeichert, kann vom User in Native-App revoked werden.
model MagicSession {
    id         String    @id @default(uuid()) @db.Uuid
    userId     String    @map("user_id") @db.Uuid
    /// "mgc_" + 48 char base64url (token = id wird NICHT preisgegeben).
    token      String    @unique
    /// Optionaler Mac-Hostname für User-UI ("Chahines MacBook Pro").
    label      String?
    createdAt  DateTime  @default(now()) @map("created_at")
    lastUsedAt DateTime  @default(now()) @map("last_used_at")
    revokedAt  DateTime? @map("revoked_at")

    @@index([userId])
    @@index([token])
    @@map("magic_sessions")
    @@schema("rebreak")
}

// Wenn ein neues Gerät versucht sich zu registrieren UND das Device-Limit
// erreicht ist (oder User Approval explizit wünscht), erstellt das neue Gerät
// eine DeviceApprovalRequest mit 6-stelligem Code. Andere aktive Geräte des
// Users werden via supabase_realtime benachrichtigt und zeigen ein Sheet mit
// dem Code + [Erlauben] / [Ablehnen]. User vergleicht visuell den Code auf
// beiden Geräten (verhindert Code-Forwarding-Attacken).
//
// Email-Fallback: Wenn kein anderes Gerät online ODER User klickt "Per Email
// senden", verschickt der Server eine Mail mit dem Code + One-Click-Approval-Link.
//
// Flow:
// 1. Neues Device → register → 403 DEVICE_LIMIT_REACHED
// 2. User wählt "Auf anderem Gerät bestätigen" → POST /api/devices/approvals
// 3. Server erstellt Row, broadcastet via realtime
// 4. Existing Device → approve → Server marked approved + auto-evictiert das
//    älteste Device (oder das vom User gewählte) + erstellt neuen UserDevice-Slot
// 5. Neues Device pollt GET /api/devices/approvals/:id → status=approved → retry register
model DeviceApprovalRequest {
    id     String @id @default(uuid()) @db.Uuid
    userId String @map("user_id") @db.Uuid

    // Info über das NEUE Gerät (das sich anmelden will)
    newDeviceId  String  @map("new_device_id")
    newPlatform  String  @map("new_platform")
    newModel     String? @map("new_model")
    newName      String? @map("new_name")
    newOsVersion String? @map("new_os_version")

    /// 6-stelliger Code (z.B. "123456"). Wird auf BEIDEN Geräten gezeigt für
    /// visuellen Vergleich (iCloud-Pattern). Plain-text gespeichert weil
    /// kurzlebig (10min TTL) und an userId gebunden.
    code String

    /// pending | approved | rejected | expired
    status String @default("pending")

    /// Welches existing Device hat approved (für Audit-Log).
    /// NULL wenn via Email-Link approved.
    approvedByDeviceRowId String?   @map("approved_by_device_row_id") @db.Uuid
    approvedAt            DateTime? @map("approved_at")
    rejectedAt            DateTime? @map("rejected_at")

    /// Welches Device wurde evictiert um Platz zu machen (UserDevice.id).
    /// NULL wenn User keine Eviction nötig hatte (Slot frei).
    evictedDeviceRowId String? @map("evicted_device_row_id") @db.Uuid

    /// Wann Email mit Approval-Link/Code verschickt wurde (Rate-Limit: 1x pro Request).
    emailSentAt DateTime? @map("email_sent_at")
    /// One-Time-Token für Approval via Email-Link (statt App-Approval).
    /// 32-char hex. NULL bis email-fallback getriggert.
    emailToken  String?   @unique @map("email_token")

    createdAt DateTime @default(now()) @map("created_at")
    /// Approval läuft nach 10 Minuten ab.
    expiresAt DateTime @map("expires_at")

    @@index([userId, status])
    @@index([userId, createdAt(sort: Desc)])
    @@map("device_approval_requests")
    @@schema("rebreak")
}

// Multi-Device DNS-Schutz: Legend-User können bis zu 3 Geräte (Mac/iOS/Android/Windows)
// mit einem per-Device DoH-Token schützen. Token wird in die mobileconfig/DoH-URL
// eingebettet — ist die einzige Auth für den DoH-Server (Phase 2).
model ProtectedDevice {
    id             String    @id @default(uuid()) @db.Uuid
    userId         String    @map("user_id") @db.Uuid
    /// Per-Device DoH-Token, embedded in mobileconfig ServerURL.
    /// Format: 32-char cryptographic-random hex.
    dnsToken       String    @unique @map("dns_token")
    /// "mac" | "windows" | "ios" | "android"
    platform       String
    /// User-friendly label, z.B. "MacBook Pro" oder "Olfas iPhone"
    label          String
    /// pending (enrolled, profile not installed yet) | active (user confirmed install) | degraded (plan downgrade — Grace läuft) | revoked
    status         String    @default("pending")
    /// User confirmed install via App (not server-side verified yet — DoH-routing kommt in Phase 2)
    installedAt    DateTime? @map("installed_at")
    /// Optional: DoH-server pingt das später (Phase 2, separater Sprint)
    lastDnsQueryAt DateTime? @map("last_dns_query_at")
    /// Gesetzt wenn Plan-Downgrade das Gerät auf degraded setzt. Nach 14d Passthrough.
    /// Bei Re-Upgrade: zurück auf active, degradedAt=null.
    degradedAt     DateTime? @map("degraded_at")
    revokedAt      DateTime? @map("revoked_at")
    createdAt      DateTime  @default(now()) @map("created_at")
    updatedAt      DateTime  @updatedAt @map("updated_at")

    @@index([userId])
    @@index([dnsToken])
    @@map("protected_devices")
    @@schema("rebreak")
}

/// Short-lived PKCE state entries for Microsoft OAuth flow.
/// Created by POST /api/mail/oauth/microsoft/init, consumed + deleted by
/// POST /api/mail/oauth/microsoft/callback.
/// TTL: 10 minutes — entries older than that are rejected and garbage-collected.
model OauthPendingState {
    id           String   @id @default(uuid()) @db.Uuid
    /// Random 128-bit hex string used as CSRF state parameter in auth URL.
    stateId      String   @unique @map("state_id")
    userId       String   @map("user_id") @db.Uuid
    /// PKCE code_verifier (random 43-128 char string, S256 method).
    /// Never leaves the server — only used for token exchange.
    codeVerifier String   @map("code_verifier")
    /// Optional: pre-filled email for login_hint parameter.
    email        String?
    createdAt    DateTime @default(now()) @map("created_at")

    @@index([stateId])
    @@index([createdAt])
    @@map("oauth_pending_states")
    @@schema("rebreak")
}

/// Client error / crash reports submitted by users or captured automatically.
/// Intentionally minimal PII: no emails, passwords, tokens, or message content.
/// Used for debugging during test phases and high-traffic rollouts.
model ErrorReport {
    id          String   @id @default(uuid()) @db.Uuid
    source      String /// "native" | "magic" | "web" | "manual" | "auto"
    severity    String /// "error" | "crash" | "warning" | "info"
    title       String
    message     String
    stack       String?
    context     Json? /// free-form structured context (screen, device info, etc.)
    deviceInfo  Json? @map("device_info") /// platform, osVersion, appVersion, model
    profileId   String?  @map("profile_id") @db.Uuid
    /// Original client timestamp if provided, otherwise server time.
    reportedAt  DateTime @map("reported_at")
    createdAt   DateTime @default(now()) @map("created_at")

    @@index([source])
    @@index([severity])
    @@index([createdAt])
    @@index([profileId])
    @@map("error_reports")
    @@schema("rebreak")
}
