// Prisma schema — TrahKeluarga.com V1
//
// Built in T-001 from each module's `spec/<module>/tier-b-implementation.md`.
// One physical database (ADR-0002), four module-owned table groups. Prisma
// itself does not enforce module boundaries — cross-module FKs exist only
// for referential integrity (per spec/modules.md's cross-module coupling
// rule) and are commented below wherever they cross a module seam. Only
// each owning module's own application code is expected to write to its
// tables (see spec/modules.md, spec/README.md).
//
// Multi-tenancy (REQ-FAM-005): every Trah-scoped table carries a `trahId`
// column with an index so every later query can filter by it directly,
// without joining through `persons` first.

generator client {
  provider = "prisma-client-js"
  // Runtime queries go through @prisma/adapter-pg (see src/lib/prisma.ts)
  // instead of Prisma's default Rust query engine — the Rust engine's Tokio
  // runtime spawns a worker thread per reported CPU core, which exceeds the
  // process/thread ceiling on constrained shared hosting (observed as
  // "OS can't spawn worker thread: Resource temporarily unavailable" panics
  // in production on Rumahweb's cPanel/CloudLinux LVE). driverAdapters runs
  // queries through the plain-JS `pg` driver instead, with no native
  // multi-threaded runtime at request time. `prisma migrate`/`db push` still
  // use the Rust schema-engine locally, which is unaffected (dev-only).
  // (Stable as of Prisma 6.19 — no previewFeatures flag needed.)
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  // Neon-specific: migrations run better over a direct (non-pooled)
  // connection than through the pgbouncer-style pooler DATABASE_URL uses
  // at runtime — Prisma+Neon's documented recommendation.
  directUrl = env("DIRECT_URL")
}

// ---------------------------------------------------------------------------
// Module 1: Identity & Access (IAM)
// Owns: User, Session, PasswordResetToken.
// ---------------------------------------------------------------------------

model User {
  id           String   @id @default(uuid())
  email        String?  @unique
  phone        String?  @unique
  passwordHash String   @map("password_hash")
  displayName  String?  @map("display_name")
  createdAt    DateTime @default(now()) @map("created_at")
  updatedAt    DateTime @updatedAt @map("updated_at")

  sessions            Session[]
  passwordResetTokens PasswordResetToken[]

  // Cross-module: read by Trah & Membership (Trah.createdByUserId,
  // Membership.userId) and Audit (AuditLog.actorUserId) for referential
  // integrity only — those modules never write to `users`.
  createdTrahs    Trah[]
  memberships     Membership[]
  auditLogEntries AuditLog[]

  @@map("users")
}

model Session {
  id           String   @id @default(uuid())
  userId       String   @map("user_id")
  sessionToken String   @unique @map("session_token")
  expiresAt    DateTime @map("expires_at")
  createdAt    DateTime @default(now()) @map("created_at")

  user User @relation(fields: [userId], references: [id])

  @@index([userId])
  @@map("sessions")
}

model PasswordResetToken {
  id        String    @id @default(uuid())
  userId    String    @map("user_id")
  codeHash  String    @map("code_hash")
  expiresAt DateTime  @map("expires_at")
  usedAt    DateTime? @map("used_at")
  createdAt DateTime  @default(now()) @map("created_at")

  user User @relation(fields: [userId], references: [id])

  @@index([userId])
  @@map("password_reset_tokens")
}

// ---------------------------------------------------------------------------
// Module 2: Trah & Membership
// Owns: Trah, Membership, Invitation.
// ---------------------------------------------------------------------------

enum MembershipRole {
  OWNER
  ADMIN
  EDITOR
  CONTRIBUTOR
  MEMBER
  VIEWER
}

enum MembershipStatus {
  ACTIVE
  REMOVED
}

enum InvitationStatus {
  PENDING
  ACCEPTED
  EXPIRED
  REVOKED
}

model Trah {
  id              String   @id @default(uuid())
  name            String
  // Cross-module FK: Trah & Membership (owner) -> IAM (User), read-only ref.
  createdByUserId String   @map("created_by_user_id")
  createdAt       DateTime @default(now()) @map("created_at")

  createdByUser User         @relation(fields: [createdByUserId], references: [id])
  memberships   Membership[]
  invitations   Invitation[]

  // Cross-module: read by Genealogy (Person.trahId, Relationship.trahId)
  // and Audit (AuditLog.trahId) for referential integrity only.
  persons         Person[]
  relationships   Relationship[]
  auditLogEntries AuditLog[]

  @@map("trahs")
}

