-- ============================================================================
-- RBAC Schema: User Roles Table
-- Assigns roles to users within a tenant
-- ============================================================================

-- ============================================================================
-- USER_ROLES TABLE
-- Maps users to their assigned roles
-- ============================================================================

CREATE TABLE IF NOT EXISTS rbac.user_roles (
    -- Primary key
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- User reference
    user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,

    -- Role reference
    role_id UUID NOT NULL REFERENCES rbac.roles(id) ON DELETE CASCADE,

    -- Tenant (denormalized for RLS efficiency)
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,

    -- Assignment metadata
    is_primary BOOLEAN NOT NULL DEFAULT false,
    assigned_reason TEXT,

    -- Validity period (optional for temporary roles)
    valid_from TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    valid_until TIMESTAMPTZ DEFAULT NULL,

    -- Status
    is_active BOOLEAN NOT NULL DEFAULT true,

    -- Audit fields
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    assigned_by UUID REFERENCES users.users(id),
    revoked_at TIMESTAMPTZ DEFAULT NULL,
    revoked_by UUID REFERENCES users.users(id),

    -- Constraints
    CONSTRAINT uq_user_role UNIQUE (user_id, role_id),
    CONSTRAINT chk_valid_period CHECK (valid_until IS NULL OR valid_until > valid_from)
);

-- ============================================================================
-- INDEXES
-- ============================================================================

CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON rbac.user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON rbac.user_roles(role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_tenant_id ON rbac.user_roles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_is_active ON rbac.user_roles(is_active);
CREATE INDEX IF NOT EXISTS idx_user_roles_is_primary ON rbac.user_roles(user_id, is_primary) WHERE is_primary = true;
CREATE INDEX IF NOT EXISTS idx_user_roles_validity ON rbac.user_roles(valid_from, valid_until) WHERE is_active = true;

-- ============================================================================
-- ROW LEVEL SECURITY
-- ============================================================================

ALTER TABLE rbac.user_roles ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only see role assignments in their tenant
CREATE POLICY user_roles_tenant_isolation ON rbac.user_roles
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

-- ============================================================================
-- TRIGGERS
-- ============================================================================

-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION rbac.update_user_roles_timestamp()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_user_roles_updated_at
    BEFORE UPDATE ON rbac.user_roles
    FOR EACH ROW
    EXECUTE FUNCTION rbac.update_user_roles_timestamp();

-- Ensure only one primary role per user
CREATE OR REPLACE FUNCTION rbac.ensure_single_primary_role()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.is_primary = true THEN
        UPDATE rbac.user_roles
        SET is_primary = false
        WHERE user_id = NEW.user_id
          AND id != NEW.id
          AND is_primary = true;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_single_primary_role
    BEFORE INSERT OR UPDATE ON rbac.user_roles
    FOR EACH ROW
    WHEN (NEW.is_primary = true)
    EXECUTE FUNCTION rbac.ensure_single_primary_role();

-- ============================================================================
-- VIEW: User with roles and permissions
-- ============================================================================

CREATE OR REPLACE VIEW rbac.v_user_permissions AS
SELECT DISTINCT
    ur.user_id,
    ur.tenant_id,
    u.email,
    u.display_name,
    r.id AS role_id,
    r.name AS role_name,
    r.slug AS role_slug,
    r.hierarchy_level,
    ur.is_primary,
    p.code AS permission_code,
    p.module,
    p.action,
    p.resource,
    rp.grant_type
FROM rbac.user_roles ur
JOIN users.users u ON ur.user_id = u.id
JOIN rbac.roles r ON ur.role_id = r.id
JOIN rbac.role_permissions rp ON r.id = rp.role_id
JOIN rbac.permissions p ON rp.permission_id = p.id
WHERE ur.is_active = true
  AND r.is_active = true
  AND p.is_active = true
  AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP)
  AND ur.valid_from <= CURRENT_TIMESTAMP;

-- ============================================================================
-- FUNCTION: Check if user has permission
-- ============================================================================

CREATE OR REPLACE FUNCTION rbac.user_has_permission(
    p_user_id UUID,
    p_tenant_id UUID,
    p_permission_code VARCHAR(100)
)
RETURNS BOOLEAN AS $$
DECLARE
    v_has_allow BOOLEAN;
    v_has_deny BOOLEAN;
