-- ==========================================================================
--  Einkaufsliste – vollständiges Datenbankschema
--  Entspricht dem Stand nach Alembic-Revision 0013.
-- ==========================================================================
--
--  Normalerweise wird diese Datei NICHT gebraucht: Der api-Container
--  führt beim Start `alembic upgrade head` aus und legt alles selbst an.
--
--  Sie ist da für:
--    * Nachvollziehbarkeit im Versionsverwaltungssystem – man sieht auf
--      einen Blick, wie das Schema aussieht, ohne elf Migrationen zu lesen
--    * Aufsetzen ohne Alembic, etwa auf einer verwalteten Datenbank
--    * Vergleich, wenn eine Migration irgendwo halb durchgelaufen ist
--
--  Einspielen:
--    docker compose up -d db
--    docker compose exec -T db mariadb -u root -p einkaufsapp < db/schema.sql
--
--  Der Eintrag in `alembic_version` am Ende ist wichtig: Ohne ihn würde
--  Alembic beim nächsten Start alle Migrationen erneut anwenden und an
--  den bereits vorhandenen Tabellen scheitern.
--
--  Alle Bezeichner in Backticks, weil `key` und `count` in MariaDB
--  Schlüsselwörter sind.
-- ==========================================================================

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- --------------------------------------------------------------------------
--  Konten und Anmeldung
-- --------------------------------------------------------------------------

