-- ============================================================================
-- 0095_time_tracking_smartsheet_sync.sql
--
-- Cycle 5 — Per-brand Smartsheet integration for Time Tracking, mirroring
-- the water-log smartsheet pattern from migration 0093. Three tables:
--
--   1. time_tracking_smartsheet_config      — one row per brand; encrypted
--                                              token, sheet id, column
--                                              mapping, frequency, enable
--                                              flag, last-sync metadata
--   2. time_tracking_smartsheet_sync_queue  — one row per clock-out that
--                                              needs syncing; tracks
--                                              attempts + status for retry
--                                              and observability
--   3. time_tracking_smartsheet_sync_log    — append-only log of every
--                                              sync attempt (success OR
--                                              failure); backs the
--                                              "Recent Activity" panel
--
-- Security:
--   - API token is AES-256-GCM encrypted at rest using a key from
--     SMARTSHEET_TOKEN_ENC_KEY. The token NEVER leaves the server in
--     plaintext via any server action / API response.
--   - All tables brand-scoped via the existing `app.current_brand_id`
--     GUC + `current_brand_id()` function (see 0001_init.sql).
--
-- Cycle 5 scope:
--   This migration ONLY introduces schema + RLS. The actual sync
--   service, server actions, and admin UI live in their own files.
--   The brand will fill in sheet_id + token + column mapping when
--   the customer is ready; everything works end-to-end once they do.
-- ============================================================================

-- ── 1. Config table ────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_smartsheet_config (
  brand_id UUID PRIMARY KEY REFERENCES brands(id) ON DELETE CASCADE,

  -- The numeric Smartsheet sheet ID (not the share URL).
  -- UI accepts either form and parses to numeric here.
  sheet_id TEXT NOT NULL,

  -- AES-256-GCM encrypted API token (stored as BYTEA so we can store
  -- raw bytes, not base64). Use the helpers in src/lib/crypto.ts.
  encrypted_api_token BYTEA NOT NULL,
  token_iv BYTEA NOT NULL,         -- 12 random bytes per encrypt
  token_auth_tag BYTEA NOT NULL,   -- 16-byte GCM auth tag

  -- Maps our internal time-tracking fields → Smartsheet column IDs.
  -- Shape: { log_id: string, clock_in: string, clock_out: string,
  --          worker: string, task: string, hours: string,
  --          lunch_minutes: string | null, notes: string | null }
  -- `log_id` and `clock_in` are required (used for dedup).
  column_mapping JSONB NOT NULL,

  -- 'realtime' | 'every_15_minutes' | 'hourly'
  sync_frequency TEXT NOT NULL DEFAULT 'hourly'
    CHECK (sync_frequency IN ('realtime','every_15_minutes','hourly')),

  sync_enabled BOOLEAN NOT NULL DEFAULT false,

  -- Set after every sync attempt (success or failure).
  last_sync_at TIMESTAMPTZ,
  last_sync_error TEXT,

  -- User IDs from `neon_auth.user`; stored as text so we don't
  -- require a FK to a specific auth backend.
  created_by TEXT,
  updated_by TEXT,

  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Auto-bump updated_at on row UPDATE.
DROP TRIGGER IF EXISTS time_tracking_smartsheet_config_set_updated_at ON time_tracking_smartsheet_config;
CREATE TRIGGER time_tracking_smartsheet_config_set_updated_at
  BEFORE UPDATE ON time_tracking_smartsheet_config
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();

-- RLS: brand-scoped.
ALTER TABLE time_tracking_smartsheet_config ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation ON time_tracking_smartsheet_config;
CREATE POLICY tenant_isolation ON time_tracking_smartsheet_config FOR ALL
  USING (brand_id = current_brand_id() OR is_platform_admin())
  WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());