BEGIN
    -- Check for explicit deny first
    SELECT EXISTS (
        SELECT 1
        FROM rbac.v_user_permissions
        WHERE user_id = p_user_id
          AND tenant_id = p_tenant_id
          AND permission_code = p_permission_code
          AND grant_type = 'deny'
    ) INTO v_has_deny;

    IF v_has_deny THEN
        RETURN false;
    END IF;

    -- Check for allow
    SELECT EXISTS (
        SELECT 1
        FROM rbac.v_user_permissions
        WHERE user_id = p_user_id
          AND tenant_id = p_tenant_id
          AND permission_code = p_permission_code
          AND grant_type = 'allow'
    ) INTO v_has_allow;

    RETURN v_has_allow;
END;
$$ LANGUAGE plpgsql STABLE;

-- ============================================================================
-- FUNCTION: Get user's effective permissions
-- ============================================================================

CREATE OR REPLACE FUNCTION rbac.get_user_permissions(
    p_user_id UUID,
    p_tenant_id UUID
)
RETURNS TABLE (
    permission_code VARCHAR(100),
    permission_name VARCHAR(200),
    module VARCHAR(50),
    action VARCHAR(20),
    resource VARCHAR(100)
) AS $$
BEGIN
    RETURN QUERY
    SELECT DISTINCT
        p.code,
        p.name,
        p.module,
        p.action,
        p.resource
    FROM rbac.user_roles ur
    JOIN rbac.roles r ON ur.role_id = r.id
    JOIN rbac.role_permissions rp ON r.id = rp.role_id
    JOIN rbac.permissions p ON rp.permission_id = p.id
    WHERE ur.user_id = p_user_id
      AND ur.tenant_id = p_tenant_id
      AND ur.is_active = true
      AND r.is_active = true
      AND p.is_active = true
      AND rp.grant_type = 'allow'
      AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP)
      AND ur.valid_from <= CURRENT_TIMESTAMP
      AND NOT EXISTS (
          -- Exclude denied permissions
          SELECT 1
          FROM rbac.user_roles ur2
          JOIN rbac.role_permissions rp2 ON ur2.role_id = rp2.role_id
          WHERE ur2.user_id = p_user_id
            AND ur2.tenant_id = p_tenant_id
            AND rp2.permission_id = p.id
            AND rp2.grant_type = 'deny'
            AND ur2.is_active = true
      )
    ORDER BY p.module, p.code;
END;
$$ LANGUAGE plpgsql STABLE;

-- ============================================================================
-- FUNCTION: Assign role to user
-- ============================================================================

CREATE OR REPLACE FUNCTION rbac.assign_role_to_user(
    p_user_id UUID,
    p_role_id UUID,
    p_tenant_id UUID,
    p_assigned_by UUID,
    p_is_primary BOOLEAN DEFAULT false,
    p_valid_until TIMESTAMPTZ DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
    v_assignment_id UUID;
BEGIN
    INSERT INTO rbac.user_roles (
        user_id, role_id, tenant_id, is_primary,
        valid_until, assigned_by
    ) VALUES (
        p_user_id, p_role_id, p_tenant_id, p_is_primary,
        p_valid_until, p_assigned_by
    )
    ON CONFLICT (user_id, role_id) DO UPDATE SET
        is_active = true,
        is_primary = EXCLUDED.is_primary,
        valid_until = EXCLUDED.valid_until,
        assigned_by = EXCLUDED.assigned_by,
        revoked_at = NULL,
        revoked_by = NULL,
        updated_at = CURRENT_TIMESTAMP
    RETURNING id INTO v_assignment_id;

    RETURN v_assignment_id;
END;
$$ LANGUAGE plpgsql;

-- ============================================================================
-- GRANTS
-- ============================================================================

GRANT SELECT, INSERT, UPDATE, DELETE ON rbac.user_roles TO trading_user;
GRANT SELECT ON rbac.v_user_permissions TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.user_has_permission TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.get_user_permissions TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.assign_role_to_user TO trading_user;

-- ============================================================================
-- COMMENTS
-- ============================================================================

COMMENT ON TABLE rbac.user_roles IS 'Maps users to their assigned roles within a tenant';
COMMENT ON COLUMN rbac.user_roles.is_primary IS 'Primary role shown in UI, user can have multiple roles';
COMMENT ON COLUMN rbac.user_roles.valid_from IS 'When the role assignment becomes active';
COMMENT ON COLUMN rbac.user_roles.valid_until IS 'When the role assignment expires (NULL = never)';
COMMENT ON VIEW rbac.v_user_permissions IS 'Flattened view of users with their effective permissions';
COMMENT ON FUNCTION rbac.user_has_permission IS 'Check if a user has a specific permission';
COMMENT ON FUNCTION rbac.get_user_permissions IS 'Get all effective permissions for a user';
