generator client {
  provider = "prisma-client-js"
}

datasource db {
  // Swap to "postgresql" for production scale — just change provider + DATABASE_URL.
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

// Categories are their own table so you can add/rename/remove them from the admin
// panel. Product.category stays a plain string (matching Category.slug) rather
// than a foreign key, so nothing breaks if a category is renamed after the fact.
model Category {
  id        String   @id @default(cuid())
  name      String // display label, e.g. "Phone Cases"
  slug      String   @unique // stored key, e.g. "PHONE_CASE" — matches Product.category
  showOnPersonalised Boolean @default(true) // appears as a filter on /made-to-order
  showOnProducts     Boolean @default(true) // appears as a filter on /products
  createdAt DateTime @default(now())
}

model Product {
  id          String   @id @default(cuid())
  slug        String   @unique
  name        String
  category    String
  description String
  basePrice   Int // cents — used for ready-made items on /products
  personalisedPrice Int? // cents — used for made-to-order items on /made-to-order; falls back to basePrice if not set
  colors      String // JSON array of hex strings, e.g. ["#17181C","#FBF9F5"]
  colorPhotos String? // JSON object mapping hex -> photo data URL, e.g. {"#17181C":"data:..."}.
  colorPhotosBack String? // same idea, for the back view of each color
  // Lets a product show the real photo for whichever color is selected, instead of
  // tinting one photo. Falls back to imageUrl/photoRecolorable/mockup for any color
  // that doesn't have its own photo here.
  sizes       String? // JSON array of size labels, e.g. ["S","M","L","XL"] — leave null for products with no sizing (mugs, phone cases)
  mockup      String // which SVG mockup component to render, e.g. "tshirt" | "hoodie" | "mug" | "phonecase"
  printArea   String // JSON: {"xPct":..,"yPct":..,"wPct":..,"hPct":..}
  printAreaBack String? // same shape, for the back print area — only used when imageUrlBack is set
  imageUrl    String? // optional real product photo — shown instead of the illustration when set
  imageUrlBack String? // optional back-view photo, toggled alongside the front photo
  photoRecolorable Boolean @default(false) // if true, the photo is treated as a grayscale
  // "shading map" and multiplied by the selected color swatch live in the browser,
  // instead of being shown as a fixed image. Only looks right for black/white/gray
  // source photos — recoloring an already-colored photo produces muddy results.
  showOnPersonalised Boolean @default(true) // list on /made-to-order
  showOnProducts     Boolean @default(false) // list on /products — same product/photo can show on both
  createdAt   DateTime @default(now())

  salePrice    Int? // cents — set alongside a date window below to put this product on sale
  saleStartsAt DateTime? // leave null to start the sale immediately
  saleEndsAt   DateTime? // leave null for an open-ended sale

  orderItems      OrderItem[]
  proofs          DesignProof[]
  customRequests  CustomRequest[]
  promotions      Promotion[] // only relevant to promotions scoped to specific items, not "all items"
  collections     Collection[] // occasion/recipient tags, e.g. "Christmas", "For Teachers"
  manualSaleItems ManualSaleItem[]
  stockLevels     StockLevel[]
  purchaseItems   PurchaseItem[]
  productImages   ProductImage[]
}

// Bulk-uploaded product images with auto-detected colors. Supports any product type
// (tees, hoodies, mugs, etc.). User uploads a folder, colors are extracted, then saved
// as color-swatch references for the product's color palette.
model ProductImage {
  id            String   @id @default(cuid())
  productId     String
  product       Product  @relation(fields: [productId], references: [id], onDelete: Cascade)
  color         String // hex color detected from the image, e.g. "#1EA7E0"
  imageDataUrl  String // the uploaded image itself as a data URL
  createdAt     DateTime @default(now())

  @@unique([productId, color])
}

// Quantity on hand for one product/colour/size combination. Purchases increase
// it, paid website orders and manual sales decrease it. Info-only: counts can
// go negative (shown highlighted in admin as "your records are out of sync")
// and never block anything on the shop side.
model StockLevel {
  id        String   @id @default(cuid())
  productId String
  product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)
  color     String
  size      String   @default("") // '' = product has no sizes — a real empty value, not NULL, so the unique index below actually dedupes (SQLite treats NULLs as distinct)
  quantity  Int      @default(0)
  updatedAt DateTime @updatedAt

  @@unique([productId, color, size])
}