-- ── 2. Sync queue (one row per clock-out awaiting sync) ────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_smartsheet_sync_queue (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,

  -- FK to the clock-out. ON DELETE CASCADE means deleting a log row
  -- also removes its queue row, preventing orphaned sync attempts.
  log_id UUID NOT NULL REFERENCES time_tracking_logs(id) ON DELETE CASCADE,

  -- The Smartsheet row ID returned from a successful sync.
  -- NULL = not yet synced.
  smartsheet_row_id TEXT,

  -- 'pending' | 'syncing' | 'synced' | 'failed'
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending','syncing','synced','failed')),

  -- Bumped on every attempt (success or failure). Caps at 5 (after
  -- which the row stays 'failed' permanently; admin must re-enable).
  attempts INT NOT NULL DEFAULT 0,

  -- Last failure reason (sanitized — token never appears here).
  last_error TEXT,

  -- When the next retry is allowed. Set to NOW() initially; pushed
  -- out by exponential backoff on failure (2^attempts minutes, cap 1h).
  next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  synced_at TIMESTAMPTZ,

  CONSTRAINT time_tracking_smartsheet_queue_log_unique UNIQUE (brand_id, log_id)
);

-- Hot path: drain query scans `(brand_id, status, next_attempt_at)`.
CREATE INDEX IF NOT EXISTS time_tracking_smartsheet_queue_brand_status_idx
  ON time_tracking_smartsheet_sync_queue (brand_id, status, next_attempt_at);

-- Recent-attempts view.
CREATE INDEX IF NOT EXISTS time_tracking_smartsheet_queue_brand_created_idx
  ON time_tracking_smartsheet_sync_queue (brand_id, created_at DESC);

ALTER TABLE time_tracking_smartsheet_sync_queue ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation ON time_tracking_smartsheet_sync_queue;
CREATE POLICY tenant_isolation ON time_tracking_smartsheet_sync_queue FOR ALL
  USING (brand_id = current_brand_id() OR is_platform_admin())
  WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());

-- ── 3. Sync log (append-only, backs Recent Activity UI) ────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_smartsheet_sync_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,

  log_id UUID REFERENCES time_tracking_logs(id) ON DELETE SET NULL,
  smartsheet_row_id TEXT,

  -- 'sync' | 'retry' | 'skip' (dedup hit) | 'queue'
  action TEXT NOT NULL
    CHECK (action IN ('sync','retry','skip','queue')),

  success BOOLEAN NOT NULL,
  error TEXT,
  duration_ms INT,

  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS time_tracking_smartsheet_log_brand_recent_idx
  ON time_tracking_smartsheet_sync_log (brand_id, created_at DESC);

ALTER TABLE time_tracking_smartsheet_sync_log ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation ON time_tracking_smartsheet_sync_log;
CREATE POLICY tenant_isolation ON time_tracking_smartsheet_sync_log FOR ALL
  USING (brand_id = current_brand_id() OR is_platform_admin())
  WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());

-- ── 4. Helper view: latest sync per log row (for dedup decisions) ─────────

CREATE OR REPLACE VIEW time_tracking_smartsheet_latest_sync AS
SELECT DISTINCT ON (q.brand_id, q.log_id)
  q.brand_id,
  q.log_id,
  q.smartsheet_row_id,
  q.status,
  q.attempts,
  q.last_error,
  q.next_attempt_at,
  q.created_at AS queued_at,
  q.synced_at
FROM time_tracking_smartsheet_sync_queue q
ORDER BY q.brand_id, q.log_id, q.created_at DESC;

COMMENT ON TABLE time_tracking_smartsheet_config IS
  'Per-brand Smartsheet integration config for Time Tracking. Token is AES-256-GCM encrypted.';
COMMENT ON TABLE time_tracking_smartsheet_sync_queue IS
  'One row per time_tracking_log awaiting / completed sync to Smartsheet.';
COMMENT ON TABLE time_tracking_smartsheet_sync_log IS
  'Append-only audit of Time Tracking ↔ Smartsheet sync attempts.';