model Membership {
  id              String           @id @default(uuid())
  trahId          String           @map("trah_id")
  // Cross-module FK: Trah & Membership (owner) -> IAM (User), read-only ref.
  userId          String           @map("user_id")
  role            MembershipRole
  // Cross-module FK: Trah & Membership (owner) -> Genealogy (Person),
  // read-only ref used to record the User<->Person "claim" link
  // (INV-TM-4: at most one active claim per Person).
  claimedPersonId String?          @unique @map("claimed_person_id")
  status          MembershipStatus @default(ACTIVE)
  joinedAt        DateTime         @default(now()) @map("joined_at")

  trah          Trah    @relation(fields: [trahId], references: [id])
  user          User    @relation(fields: [userId], references: [id])
  claimedPerson Person? @relation("MembershipClaimedPerson", fields: [claimedPersonId], references: [id])

  invitationsSent Invitation[]

  // Cross-module: read by Genealogy (Person.createdByMembershipId,
  // Relationship.createdByMembershipId, PersonFact.createdByMembershipId)
  // and Audit (AuditLog.actorMembershipId) for referential integrity only.
  createdPersons       Person[]
  createdPersonFacts   PersonFact[]
  createdRelationships Relationship[]
  auditLogEntries      AuditLog[]

  @@unique([trahId, userId]) // INV-TM-5
  @@index([trahId])
  @@index([userId])
  @@map("memberships")
}

model Invitation {
  id                  String           @id @default(uuid())
  trahId              String           @map("trah_id")
  inviterMembershipId String           @map("inviter_membership_id")
  targetEmail         String?          @map("target_email")
  targetPhone         String?          @map("target_phone")
  token               String           @unique
  proposedRole        MembershipRole   @map("proposed_role")
  status              InvitationStatus @default(PENDING)
  createdAt           DateTime         @default(now()) @map("created_at")
  expiresAt           DateTime         @map("expires_at")

  trah              Trah       @relation(fields: [trahId], references: [id])
  inviterMembership Membership @relation(fields: [inviterMembershipId], references: [id])

  @@index([trahId])
  @@map("invitations")
}

// ---------------------------------------------------------------------------
// Module 3: Genealogy
// Owns: Person, PersonFact, Residence, Contact, Relationship.
// ---------------------------------------------------------------------------

enum Gender {
  MALE
  FEMALE
  UNKNOWN
}

enum LifeStatus {
  ALIVE
  DECEASED
  UNKNOWN
}

enum PersonFactField {
  FULL_NAME
  NICKNAME
  BIRTH_DATE
  BIRTH_PLACE
  DEATH_DATE
  DEATH_PLACE
}

enum PrecisionQualifier {
  EXACT
  YEAR_ONLY
  MONTH_YEAR
  CIRCA
  BEFORE
  AFTER
}

enum VerificationStatus {
  VERIFIED
  PROBABLE
  UNCERTAIN
  DISPUTED
}

enum ContactType {
  WHATSAPP
  PHONE
  EMAIL
  OTHER
}

enum ContactVisibility {
  ONLY_ME
  FAMILY
  PUBLIC
}

enum RelationshipType {
  BIOLOGICAL_PARENT_CHILD
  ADOPTIVE_PARENT_CHILD
  STEP_PARENT_CHILD
  PARTNER
  SIBLING
}

enum PartnerStatus {
  MARRIED
  DIVORCED
  WIDOWED
  ENDED
}

model Person {
  id                    String     @id @default(uuid())
  // Cross-module FK: Genealogy (owner) -> Trah & Membership (Trah).
  trahId                String     @map("trah_id")
  displayName           String     @map("display_name")
  gender                Gender     @default(UNKNOWN)
  lifeStatus            LifeStatus @default(UNKNOWN) @map("life_status")
  // Cross-module FK: Genealogy (owner) -> Trah & Membership (Membership).
  createdByMembershipId String     @map("created_by_membership_id")
  createdAt             DateTime   @default(now()) @map("created_at")
  updatedAt             DateTime   @updatedAt @map("updated_at")

  trah                Trah       @relation(fields: [trahId], references: [id])
  createdByMembership Membership @relation(fields: [createdByMembershipId], references: [id])

  facts      PersonFact[]
  residences Residence[]
  contacts   Contact[]

  relationshipsAsA Relationship[] @relation("RelationshipPersonA")
  relationshipsAsB Relationship[] @relation("RelationshipPersonB")

  // Cross-module: read by Trah & Membership (Membership.claimedPersonId)
  // for referential integrity only.
  claimedByMembership Membership? @relation("MembershipClaimedPerson")

  @@index([trahId])
  @@map("persons")
}