// Stock coming in: one supplier invoice, entered by hand with the invoice
// file (PDF or photo) attached for record-keeping. Product lines add to
// StockLevel; material lines (ink, vinyl, packaging) are just logged — they
// have no running count, and will feed the expenses list in a later stage.
// Every Purchase auto-creates an Expense on creation.
model Purchase {
  id              String   @id @default(cuid())
  supplierName    String
  purchasedAt     DateTime @default(now()) // invoice date, editable — not entry time
  invoiceDataUrl  String? // the uploaded invoice itself, as a data URL
  invoiceFileName String?
  total           Int // pence — sum of line totals, recomputed server-side
  notes           String?
  createdAt       DateTime @default(now())

  items     PurchaseItem[]
  expense   Expense? // auto-created on Purchase creation; cascade delete if Purchase is deleted
}

model PurchaseItem {
  id          String   @id @default(cuid())
  purchaseId  String
  purchase    Purchase @relation(fields: [purchaseId], references: [id], onDelete: Cascade)
  productId   String? // null = loose material line (no stock tracking)
  product     Product? @relation(fields: [productId], references: [id])
  description String // product name snapshot, or the material description
  color       String?
  size        String?
  quantity    Int
  unitCost    Int // pence
}

// Expenses: everything that goes out. Auto-created from purchases (category =
// "Stock purchases"), plus manual entries for postage, utilities, equipment, etc.
// Receipt can be the invoice file or a standalone attachment. Pence storage like
// everything else.
model Expense {
  id            String   @id @default(cuid())
  date          DateTime @default(now())
  description   String
  category      String   @default("Stock purchases") // "Stock purchases", "Postage", others as user enters them
  amount        Int // pence
  receiptDataUrl String? // uploaded receipt as data URL
  receiptFileName String?
  notes         String?
  purchaseId    String? @unique // null = standalone expense; set = auto-created from a Purchase (one-to-one)
  purchase      Purchase? @relation(fields: [purchaseId], references: [id], onDelete: SetNull)
  createdAt     DateTime @default(now())
}

// A design you (the shop owner) upload for one specific customer to review and buy —
// separate from the self-serve design tool. You create one of these per custom job,
// send the customer the link, and they approve it straight into their cart.
model DesignProof {
  id            String   @id @default(cuid())
  productId     String
  product       Product  @relation(fields: [productId], references: [id])
  customerName  String?
  customerEmail String
  color         String
  quantity      Int      @default(1)
  unitPrice     Int // cents — defaults to the product's base price if not overridden
  imageDataUrl  String // the finished design image you uploaded, shown as-is to the customer
  note          String? // optional message shown alongside the design
  status        String   @default("PENDING") // PENDING | APPROVED
  createdAt     DateTime @default(now())

  messages Message[]
}

// One chat thread per design proof — simple polling-based chat, no websockets needed.
model Message {
  id        String      @id @default(cuid())
  proofId   String
  proof     DesignProof @relation(fields: [proofId], references: [id], onDelete: Cascade)
  sender    String // "ADMIN" | "CUSTOMER"
  body      String
  createdAt DateTime    @default(now())
}

