-- EncryptID PostgreSQL Schema
-- Run once to initialize the database

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  display_name TEXT,
  did TEXT,
  email TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

-- Profile columns (added for user profile management)
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_email TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_email_is_recovery BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS wallet_address TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();

-- Email forwarding (Mailcow alias)
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_forward_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_forward_mailcow_id TEXT;

CREATE TABLE IF NOT EXISTS credentials (
  credential_id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  public_key TEXT NOT NULL,
  counter INTEGER DEFAULT 0,
  transports TEXT[],
  created_at TIMESTAMPTZ DEFAULT NOW(),
  last_used TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_credentials_user_id ON credentials(user_id);

CREATE TABLE IF NOT EXISTS challenges (
  challenge TEXT PRIMARY KEY,
  user_id TEXT,
  type TEXT NOT NULL CHECK (type IN ('registration', 'authentication', 'device_registration', 'wallet_link')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL
);

-- Auto-clean expired challenges (run periodically or use pg_cron)
CREATE INDEX IF NOT EXISTS idx_challenges_expires_at ON challenges(expires_at);

CREATE TABLE IF NOT EXISTS recovery_tokens (
  token TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  type TEXT NOT NULL CHECK (type IN ('email_verify', 'account_recovery')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  used BOOLEAN DEFAULT FALSE
);

CREATE INDEX IF NOT EXISTS idx_recovery_tokens_user_id ON recovery_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_expires_at ON recovery_tokens(expires_at);

-- Space membership: source of truth for user roles across the r*.online ecosystem
CREATE TABLE IF NOT EXISTS space_members (
  space_slug TEXT NOT NULL,
  user_did TEXT NOT NULL,
  role TEXT NOT NULL CHECK (role IN ('viewer', 'participant', 'moderator', 'admin')),
  joined_at TIMESTAMPTZ DEFAULT NOW(),
  granted_by TEXT,
  PRIMARY KEY (space_slug, user_did)
);

CREATE INDEX IF NOT EXISTS idx_space_members_user_did ON space_members(user_did);
CREATE INDEX IF NOT EXISTS idx_space_members_space_slug ON space_members(space_slug);

-- ============================================================================
-- GUARDIAN RECOVERY (2-of-3 social recovery)
-- ============================================================================

-- Guardians: up to 3 trusted contacts per account
CREATE TABLE IF NOT EXISTS guardians (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  name TEXT NOT NULL,                          -- display name (e.g. "Mom", "Alex")
  email TEXT,                                  -- for sending invite/approval emails
  guardian_user_id TEXT REFERENCES users(id),   -- if they have an EncryptID account
  status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked')),
  invite_token TEXT UNIQUE,                    -- token for invite link
  invite_expires_at TIMESTAMPTZ,
  accepted_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_guardians_user_id ON guardians(user_id);
CREATE INDEX IF NOT EXISTS idx_guardians_guardian_user_id ON guardians(guardian_user_id);
CREATE INDEX IF NOT EXISTS idx_guardians_invite_token ON guardians(invite_token);

-- Recovery requests: when a user needs to recover their account
CREATE TABLE IF NOT EXISTS recovery_requests (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'cancelled', 'completed', 'expired')),
  threshold INTEGER NOT NULL DEFAULT 2,        -- approvals needed (2-of-3)
  approval_count INTEGER NOT NULL DEFAULT 0,
  initiated_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,             -- 7 days to collect approvals
  completed_at TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_recovery_requests_user_id ON recovery_requests(user_id);

-- Recovery approvals: guardian votes on a recovery request
CREATE TABLE IF NOT EXISTS recovery_approvals (
  request_id TEXT NOT NULL REFERENCES recovery_requests(id) ON DELETE CASCADE,
  guardian_id TEXT NOT NULL REFERENCES guardians(id) ON DELETE CASCADE,
  approval_token TEXT UNIQUE,                  -- token sent via email/link for one-click approve
  approved_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (request_id, guardian_id)
);

CREATE INDEX IF NOT EXISTS idx_recovery_approvals_token ON recovery_approvals(approval_token);

-- Device link tokens: for linking a second device via QR or email
CREATE TABLE IF NOT EXISTS device_links (
  token TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,             -- 10 minutes
  used BOOLEAN DEFAULT FALSE
);

CREATE INDEX IF NOT EXISTS idx_device_links_user_id ON device_links(user_id);

-- ============================================================================
-- ENCRYPTED POSTAL ADDRESSES (zero-knowledge address storage)
-- ============================================================================

-- Addresses are encrypted client-side with passkey-derived AES-256-GCM keys.
-- The server stores opaque ciphertext + cleartext metadata (label, isDefault) for UI listing.
CREATE TABLE IF NOT EXISTS encrypted_addresses (
  id TEXT NOT NULL,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  ciphertext TEXT NOT NULL,
  iv TEXT NOT NULL,
  label TEXT NOT NULL CHECK (label IN ('home', 'work', 'shipping', 'billing', 'other')),
  label_custom TEXT,
  is_default BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_encrypted_addresses_user_id ON encrypted_addresses(user_id);

-- ============================================================================
-- ROLE RENAME: participant → member
-- ============================================================================

UPDATE space_members SET role = 'member' WHERE role = 'participant';
ALTER TABLE space_members DROP CONSTRAINT IF EXISTS space_members_role_check;
ALTER TABLE space_members ADD CONSTRAINT space_members_role_check
  CHECK (role IN ('viewer', 'member', 'moderator', 'admin'));

-- ============================================================================
-- SPACE INVITES
-- ============================================================================

CREATE TABLE IF NOT EXISTS space_invites (
  id TEXT PRIMARY KEY,
  space_slug TEXT NOT NULL,
  email TEXT,
  role TEXT NOT NULL DEFAULT 'member'
    CHECK (role IN ('viewer', 'member', 'moderator', 'admin')),
  token TEXT UNIQUE NOT NULL,
  invited_by TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending', 'accepted', 'expired', 'revoked')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  accepted_at TIMESTAMPTZ,
  accepted_by_did TEXT
);
CREATE INDEX IF NOT EXISTS idx_space_invites_token ON space_invites(token);
CREATE INDEX IF NOT EXISTS idx_space_invites_space ON space_invites(space_slug);

-- ============================================================================
-- NOTIFICATIONS
-- ============================================================================

CREATE TABLE IF NOT EXISTS notifications (
  id TEXT PRIMARY KEY,
  user_did TEXT NOT NULL,
  category TEXT NOT NULL CHECK (category IN ('space', 'module', 'system', 'social')),
  event_type TEXT NOT NULL,
  title TEXT NOT NULL,
  body TEXT,
  space_slug TEXT,
  module_id TEXT,
  action_url TEXT,
  actor_did TEXT,
  actor_username TEXT,
  metadata JSONB DEFAULT '{}',
  read BOOLEAN DEFAULT FALSE,
  dismissed BOOLEAN DEFAULT FALSE,
  delivered_ws BOOLEAN DEFAULT FALSE,
  delivered_email BOOLEAN DEFAULT FALSE,
  delivered_push BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  read_at TIMESTAMPTZ,
  expires_at TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_notif_user_unread ON notifications(user_did, read)
  WHERE NOT read AND NOT dismissed;
CREATE INDEX IF NOT EXISTS idx_notif_user_created ON notifications(user_did, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notif_expires ON notifications(expires_at)
  WHERE expires_at IS NOT NULL;

CREATE TABLE IF NOT EXISTS notification_preferences (
  user_did TEXT PRIMARY KEY,
  email_enabled BOOLEAN DEFAULT TRUE,
  push_enabled BOOLEAN DEFAULT TRUE,
  quiet_hours_start TEXT,
  quiet_hours_end TEXT,
  muted_spaces TEXT[] DEFAULT '{}',
  muted_categories TEXT[] DEFAULT '{}',
  digest_frequency TEXT DEFAULT 'none'
    CHECK (digest_frequency IN ('none', 'daily', 'weekly')),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- ============================================================================
-- FUND CLAIMS (on-ramp claim-via-EncryptID flow)
-- ============================================================================

CREATE TABLE IF NOT EXISTS fund_claims (
  id TEXT PRIMARY KEY,
  token TEXT UNIQUE NOT NULL,
  email_hash TEXT NOT NULL,
  email TEXT,                          -- nulled after claim or 7 days
  wallet_address TEXT NOT NULL,
  openfort_player_id TEXT,
  fiat_amount TEXT,
  fiat_currency TEXT DEFAULT 'USD',
  session_id TEXT,
  provider TEXT,                       -- 'coinbase' | 'transak'
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending', 'claimed', 'expired', 'resent')),
  claimed_by_user_id TEXT REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  claimed_at TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_fund_claims_token ON fund_claims(token);
CREATE INDEX IF NOT EXISTS idx_fund_claims_email_hash ON fund_claims(email_hash);
CREATE INDEX IF NOT EXISTS idx_fund_claims_expires ON fund_claims(expires_at);

-- ============================================================================
-- OIDC PROVIDER (Authorization Code flow for Postiz, etc.)
-- ============================================================================

CREATE TABLE IF NOT EXISTS oidc_clients (
  client_id TEXT PRIMARY KEY,
  client_secret TEXT NOT NULL,
  name TEXT NOT NULL,
  redirect_uris TEXT[] NOT NULL,
  allowed_emails TEXT[] DEFAULT '{}',  -- empty = unrestricted
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS oidc_auth_codes (
  code TEXT PRIMARY KEY,
  client_id TEXT NOT NULL REFERENCES oidc_clients(client_id),
  user_id TEXT NOT NULL REFERENCES users(id),
  redirect_uri TEXT NOT NULL,
  scope TEXT DEFAULT 'openid profile email',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  used BOOLEAN DEFAULT FALSE
);

CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_expires ON oidc_auth_codes(expires_at);

-- ============================================================================
-- IDENTITY INVITES ("Claim your rSpace" flow)
-- ============================================================================

CREATE TABLE IF NOT EXISTS identity_invites (
  id TEXT PRIMARY KEY,
  token TEXT UNIQUE NOT NULL,
  email TEXT NOT NULL,
  invited_by_user_id TEXT NOT NULL REFERENCES users(id),
  invited_by_username TEXT NOT NULL,
  message TEXT,                          -- optional personal message
  space_slug TEXT,                       -- auto-join this space on claim
  space_role TEXT DEFAULT 'member'
    CHECK (space_role IN ('viewer', 'member', 'moderator', 'admin')),
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending', 'claimed', 'expired', 'revoked')),
  claimed_by_user_id TEXT REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  claimed_at TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_identity_invites_token ON identity_invites(token);
CREATE INDEX IF NOT EXISTS idx_identity_invites_email ON identity_invites(email);
CREATE INDEX IF NOT EXISTS idx_identity_invites_invited_by ON identity_invites(invited_by_user_id);
CREATE INDEX IF NOT EXISTS idx_identity_invites_expires ON identity_invites(expires_at);

-- OIDC client invite: optional link to an OIDC client for "Accept Your Role" flow
ALTER TABLE identity_invites ADD COLUMN IF NOT EXISTS client_id TEXT REFERENCES oidc_clients(client_id);
CREATE INDEX IF NOT EXISTS idx_identity_invites_client_id ON identity_invites(client_id);

-- ============================================================================
-- CHALLENGES: extend type constraint to include 'wallet_link'
-- ============================================================================

ALTER TABLE challenges DROP CONSTRAINT IF EXISTS challenges_type_check;
ALTER TABLE challenges ADD CONSTRAINT challenges_type_check
  CHECK (type IN ('registration', 'authentication', 'device_registration', 'wallet_link'));

-- ============================================================================
-- LINKED EXTERNAL WALLETS (SIWE-verified wallet associations)
-- ============================================================================

-- Server stores encrypted blobs + address hash for dedup.
-- Cleartext wallet data is never stored server-side.
CREATE TABLE IF NOT EXISTS linked_wallets (
  id TEXT NOT NULL,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  ciphertext TEXT NOT NULL,
  iv TEXT NOT NULL,
  address_hash TEXT NOT NULL,
  source TEXT NOT NULL CHECK (source IN ('external-eoa', 'external-safe')),
  verified BOOLEAN DEFAULT FALSE,
  linked_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_linked_wallets_user_id ON linked_wallets(user_id);
CREATE INDEX IF NOT EXISTS idx_linked_wallets_address_hash ON linked_wallets(address_hash);

-- ============================================================================
-- PUSH SUBSCRIPTIONS (Web Push notifications)
-- ============================================================================

CREATE TABLE IF NOT EXISTS push_subscriptions (
  id TEXT PRIMARY KEY,
  user_did TEXT NOT NULL,
  endpoint TEXT NOT NULL,
  key_p256dh TEXT NOT NULL,
  key_auth TEXT NOT NULL,
  user_agent TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  last_used TIMESTAMPTZ,
  UNIQUE (user_did, endpoint)
);

CREATE INDEX IF NOT EXISTS idx_push_sub_user ON push_subscriptions(user_did);

-- Prevent duplicate wallet links per user (application-level check + DB enforcement)
DO $$ BEGIN
  ALTER TABLE linked_wallets ADD CONSTRAINT linked_wallets_user_address_unique
    UNIQUE (user_id, address_hash);
EXCEPTION WHEN duplicate_table THEN NULL;
END $$;
