-- BlindMaster schema. Idempotent: safe on fresh DB and replays cleanly against
-- an existing one. Source of truth for table structure, constraints, indexes,
-- and triggers. Restored from oldVersion.sql backup + newer additions.

-- ── Trigger: auto-delete empty groups ───────────────────────────────────────
CREATE OR REPLACE FUNCTION public.delete_group_if_empty() RETURNS trigger
    LANGUAGE plpgsql AS $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM group_peripherals WHERE group_id = OLD.group_id) THEN
    DELETE FROM groups WHERE id = OLD.group_id;
  END IF;
  RETURN OLD;
END;
$$;

-- ── Core auth ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS users (
  id                    INTEGER      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email                 TEXT         NOT NULL UNIQUE,
  password_hash_string  TEXT         NOT NULL,
  name                  TEXT,
  verification_token    TEXT,
  is_verified           BOOLEAN      NOT NULL DEFAULT FALSE,
  timezone              TEXT         DEFAULT 'America/Chicago',
  apns_token            TEXT,
  created_at            TIMESTAMPTZ  DEFAULT NOW()
);

CREATE UNIQUE INDEX IF NOT EXISTS users_id_idx ON users(id);

-- UNLOGGED: ephemeral session state — no WAL writes, cleared on crash (correct
-- behaviour; clients reconnect anyway). One row per user enforced by PK.
CREATE UNLOGGED TABLE IF NOT EXISTS user_tokens (
  user_id    INTEGER  PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  token      TEXT     NOT NULL,
  connected  BOOLEAN  DEFAULT FALSE,
  socket     VARCHAR(255)
);

-- Convert to UNLOGGED if the table already exists as a logged table.
-- user_tokens holds only live socket state; clearing it on restart is correct.
DO $$ BEGIN
  IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'user_tokens' AND relpersistence = 'p') THEN
    ALTER TABLE user_tokens SET UNLOGGED;
  END IF;
END $$;

CREATE INDEX IF NOT EXISTS idx_user_tokens_token             ON user_tokens(token);
CREATE INDEX IF NOT EXISTS idx_user_tokens_active_socket     ON user_tokens(user_id, socket) WHERE connected = true;
CREATE INDEX IF NOT EXISTS idx_user_tokens_socket_connected  ON user_tokens(socket) WHERE connected = true;

CREATE TABLE IF NOT EXISTS password_reset_tokens (
  email       TEXT         PRIMARY KEY,
  token       TEXT         NOT NULL,
  expires_at  TIMESTAMPTZ  NOT NULL,
  created_at  TIMESTAMPTZ  DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS user_pending_emails (
  user_id        INTEGER      PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  pending_email  TEXT         NOT NULL,
  token          TEXT         NOT NULL,
  expires_at     TIMESTAMPTZ  NOT NULL
);

-- ── Hardware: hubs (devices) and their controlled blinds (peripherals) ───────
CREATE TABLE IF NOT EXISTS devices (
  id           INTEGER  GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id      INTEGER  NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  device_name  TEXT     NOT NULL,
  max_ports    INTEGER  NOT NULL DEFAULT 4,
  battery_soc  SMALLINT,
  timezone     TEXT,
  UNIQUE (user_id, device_name)
);

CREATE INDEX IF NOT EXISTS idx_devices_user_id   ON devices(user_id);
CREATE INDEX IF NOT EXISTS idx_devices_max_ports ON devices(max_ports);

-- UNLOGGED: ephemeral device connection state, same rationale as user_tokens.
CREATE UNLOGGED TABLE IF NOT EXISTS device_tokens (
  device_id  INTEGER      NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
  token      TEXT         NOT NULL,
  connected  BOOLEAN      DEFAULT FALSE,
  socket     VARCHAR(255)
);

DO $$ BEGIN
  IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'device_tokens' AND relpersistence = 'p') THEN
    ALTER TABLE device_tokens SET UNLOGGED;
  END IF;
END $$;

CREATE INDEX IF NOT EXISTS idx_device_tokens_token           ON device_tokens(token);
CREATE INDEX IF NOT EXISTS idx_device_tokens_device_id_connected ON device_tokens(device_id) WHERE connected = true;
CREATE INDEX IF NOT EXISTS idx_device_tokens_active_socket   ON device_tokens(device_id, socket) WHERE connected = true;
CREATE INDEX IF NOT EXISTS idx_device_tokens_socket_connected ON device_tokens(socket) WHERE connected = true;