model PersonFact {
  id                    String              @id @default(uuid())
  personId              String              @map("person_id")
  // Denormalized from persons.trahId (judgment call, see T-001 report):
  // spec lists this table without its own trah_id column, but every
  // Trah-scoped table needs one per REQ-FAM-005/the multi-tenancy rule so
  // later reads can filter directly without joining through `persons`.
  trahId                String              @map("trah_id")
  field                 PersonFactField
  value                 Json
  precisionQualifier    PrecisionQualifier? @map("precision_qualifier")
  verificationStatus    VerificationStatus  @default(VERIFIED) @map("verification_status")
  source                String?
  isPreferred           Boolean             @default(true) @map("is_preferred")
  // Cross-module FK: Genealogy (owner) -> Trah & Membership (Membership).
  createdByMembershipId String              @map("created_by_membership_id")
  createdAt             DateTime            @default(now()) @map("created_at")

  person              Person     @relation(fields: [personId], references: [id])
  createdByMembership Membership @relation(fields: [createdByMembershipId], references: [id])

  @@index([trahId])
  @@index([personId, field])
  @@map("person_facts")
}

model Residence {
  id        String    @id @default(uuid())
  personId  String    @map("person_id")
  // Denormalized from persons.trahId — same judgment call as PersonFact.
  trahId    String    @map("trah_id")
  place     String
  startDate DateTime  @map("start_date") @db.Date
  endDate   DateTime? @map("end_date") @db.Date
  isCurrent Boolean   @default(false) @map("is_current")

  person Person @relation(fields: [personId], references: [id])

  @@index([trahId])
  @@index([personId])
  @@map("residences")
}

model Contact {
  id         String            @id @default(uuid())
  personId   String            @map("person_id")
  // Denormalized from persons.trahId — same judgment call as PersonFact.
  trahId     String            @map("trah_id")
  type       ContactType
  value      String
  visibility ContactVisibility
  createdAt  DateTime          @default(now()) @map("created_at")

  person Person @relation(fields: [personId], references: [id])

  @@index([trahId])
  @@index([personId])
  @@map("contacts")
}

model Relationship {
  id                    String           @id @default(uuid())
  // Cross-module FK: Genealogy (owner) -> Trah & Membership (Trah).
  trahId                String           @map("trah_id")
  personAId             String           @map("person_a_id")
  personBId             String           @map("person_b_id")
  type                  RelationshipType
  startDate             DateTime?        @map("start_date") @db.Date
  endDate               DateTime?        @map("end_date") @db.Date
  partnerStatus         PartnerStatus?   @map("partner_status")
  // Cross-module FK: Genealogy (owner) -> Trah & Membership (Membership).
  createdByMembershipId String           @map("created_by_membership_id")
  createdAt             DateTime         @default(now()) @map("created_at")
  updatedAt             DateTime         @updatedAt @map("updated_at")

  trah                Trah       @relation(fields: [trahId], references: [id])
  personA             Person     @relation("RelationshipPersonA", fields: [personAId], references: [id])
  personB             Person     @relation("RelationshipPersonB", fields: [personBId], references: [id])
  createdByMembership Membership @relation(fields: [createdByMembershipId], references: [id])

  // Prevents duplicate identical directed edges for the non-PARTNER types.
  // PARTNER is deliberately excluded here (REQ-REL-002 needs multiple
  // PARTNER rows for the same pair, concurrently or over time) — see the
  // separate partial unique index in the generated migration SQL, since
  // Prisma's declarative `@@unique` cannot express the `where type = ...`
  // partial-index condition from the spec; both partial uniques
  // (non-PARTNER dedupe, PARTNER dedupe) are added by hand-editing the
  // generated migration SQL.
  @@index([trahId])
  @@index([personAId])
  @@index([personBId])
  @@map("relationships")
}

// ---------------------------------------------------------------------------
// Module 4: Audit
// Owns: AuditLog.
// ---------------------------------------------------------------------------

enum AuditEntityType {
  PERSON
  RELATIONSHIP
}

enum AuditAction {
  CREATE
  UPDATE
}

model AuditLog {
  id                String          @id @default(uuid())
  // Cross-module FK: Audit (owner) -> Trah & Membership (Trah).
  trahId            String          @map("trah_id")
  entityType        AuditEntityType @map("entity_type")
  entityId          String          @map("entity_id")
  // Cross-module FK: Audit (owner) -> IAM (User).
  actorUserId       String          @map("actor_user_id")
  // Cross-module FK: Audit (owner) -> Trah & Membership (Membership).
  actorMembershipId String          @map("actor_membership_id")
  action            AuditAction
  changes           Json
  createdAt         DateTime        @default(now()) @map("created_at")

  trah            Trah       @relation(fields: [trahId], references: [id])
  actorUser       User       @relation(fields: [actorUserId], references: [id])
  actorMembership Membership @relation(fields: [actorMembershipId], references: [id])

  @@index([trahId, entityType, entityId, createdAt])
  @@map("audit_log")
}