CREATE TABLE `user` (
  `id`                   VARCHAR(36)  NOT NULL,
  -- Normalisiert (kleingeschrieben, getrimmt). Die Eindeutigkeit steht
  -- auf diesem Wert, deshalb muss die Anwendung konsequent normalisieren.
  `email`                VARCHAR(255) NOT NULL,
  `display_name`         VARCHAR(80)  NULL,
  -- Argon2id
  `password_hash`        VARCHAR(255) NOT NULL,
  `verified_at`          DATETIME     NULL,
  `is_admin`             TINYINT(1)   NOT NULL DEFAULT 0,
  `is_active`            TINYINT(1)   NOT NULL DEFAULT 1,
  -- Sperrt alles außer "Profil lesen" und "Passwort ändern"
  `must_change_password` TINYINT(1)   NOT NULL DEFAULT 0,
  -- Grundlage der automatischen Deaktivierung. Wird bei der Anmeldung
  -- gesetzt und danach höchstens stündlich nachgeführt.
  `last_seen_at`         DATETIME     NULL,
  -- Seit wann deaktiviert. Grundlage der automatischen Löschung;
  -- is_active allein sagt nichts über die Dauer.
  `deactivated_at`       DATETIME     NULL,
  `created_at`           DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_user_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `user_session` (
  `id`           VARCHAR(36) NOT NULL,
  -- Nur der SHA-256-Hash des Sitzungstokens. Der Klartext steht
  -- ausschließlich im HttpOnly-Cookie des Browsers.
  `token_hash`   VARCHAR(64) NOT NULL,
  `user_id`      VARCHAR(36) NOT NULL,
  -- Double-Submit-Verfahren: steht zusätzlich in einem lesbaren Cookie
  -- und muss bei schreibenden Anfragen im Header wiederkommen.
  `csrf_token`   VARCHAR(64) NOT NULL,
  `created_at`   DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_seen_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `expires_at`   DATETIME    NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_user_session_token` (`token_hash`),
  KEY `ix_user_session_expires` (`expires_at`),
  KEY `fk_user_session_user` (`user_id`),
  CONSTRAINT `fk_user_session_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `email_token` (
  `id`         VARCHAR(36) NOT NULL,
  `token_hash` VARCHAR(64) NOT NULL,
  `user_id`    VARCHAR(36) NOT NULL,
  -- "verify" | "reset"
  `purpose`    VARCHAR(32) NOT NULL,
  `expires_at` DATETIME    NOT NULL,
  `used_at`    DATETIME    NULL,
  `created_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_email_token_token` (`token_hash`),
  KEY `ix_email_token_user_purpose` (`user_id`, `purpose`),
  CONSTRAINT `fk_email_token_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Laufende Änderung einer E-Mail-Adresse.
--
-- Bei gewöhnlichen Konten muss nur die NEUE Adresse bestätigen; die alte
-- bekommt eine Benachrichtigung. Bei Administratorkonten müssen BEIDE
-- bestätigen: Wer Zugriff auf ein Administratorkonto erlangt, könnte
-- sonst die Adresse auf eine eigene umstellen und sich dauerhaft
-- einnisten - der rechtmäßige Inhaber verlöre den Weg zurück über
-- "Passwort vergessen".
CREATE TABLE `email_change` (
  `id`               VARCHAR(36)  NOT NULL,
  `user_id`          VARCHAR(36)  NOT NULL,
  `old_email`        VARCHAR(255) NOT NULL,
  `new_email`        VARCHAR(255) NOT NULL,
  `token_new_hash`   VARCHAR(64)  NOT NULL,
  `token_old_hash`   VARCHAR(64)  NULL,
  -- True bei Administratorkonten
  `requires_old`     TINYINT(1)   NOT NULL DEFAULT 0,
  `confirmed_new_at` DATETIME     NULL,
  `confirmed_old_at` DATETIME     NULL,
  `applied_at`       DATETIME     NULL,
  `cancelled_at`     DATETIME     NULL,
  `requested_by`     VARCHAR(36)  NULL,
  `created_at`       DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `expires_at`       DATETIME     NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_email_change_token_new` (`token_new_hash`),
  UNIQUE KEY `uq_email_change_token_old` (`token_old_hash`),
  KEY `ix_email_change_user` (`user_id`),
  KEY `fk_email_change_requested_by` (`requested_by`),
  CONSTRAINT `fk_email_change_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_email_change_requested_by` FOREIGN KEY (`requested_by`)
    REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Betrieb
-- --------------------------------------------------------------------------

CREATE TABLE `setting` (
  `key`        VARCHAR(64)  NOT NULL,
  `value`      VARCHAR(255) NOT NULL,
  `updated_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
                            ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Zählerbasiertes Rate Limiting ohne Redis: ein Datensatz je
-- (Schlüssel, Zeitfenster).
CREATE TABLE `rate_limit` (
  `bucket`       VARCHAR(160) NOT NULL,
  `window_start` DATETIME     NOT NULL,
  `count`        INT          NOT NULL DEFAULT 0,
  PRIMARY KEY (`bucket`, `window_start`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Listen und Mitgliedschaften
-- --------------------------------------------------------------------------

CREATE TABLE `shopping_list` (
  `id`         VARCHAR(36)  NOT NULL,
  `name`       VARCHAR(120) NOT NULL,
  `owner_id`   VARCHAR(36)  NOT NULL,
  -- Monoton steigender Zähler je Liste. Jede Änderung erhöht ihn und
  -- schreibt den neuen Wert in row_rev der geänderten Zeile.
  `rev`        BIGINT       NOT NULL DEFAULT 0,
  `created_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
                            ON UPDATE CURRENT_TIMESTAMP,
  -- Soft Delete: Offline gebliebene Geräte sollen vom Löschen erfahren.
  `deleted_at` DATETIME     NULL,
  -- Vorwarnungen vor der automatischen Löschung, getrennt vermerkt,
  -- damit jede Stufe genau einmal verschickt wird
  `warn_7d_at` DATETIME     NULL,
  `warn_1d_at` DATETIME     NULL,
  PRIMARY KEY (`id`),
  KEY `fk_shopping_list_owner` (`owner_id`),
  -- RESTRICT: Ein Konto lässt sich nicht löschen, solange ihm Listen
  -- gehören. Sonst verwaisen sie.
  CONSTRAINT `fk_shopping_list_owner` FOREIGN KEY (`owner_id`)
    REFERENCES `user` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `list_member` (
  `list_id`          VARCHAR(36) NOT NULL,
  `user_id`          VARCHAR(36) NOT NULL,
  -- "owner" | "editor" | "viewer"
  `role`             VARCHAR(16) NOT NULL DEFAULT 'editor',
  -- Zusatzrecht, unabhängig von der Rolle
  `may_share_public` TINYINT(1)  NOT NULL DEFAULT 0,
  -- Archivierung ist eine Eigenschaft der MITGLIEDSCHAFT, nicht der
  -- Liste: Dieselbe Liste kann bei einem Mitglied im Archiv liegen und
  -- beim anderen aktiv sein.
  `archived_at`      DATETIME    NULL,
  -- Grundlage der automatischen Archivierung, je Person getrennt
  `last_opened_at`   DATETIME    NULL,
  `joined_at`        DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`list_id`, `user_id`),
  KEY `ix_list_member_user` (`user_id`),
  CONSTRAINT `fk_list_member_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_list_member_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Katalog: Märkte, Warengruppen, Artikel
-- --------------------------------------------------------------------------

CREATE TABLE `market` (
  `list_id`    VARCHAR(36)  NOT NULL,
  `id`         VARCHAR(36)  NOT NULL,
  `name`       VARCHAR(120) NOT NULL,
  -- Bestimmt die Reihenfolge in Liste und Ausdruck
  `sort_order` INT          NOT NULL DEFAULT 0,
  `row_rev`    BIGINT       NOT NULL DEFAULT 0,
  `deleted_at` DATETIME     NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_market_list_name` (`list_id`, `name`),
  KEY `ix_market_list_rev` (`list_id`, `row_rev`),
  CONSTRAINT `fk_market_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `category` (
  `list_id`    VARCHAR(36)  NOT NULL,
  `id`         VARCHAR(36)  NOT NULL,
  `name`       VARCHAR(120) NOT NULL,
  `sort_order` INT          NOT NULL DEFAULT 0,
  `row_rev`    BIGINT       NOT NULL DEFAULT 0,
  `deleted_at` DATETIME     NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_category_list_name` (`list_id`, `name`),
  KEY `ix_category_list_rev` (`list_id`, `row_rev`),
  CONSTRAINT `fk_category_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `article` (
  `id`                  VARCHAR(36)  NOT NULL,
  `list_id`             VARCHAR(36)  NOT NULL,
  `name`                VARCHAR(200) NOT NULL,
  `barcode`             VARCHAR(64)  NULL,
  `note`                VARCHAR(500) NULL,
  -- Vorgaben für neue Listeneinträge
  `default_market_id`   VARCHAR(36)  NULL,
  `default_category_id` VARCHAR(36)  NULL,
  `row_rev`             BIGINT       NOT NULL DEFAULT 0,
  `deleted_at`          DATETIME     NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_article_list_name` (`list_id`, `name`),
  KEY `ix_article_list_rev` (`list_id`, `row_rev`),
  KEY `ix_article_barcode` (`list_id`, `barcode`),
  KEY `fk_article_market` (`default_market_id`),
  KEY `fk_article_category` (`default_category_id`),
  CONSTRAINT `fk_article_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_article_market` FOREIGN KEY (`default_market_id`)
    REFERENCES `market` (`id`) ON DELETE SET NULL,
  CONSTRAINT `fk_article_category` FOREIGN KEY (`default_category_id`)
    REFERENCES `category` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Frei definierbare Eigenschaften: Verpackungseinheit, Farbe, Größe.
-- Spalte heißt attr_name, weil `key` reserviert ist.
CREATE TABLE `article_attribute` (
  `id`         VARCHAR(36)  NOT NULL,
  `article_id` VARCHAR(36)  NOT NULL,
  `attr_name`  VARCHAR(80)  NOT NULL,
  `attr_value` VARCHAR(300) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_article_attr` (`article_id`, `attr_name`),
  CONSTRAINT `fk_article_attribute_article` FOREIGN KEY (`article_id`)
    REFERENCES `article` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- In welchen Märkten ist der Artikel erhältlich
CREATE TABLE `article_market` (
  `article_id` VARCHAR(36) NOT NULL,
  `market_id`  VARCHAR(36) NOT NULL,
  PRIMARY KEY (`article_id`, `market_id`),
  KEY `fk_article_market_market` (`market_id`),
  CONSTRAINT `fk_article_market_article` FOREIGN KEY (`article_id`)
    REFERENCES `article` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_article_market_market` FOREIGN KEY (`market_id`)
    REFERENCES `market` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Listeneinträge
-- --------------------------------------------------------------------------

CREATE TABLE `list_item` (
  `id`          VARCHAR(36)   NOT NULL,
  `list_id`     VARCHAR(36)   NOT NULL,
  `article_id`  VARCHAR(36)   NOT NULL,
  -- Überschreiben die Vorgaben aus dem Artikel, falls gesetzt
  `market_id`   VARCHAR(36)   NULL,
  `category_id` VARCHAR(36)   NULL,
  -- Stückzahl: wie viele Gebinde. price_cents gilt für EINES davon,
  -- die Summe ist price_cents * count.
  `count`       INT           NOT NULL DEFAULT 1,
  -- Gebinde: wie groß eine Packung ist (500 ml, 250 g)
  `pack_size`   DECIMAL(10,3) NULL,
  `pack_unit`   VARCHAR(32)   NULL,
  -- Nähere Bestimmung, z. B. "bunt, ganz"
  `variant`     VARCHAR(200)  NULL,
  -- Bemerkung für den Einkaufenden
  `note`        VARCHAR(500)  NULL,
  -- "open" | "bought" | "deferred"
  `status`      VARCHAR(16)   NOT NULL DEFAULT 'open',
  -- Preis in Cent für EIN Gebinde. Niemals als Fließkommazahl rechnen.
  `price_cents` INT           NULL,
  `created_by`  VARCHAR(36)   NULL,
  `row_rev`     BIGINT        NOT NULL DEFAULT 0,
  `created_at`  DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at`  DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP
                              ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at`  DATETIME      NULL,
  PRIMARY KEY (`id`),
  KEY `ix_list_item_list_rev` (`list_id`, `row_rev`),
  KEY `ix_list_item_list_status` (`list_id`, `status`),
  KEY `fk_list_item_article` (`article_id`),
  KEY `fk_list_item_market` (`market_id`),
  KEY `fk_list_item_category` (`category_id`),
  KEY `fk_list_item_created_by` (`created_by`),
  CONSTRAINT `fk_list_item_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_list_item_article` FOREIGN KEY (`article_id`)
    REFERENCES `article` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_list_item_market` FOREIGN KEY (`market_id`)
    REFERENCES `market` (`id`) ON DELETE SET NULL,
  CONSTRAINT `fk_list_item_category` FOREIGN KEY (`category_id`)
    REFERENCES `category` (`id`) ON DELETE SET NULL,
  -- SET NULL: Der Eintrag bleibt, der Personenbezug verschwindet
  CONSTRAINT `fk_list_item_created_by` FOREIGN KEY (`created_by`)
    REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Teilen
-- --------------------------------------------------------------------------

CREATE TABLE `list_invite` (
  `id`          VARCHAR(36)  NOT NULL,
  `list_id`     VARCHAR(36)  NOT NULL,
  `email`       VARCHAR(255) NOT NULL,
  `role`        VARCHAR(16)  NOT NULL DEFAULT 'editor',
  -- Nur der Hash. Erneutes Senden erzeugt deshalb ein neues Token.
  `token_hash`  VARCHAR(64)  NOT NULL,
  `invited_by`  VARCHAR(36)  NULL,
  `created_at`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_sent_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `send_count`  INT          NOT NULL DEFAULT 1,
  `expires_at`  DATETIME     NOT NULL,
  `accepted_at` DATETIME     NULL,
  `accepted_by` VARCHAR(36)  NULL,
  `revoked_at`  DATETIME     NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_list_invite_token` (`token_hash`),
  KEY `ix_list_invite_list` (`list_id`),
  KEY `ix_list_invite_email` (`email`),
  KEY `fk_list_invite_invited_by` (`invited_by`),
  KEY `fk_list_invite_accepted_by` (`accepted_by`),
  CONSTRAINT `fk_list_invite_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_list_invite_invited_by` FOREIGN KEY (`invited_by`)
    REFERENCES `user` (`id`) ON DELETE SET NULL,
  CONSTRAINT `fk_list_invite_accepted_by` FOREIGN KEY (`accepted_by`)
    REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `public_share` (
  `id`             VARCHAR(36)  NOT NULL,
  `list_id`        VARCHAR(36)  NOT NULL,
  -- Nur der Hash: Ein Datenbankleck gibt keinen Zugriff auf die Listen.
  `token_hash`     VARCHAR(64)  NOT NULL,
  `label`          VARCHAR(120) NULL,
  -- Darf über diesen Link abgehakt werden?
  `allow_check`    TINYINT(1)   NOT NULL DEFAULT 1,
  `created_by`     VARCHAR(36)  NULL,
  `created_at`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  -- Pflicht: ein unbefristeter Link wäre ein dauerhaft offenes Fenster
  `expires_at`     DATETIME     NOT NULL,
  `revoked_at`     DATETIME     NULL,
  -- Grobe Nutzungsanzeige ohne IP-Adressen und ohne Zeitreihe
  `last_access_at` DATETIME     NULL,
  `access_count`   INT          NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_public_share_token` (`token_hash`),
  KEY `ix_public_share_list` (`list_id`),
  KEY `fk_public_share_created_by` (`created_by`),
  CONSTRAINT `fk_public_share_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_public_share_created_by` FOREIGN KEY (`created_by`)
    REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Synchronisation
-- --------------------------------------------------------------------------

-- Quittung für eine bereits verarbeitete Operation aus einer
-- Outbox-Warteschlange. Verhindert, dass aus einem verlorenen
-- "Butter hinzufügen" beim nächsten Versuch zweimal Butter wird.
CREATE TABLE `applied_op` (
  `op_id`      VARCHAR(64) NOT NULL,
  `list_id`    VARCHAR(36) NOT NULL,
  `user_id`    VARCHAR(36) NULL,
  `kind`       VARCHAR(32) NOT NULL,
  -- Bei item.create: die vergebene ID, damit der Client seinen
  -- vorläufigen Eintrag zuordnen kann
  `result_id`  VARCHAR(36) NULL,
  `rev`        BIGINT      NOT NULL DEFAULT 0,
  `created_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`op_id`),
  KEY `ix_applied_op_list` (`list_id`, `created_at`),
  KEY `fk_applied_op_user` (`user_id`),
  CONSTRAINT `fk_applied_op_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_applied_op_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Produktdaten und Preise
-- --------------------------------------------------------------------------

-- Zwischenspeicher für Abfragen bei Open Food Facts. Sorgt dafür, dass
-- jeder Strichcode höchstens einmal je Gültigkeitszeitraum nach außen
-- geht – und zwar vom Server, nicht vom Gerät des Nutzers.
CREATE TABLE `product_cache` (
  `barcode`    VARCHAR(64)   NOT NULL,
  `found`      TINYINT(1)    NOT NULL DEFAULT 0,
  `name`       VARCHAR(300)  NULL,
  `brand`      VARCHAR(200)  NULL,
  -- Rohtext wie "500 g" oder "6 x 33 cl"
  `package`    VARCHAR(120)  NULL,
  -- Daraus zerlegt
  `count`      INT           NULL,
  `pack_size`  DECIMAL(10,3) NULL,
  `pack_unit`  VARCHAR(32)   NULL,
  `source`     VARCHAR(32)   NOT NULL DEFAULT 'openfoodfacts',
  `fetched_at` DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`barcode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Beobachteter Preis. Bewusst OHNE Nutzerbezug: Aus "wer hat wann wo was
-- zu welchem Preis gekauft" ließe sich ein Bewegungs- und Konsumprofil
-- bilden. Für den Preisvergleich genügt Markt, Artikel und Zeitpunkt.
CREATE TABLE `price_point` (
  `id`          VARCHAR(36)   NOT NULL,
  `list_id`     VARCHAR(36)   NOT NULL,
  `article_id`  VARCHAR(36)   NOT NULL,
  `market_id`   VARCHAR(36)   NOT NULL,
  -- Preis für EIN Gebinde
  `price_cents` INT           NOT NULL,
  `pack_size`   DECIMAL(10,3) NULL,
  `pack_unit`   VARCHAR(32)   NULL,
  `recorded_at` DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `ix_price_article_market` (`article_id`, `market_id`, `recorded_at`),
  KEY `ix_price_list` (`list_id`, `recorded_at`),
  KEY `fk_price_point_market` (`market_id`),
  CONSTRAINT `fk_price_point_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_price_point_article` FOREIGN KEY (`article_id`)
    REFERENCES `article` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_price_point_market` FOREIGN KEY (`market_id`)
    REFERENCES `market` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Push-Benachrichtigungen
-- --------------------------------------------------------------------------

CREATE TABLE `push_subscription` (
  `id`              VARCHAR(36)  NOT NULL,
  `user_id`         VARCHAR(36)  NOT NULL,
  -- Vom Push-Dienst des Browserherstellers vergebene URL
  `endpoint`        VARCHAR(500) NOT NULL,
  -- Schlüsselmaterial für die Ende-zu-Ende-Verschlüsselung. Auch der
  -- Push-Dienst kann den Inhalt der Nachricht nicht lesen.
  `p256dh`          VARCHAR(200) NOT NULL,
  `auth`            VARCHAR(100) NOT NULL,
  `label`           VARCHAR(80)  NULL,
  `created_at`      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_success_at` DATETIME     NULL,
  `failure_count`   INT          NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_push_subscription_endpoint` (`endpoint`),
  KEY `ix_push_subscription_user` (`user_id`),
  CONSTRAINT `fk_push_subscription_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Grundlage der Zwei-Stunden-Drosselung
CREATE TABLE `notify_state` (
  `list_id`          VARCHAR(36) NOT NULL,
  `user_id`          VARCHAR(36) NOT NULL,
  `last_notified_at` DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`list_id`, `user_id`),
  KEY `fk_notify_state_user` (`user_id`),
  CONSTRAINT `fk_notify_state_list` FOREIGN KEY (`list_id`)
    REFERENCES `shopping_list` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_notify_state_user` FOREIGN KEY (`user_id`)
    REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- --------------------------------------------------------------------------
--  Migrationsstand
-- --------------------------------------------------------------------------
--
--  Ohne diesen Eintrag würde Alembic beim nächsten Start alle
--  Migrationen erneut anwenden und an den vorhandenen Tabellen
--  scheitern.

CREATE TABLE `alembic_version` (
  `version_num` VARCHAR(32) NOT NULL,
  PRIMARY KEY (`version_num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `alembic_version` (`version_num`) VALUES ('0013');

SET FOREIGN_KEY_CHECKS = 1;