-- autovacuum tuned aggressively: last_pos is updated on every blind movement.
CREATE TABLE IF NOT EXISTS peripherals (
  id                 INTEGER      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  device_id          INTEGER      NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
  user_id            INTEGER      NOT NULL REFERENCES users(id)   ON DELETE CASCADE,
  peripheral_number  INTEGER      NOT NULL,
  peripheral_name    TEXT         NOT NULL,
  calibrated         BOOLEAN      DEFAULT FALSE,
  await_calib        BOOLEAN      DEFAULT FALSE,
  last_pos           INTEGER      DEFAULT 0 CHECK (last_pos >= 0 AND last_pos <= 10),
  last_set           TIMESTAMPTZ,
  UNIQUE (device_id, peripheral_number),
  UNIQUE (device_id, peripheral_name)
) WITH (autovacuum_vacuum_scale_factor = 0.02);

CREATE INDEX IF NOT EXISTS idx_peripherals_device_id ON peripherals(device_id);
CREATE INDEX IF NOT EXISTS idx_peripherals_id_user   ON peripherals(id, user_id);

-- ── Multi-blind groups ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS groups (
  id          INTEGER      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id     INTEGER      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  name        TEXT         NOT NULL,
  timezone    TEXT,
  created_at  TIMESTAMPTZ  DEFAULT NOW(),
  UNIQUE (user_id, name)
);

CREATE INDEX IF NOT EXISTS idx_groups_user_id ON groups(user_id);

CREATE TABLE IF NOT EXISTS group_peripherals (
  group_id       INTEGER  NOT NULL REFERENCES groups(id)      ON DELETE CASCADE,
  peripheral_id  INTEGER  NOT NULL REFERENCES peripherals(id) ON DELETE CASCADE,
  PRIMARY KEY (group_id, peripheral_id)
);

CREATE INDEX IF NOT EXISTS idx_group_peripherals_group_id      ON group_peripherals(group_id);
CREATE INDEX IF NOT EXISTS idx_group_peripherals_peripheral_id ON group_peripherals(peripheral_id);

-- Trigger fires after any peripheral is removed from a group; deletes the group
-- itself if it is now empty.
DO $$ BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_cleanup_empty_groups'
  ) THEN
    CREATE TRIGGER trigger_cleanup_empty_groups
      AFTER DELETE ON group_peripherals
      FOR EACH ROW EXECUTE FUNCTION public.delete_group_if_empty();
  END IF;
END $$;

-- ── Schedules ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS schedules (
  id                INTEGER       GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id           INTEGER       NOT NULL REFERENCES users(id)       ON DELETE CASCADE,
  schedule_type     VARCHAR(50)   NOT NULL CHECK (schedule_type IN ('peripheral', 'group')),
  peripheral_id     INTEGER       REFERENCES peripherals(id)          ON DELETE CASCADE,
  device_id         INTEGER       REFERENCES devices(id)              ON DELETE CASCADE,
  peripheral_number INTEGER,
  group_id          INTEGER       REFERENCES groups(id)               ON DELETE CASCADE,
  target_position   INTEGER       NOT NULL,
  cron_expression   VARCHAR(100)  NOT NULL,
  cron_minute       VARCHAR(50)   NOT NULL,
  cron_hour         VARCHAR(50)   NOT NULL,
  cron_days         VARCHAR(50)   NOT NULL,
  created_at        TIMESTAMPTZ   DEFAULT CURRENT_TIMESTAMP,
  updated_at        TIMESTAMPTZ   DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT peripheral_or_group CHECK (
    (schedule_type = 'peripheral' AND peripheral_id IS NOT NULL AND group_id IS NULL) OR
    (schedule_type = 'group'      AND group_id IS NOT NULL      AND peripheral_id IS NULL)
  )
);

CREATE INDEX IF NOT EXISTS idx_schedules_user_id ON schedules(user_id);

-- Prevent duplicate cron times for the same target.
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_peripheral_schedule
  ON schedules(peripheral_id, cron_expression)
  WHERE schedule_type = 'peripheral';

CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_group_schedule
  ON schedules(group_id, cron_expression)
  WHERE schedule_type = 'group';

-- ── Idempotent column additions for older DBs ────────────────────────────────
ALTER TABLE users    ADD COLUMN IF NOT EXISTS apns_token       TEXT;
ALTER TABLE users    ADD COLUMN IF NOT EXISTS timezone         TEXT DEFAULT 'America/Chicago';
ALTER TABLE users    ADD COLUMN IF NOT EXISTS verification_token TEXT;
ALTER TABLE users    ADD COLUMN IF NOT EXISTS is_verified      BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users    ADD COLUMN IF NOT EXISTS created_at       TIMESTAMPTZ DEFAULT NOW();
ALTER TABLE devices  ADD COLUMN IF NOT EXISTS battery_soc      SMALLINT;
ALTER TABLE devices  ADD COLUMN IF NOT EXISTS timezone         TEXT;
ALTER TABLE groups   ADD COLUMN IF NOT EXISTS timezone         TEXT;