model Order {
  id              String      @id @default(cuid())
  stripeSessionId String?     @unique
  email           String?
  shippingAddress String? // JSON — collected by Stripe Checkout, saved once payment completes
  status          String      @default("PENDING") // PENDING | PAID | FAILED
  archived        Boolean     @default(false)
  total           Int // cents
  createdAt       DateTime    @default(now())
  updatedAt       DateTime    @updatedAt

  customerId String? // set when the buyer was logged in at checkout — null for guest orders
  customer   Customer? @relation(fields: [customerId], references: [id])

  guestName  String? // full name for guest orders
  guestPhone String? // contact phone for guest orders

  items OrderItem[]
}

// A customer's own account — separate from the ADMIN_USER/ADMIN_PASSWORD admin login.
// Optional: checkout still works as a guest with no Customer record at all.
model Customer {
  id           String   @id @default(cuid())
  email        String   @unique
  passwordHash String
  name         String?
  createdAt    DateTime @default(now())

  orders             Order[]
  passwordResetToken PasswordResetToken?
  customRequests     CustomRequest[]
  manualSales        ManualSale[]
}

// A sale that didn't go through the website checkout — cash at a fair, a bank
// transfer arranged over Facebook, etc. Recorded by the admin from
// /admin/accounts so every sale (web + offline) lives in one ledger.
model ManualSale {
  id            String   @id @default(cuid())
  soldAt        DateTime @default(now()) // when the sale actually happened (editable, not just entry time)
  buyerName     String? // free text — a cash buyer doesn't need an account
  buyerEmail    String?
  customerId    String? // auto-linked when buyerEmail matches a Customer account
  customer      Customer? @relation(fields: [customerId], references: [id])
  paymentMethod String // CASH | BANK_TRANSFER
  total         Int // pence — sum of line totals, computed server-side on save
  notes         String?
  createdAt     DateTime @default(now())

  items ManualSaleItem[]
}

model ManualSaleItem {
  id          String     @id @default(cuid())
  saleId      String
  sale        ManualSale @relation(fields: [saleId], references: [id], onDelete: Cascade)
  productId   String? // null for free-text one-offs (e.g. "custom commission")
  product     Product?   @relation(fields: [productId], references: [id])
  description String // product name snapshot, or the free text itself
  color       String?
  size        String?
  quantity    Int
  unitPrice   Int // pence
}

// One active reset token per customer at a time — requesting a new one replaces it.
model PasswordResetToken {
  id         String   @id @default(cuid())
  customerId String   @unique
  customer   Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
  token      String   @unique
  expiresAt  DateTime
  createdAt  DateTime @default(now())
}

// A customer's own photo, submitted for the shop to design something with —
// separate from the self-serve design tool and from DesignProof (which is the
// finished result you send back once you've worked on it). No FK link between
// the two: turning a request into a proof is a one-time action, not a tracked relation.
model CustomRequest {
  id            String    @id @default(cuid())
  customerId    String?
  customer      Customer? @relation(fields: [customerId], references: [id])
  productId     String
  product       Product   @relation(fields: [productId], references: [id])
  customerName  String
  customerEmail String
  customerPhone String?
  imageDataUrl  String // the photo they want us to work with
  note          String?
  status        String    @default("NEW") // NEW | DONE
  createdAt     DateTime  @default(now())
}

// A promotion: shown as a banner on every page while its date window is active,
// AND actually discounts prices — either every product ("ALL") or a hand-picked
// set ("SPECIFIC", via the products relation below). Kept as a list rather than
// a single settings row so past promotions aren't destroyed when you create a
// new one. Price discounts stack with a product's own manual sale price by
// taking whichever is lower — see getEffectivePrice in src/lib/pricing.ts.
model Promotion {
  id              String    @id @default(cuid())
  message         String
  discountPercent Int // 1-100
  scope           String    @default("ALL") // ALL | SPECIFIC
  startsAt        DateTime? // leave null to start immediately
  endsAt          DateTime? // leave null for an open-ended promotion
  createdAt       DateTime  @default(now())

  products Product[] // only consulted when scope = SPECIFIC
}

// Occasion ("Christmas", "Birthdays") and recipient ("Teachers", "School leavers") tags
// for the two nav dropdowns of the same name. One model with a group discriminator
// (rather than two models) so there's a single set of admin pages — split into
// sections by group instead. A product can carry any number of both, hence the
// many-to-many (unlike Product.category, which is a single string).
model Collection {
  id        String   @id @default(cuid())
  name      String
  slug      String   @unique
  group     String // "OCCASION" | "RECIPIENT"
  createdAt DateTime @default(now())
  products  Product[]
}

// Unified mailing list — fed by three separate sources (customer registration,
// the footer newsletter signup, and guest checkout emails) so a single admin
// broadcast can reach all of them without querying three different shapes.
// `subscribed` is the admin's opt-out switch and is deliberately never
// overwritten by a later upsert from any source, so it always wins.
model MailingListEntry {
  id         String   @id @default(cuid())
  email      String   @unique
  name       String?
  source     String // "CUSTOMER" | "NEWSLETTER" | "GUEST" — where this address first came from
  subscribed Boolean  @default(true)
  createdAt  DateTime @default(now())
}

// Backs a small DB-based rate limiter (src/lib/rateLimit.ts) — chosen over an
// in-memory counter because that would silently reset on every serverless
// cold start and not share state across instances. Rows older than 24h are
// opportunistically pruned by the limiter itself, no separate cron needed.
model RateLimitAttempt {
  id        String   @id @default(cuid())
  key       String // e.g. "login:customer:<ip>", "checkout:<ip>"
  createdAt DateTime @default(now())

  @@index([key, createdAt])
}

// Site-wide settings — a single row (fixed id "singleton"). Currently just the
// maintenance-mode toggle: when on, customers see a "we're updating" page while
// logged-in admins still see the real site. Read on every page render (cheap
// single-row lookup), written only from the admin maintenance page.
model SiteSetting {
  id                 String  @id @default("singleton")
  maintenanceMode    Boolean @default(false)
  maintenanceMessage String?
}

// Footer email signup — just captures addresses for you to use once you pick an
// email tool (Mailchimp, Klaviyo, etc.). Not wired up to send anything itself.
model NewsletterSubscriber {
  id        String   @id @default(cuid())
  email     String   @unique
  createdAt DateTime @default(now())
}

// Photo gallery of completed work, shown on the public /gallery page. Photos are
// uploaded manually in the admin (base64 data URLs, same as products/proofs).
// Categories here are their own thing, separate from product Category — a photo's
// category is optional. (Future: an official Facebook Graph API import could add a
// nullable source/attribution column here — not built yet.)
model GalleryCategory {
  id        String         @id @default(cuid())
  name      String
  slug      String         @unique
  createdAt DateTime       @default(now())
  photos    GalleryPhoto[]
}

model GalleryPhoto {
  id           String           @id @default(cuid())
  imageDataUrl String // base64 data URL
  caption      String?
  categoryId   String? // nullable — a photo can be uncategorized
  category     GalleryCategory? @relation(fields: [categoryId], references: [id])
  createdAt    DateTime         @default(now())
}

model OrderItem {
  id                   String  @id @default(cuid())
  orderId              String
  order                Order   @relation(fields: [orderId], references: [id], onDelete: Cascade)
  productId            String
  product              Product @relation(fields: [productId], references: [id])
  productName          String // snapshot at time of order, in case the product changes later
  quantity             Int
  color                String
  size                 String?
  unitPrice            Int // cents
  designJson           String // serialized {front:[...], back:[...]} design elements
  designPreviewUrl     String // base64 PNG data URL — front design, transparent, print-ready
  designPreviewUrlBack String? // same, for the back design if there is one
  placementPreviewUrl     String? // front design shown composited on the actual product photo, for reference — where does this go when printing
  placementPreviewUrlBack String? // same, for the back
}

// Audit log for admin logins — tracks when the admin panel is accessed
model AdminLoginLog {
  id        String   @id @default(cuid())
  loginAt   DateTime @default(now())
  logoutAt  DateTime? // null = still logged in (or session expired without explicit logout)
  ipAddress String?
  userAgent String?
  success   Boolean  @default(true) // false for failed login attempts
}

// Audit log for customer logins — tracks customer account access
model CustomerLoginLog {
  id        String   @id @default(cuid())
  customerId String?
  email     String   // captured for failed attempts where customer doesn't exist
  loginAt   DateTime @default(now())
  ipAddress String?
  userAgent String?
  success   Boolean  @default(true) // false for failed login attempts
}

// Audit log for financial transactions — tracks all changes to financial data
model FinancialAuditLog {
  id          String   @id @default(cuid())
  action      String   // CREATE, UPDATE, DELETE
  entityType  String   // "ORDER", "MANUAL_SALE", "PURCHASE", "EXPENSE"
  entityId    String   // ID of the order/sale/purchase/expense
  changes     String   // JSON: {field: {old: X, new: Y}, ...}
  amount      Int?     // cents, for quick filtering of high-value changes
  changedAt   DateTime @default(now())
  changedBy   String?  // "SYSTEM" or IP address if admin
  reason      String?  // why was this changed
}

// Bank statement uploaded for reconciliation
model BankStatement {
  id              String                @id @default(cuid())
  filename        String                // original filename
  bankName        String?               // e.g., "HSBC", "Barclays"
  accountNumber   String?               // last 4 digits for safety
  statementMonth  DateTime              // the month this statement covers
  startDate       DateTime              // first transaction date in statement
  endDate         DateTime              // last transaction date in statement
  totalTransactions Int                 // count of lines in statement
  totalAmount     Int?                  // cents, total of all transactions (for verification)
  status          String    @default("PENDING") // PENDING, IN_PROGRESS, RECONCILED, ARCHIVED
  uploadedAt      DateTime  @default(now())
  reconciliedAt   DateTime? // when reconciliation was completed
  lines           BankStatementLine[]
  reconciliations BankReconciliation[]
}

// Individual transaction line from a bank statement
model BankStatementLine {
  id              String   @id @default(cuid())
  statementId     String
  statement       BankStatement @relation(fields: [statementId], references: [id], onDelete: Cascade)
  date            DateTime  // transaction date
  description     String    // e.g., "STRIPE DEPOSIT", "TRANSFER TO SAVINGS"
  amount          Int       // cents, positive for credit, negative for debit
  balance         Int?      // account balance after this transaction (if provided)
  reference       String?   // transaction reference from bank
  matched         Boolean   @default(false) // whether this line has been reconciled
  reconciliation  BankReconciliation? // the matched transaction, if any
  createdAt       DateTime  @default(now())
}

// Link between system transaction (order/sale/expense) and bank statement line
model BankReconciliation {
  id                  String   @id @default(cuid())
  statementId         String
  statement           BankStatement @relation(fields: [statementId], references: [id], onDelete: Cascade)
  bankLineId          String   @unique
  bankLine            BankStatementLine @relation(fields: [bankLineId], references: [id], onDelete: Cascade)

  // Which system transaction this was matched to
  transactionType     String   // "ORDER", "MANUAL_SALE", "REFUND", "EXPENSE", "TRANSFER_OUT", "FEE", "OTHER"
  transactionId       String?  // ID of the order/sale/expense if applicable
  description         String   // what the match was (e.g., "Order #12345", "Stripe fee")
  systemAmount        Int      // cents, amount in our system
  bankAmount          Int      // cents, amount on bank statement
  amountMatches       Boolean  // whether system and bank amounts are equal

  matchedAt           DateTime @default(now())
  matchedBy           String?  // IP/admin who manually matched this (null if auto-matched)
  confidence          Int      @default(100) // 0-100, how confident the match is (100=exact amount+date, <100=fuzzy match)
  notes               String?  // manual notes about the match
}
